Skip to content

Commit cc57855

Browse files
authored
Merge pull request #14 from 7a6163/fix/non-utf8-subtitle-encoding
Add encoding_rs and chardetng to detect and decode non-UTF-8 files
2 parents 28255ff + 3f0bceb commit cc57855

4 files changed

Lines changed: 99 additions & 5 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "time"]
2424
zip = "2"
2525
quick-xml = "0.36"
2626
tempfile = "3"
27+
encoding_rs = "0.8"
28+
chardetng = "0.1"
2729

2830
[build-dependencies]
2931
tauri-build = { version = "2", features = [] }

src-tauri/src/commands.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::build_service_info;
22
use crate::epub;
33
use crate::{
44
API_BASE, ApiResponse, ConvertEpubParams, ConvertFileParams, ConvertFileResult, EpubProgress,
5-
HttpClient, ServiceInfo, build_api_params, build_output_name, check_file_size,
5+
HttpClient, ServiceInfo, build_api_params, build_output_name, check_file_size, decode_text,
66
resolve_output_dir, validate_api_response,
77
};
88
use std::path::Path;
@@ -88,10 +88,12 @@ pub async fn convert_file(
8888
.map_err(|e| format!("FILE_METADATA_FAILED:{e}"))?;
8989
check_file_size(metadata.len())?;
9090

91-
// Read the file
92-
let content = tokio::fs::read_to_string(&canonical)
91+
// Read the file as raw bytes and decode with charset detection so that
92+
// non-UTF-8 subtitle files (Big5, GBK, Shift_JIS, UTF-16, ...) are handled.
93+
let raw = tokio::fs::read(&canonical)
9394
.await
9495
.map_err(|e| format!("FILE_READ_FAILED:{e}"))?;
96+
let (content, _encoding) = decode_text(&raw);
9597

9698
// Build API params
9799
let params = build_api_params(
@@ -170,9 +172,10 @@ pub async fn preview_convert(
170172
.await
171173
.map_err(|e| format!("FILE_METADATA_FAILED:{e}"))?;
172174
check_file_size(metadata.len())?;
173-
let content = tokio::fs::read_to_string(&canonical)
175+
let raw = tokio::fs::read(&canonical)
174176
.await
175177
.map_err(|e| format!("FILE_READ_FAILED:{e}"))?;
178+
let (content, _encoding) = decode_text(&raw);
176179

177180
let api_params = build_api_params(
178181
&content,

src-tauri/src/lib.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ pub(crate) fn check_file_size(len: u64) -> Result<(), String> {
258258
Ok(())
259259
}
260260

261+
/// Decode raw file bytes into a UTF-8 `String`, detecting the source encoding.
262+
///
263+
/// A leading BOM (UTF-8 / UTF-16) is honoured first; otherwise the charset is
264+
/// guessed with `chardetng` (the detector Firefox uses), covering common
265+
/// subtitle encodings such as Big5, GBK/GB18030, Shift_JIS and EUC. Returns the
266+
/// decoded text along with the detected encoding's canonical name.
267+
pub(crate) fn decode_text(bytes: &[u8]) -> (String, &'static str) {
268+
if let Some((enc, _)) = encoding_rs::Encoding::for_bom(bytes) {
269+
let (text, _, _) = enc.decode(bytes);
270+
return (text.into_owned(), enc.name());
271+
}
272+
273+
let mut detector = chardetng::EncodingDetector::new();
274+
detector.feed(bytes, true);
275+
let enc = detector.guess(None, true);
276+
let (text, _, _) = enc.decode(bytes);
277+
(text.into_owned(), enc.name())
278+
}
279+
261280
pub(crate) fn build_service_info(info: ServiceInfoResponse) -> Result<ServiceInfo, String> {
262281
if info.code != 0 {
263282
return Err(format!("SERVICE_INFO_FAILED:{}", info.code));
@@ -318,6 +337,54 @@ mod tests {
318337
assert_eq!(sanitize_filename_part("a!@#b$%^c"), "abc");
319338
}
320339

340+
// --- decode_text ---
341+
342+
#[test]
343+
fn decode_plain_utf8() {
344+
let (text, _) = decode_text("繁化姬".as_bytes());
345+
assert_eq!(text, "繁化姬");
346+
}
347+
348+
#[test]
349+
fn decode_utf8_with_bom() {
350+
let mut bytes = vec![0xEF, 0xBB, 0xBF];
351+
bytes.extend_from_slice("字幕".as_bytes());
352+
let (text, _) = decode_text(&bytes);
353+
assert_eq!(text, "字幕");
354+
}
355+
356+
#[test]
357+
fn decode_utf16le_with_bom() {
358+
let mut bytes = vec![0xFF, 0xFE];
359+
for unit in "字幕".encode_utf16() {
360+
bytes.extend_from_slice(&unit.to_le_bytes());
361+
}
362+
let (text, _) = decode_text(&bytes);
363+
assert_eq!(text, "字幕");
364+
}
365+
366+
#[test]
367+
fn decode_big5() {
368+
let (bytes, _, had_errors) = encoding_rs::BIG5.encode("台灣繁體字幕測試內容夠長以利偵測");
369+
assert!(!had_errors);
370+
let (text, _) = decode_text(&bytes);
371+
assert_eq!(text, "台灣繁體字幕測試內容夠長以利偵測");
372+
}
373+
374+
#[test]
375+
fn decode_gbk() {
376+
let (bytes, _, had_errors) = encoding_rs::GBK.encode("简体中文字幕测试内容够长以利侦测");
377+
assert!(!had_errors);
378+
let (text, _) = decode_text(&bytes);
379+
assert_eq!(text, "简体中文字幕测试内容够长以利侦测");
380+
}
381+
382+
#[test]
383+
fn decode_empty_is_empty() {
384+
let (text, _) = decode_text(&[]);
385+
assert_eq!(text, "");
386+
}
387+
321388
// --- build_output_name ---
322389

323390
#[test]

0 commit comments

Comments
 (0)