fix(dvb): Multiple fixes for DVB subtitles - timing, OCR quality, memory access bugs (#224) (#1826)

* fix(dvb): Multiple fixes for DVB subtitle extraction from Chinese broadcasts (#224)

This commit addresses multiple issues with DVB subtitle extraction reported in #224:

1. **PMT parsing crash fix** (ts_tables.c):
   - Added minimum length check (16 bytes) to prevent out-of-bounds access
   - Added bounds check before memcpy to prevent buffer overflow when section > 1021 bytes

2. **Negative subtitle timing fix** (general_loop.c):
   - For DVB subtitle streams, properly initialize min_pts from audio/subtitle PTS
   - This fixes the issue where all timestamps were negative (~95000 seconds off)

3. **OCR improvements** (ocr.c):
   - Fixed ignore_alpha_at_edge() which could create invalid crop windows
   - Added image inversion for DVB subtitles (light text on dark background)
     to improve Tesseract OCR accuracy
   - Added contrast normalization to further improve character recognition
   - Fixed nofontcolor check to respect --no-fontcolor parameter
   - Added iteration safety limit in color detection loop

4. **--ocrlang parameter fix** (Rust files):
   - Changed ocrlang from Language enum to String to accept Tesseract language
     names directly (e.g., "chi_tra", "chi_sim", "eng")
   - Added case-insensitive matching for --dvblang parameter
   - Added better error messages for invalid language codes

Tested with 12GB Chinese DVB broadcast file:
- Timing: All timestamps now positive (0.235s, 2.594s, etc.)
- OCR: ~80-90% accuracy with chi_tra traineddata (improved from ~70%)
- No crashes during full file processing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ocr): Fix crashes in DVB subtitle color detection

Two issues fixed in the OCR color detection code:

1. Tesseract crash during iteration:
   - The color detection pass used raw color images without preprocessing
   - Tesseract expects dark text on light background, but DVB subtitles
     have light text on dark background
   - Added grayscale conversion, inversion, and contrast enhancement
     (same preprocessing as the main OCR pass)

2. Heap corruption in histogram calculation:
   - The histogram loop had no bounds checking on array accesses
   - Tesseract could return invalid bounding boxes causing buffer overflows
   - Added validation of bounding box coordinates before processing
   - Added safe index checking for copy->data and histogram arrays

Also added skip_color_detection label for clean error handling and
proper cleanup of the preprocessed image.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dvb): Fix zero-duration subtitles and overlaps during PTS jumps

Add start_pts field to cc_subtitle struct to track raw PTS values
independent of FTS timeline resets. Modify end_time calculation in
dvbsub_handle_display_segment() to cap duration at 4 seconds when
PTS jumps cause timeline discontinuities, preventing zero-duration
and overlapping subtitles.

Also update .gitignore to exclude plans/ directory and temp files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Carlos Fernandez Sanz
2025-12-14 20:03:55 -08:00
committed by GitHub
parent 8d95ad0e7b
commit 42885caedd
11 changed files with 257 additions and 39 deletions

6
.gitignore vendored
View File

@@ -155,3 +155,9 @@ windows/*/debug/*
windows/*/CACHEDIR.TAG
windows/.rustc_info.json
linux/configure~
# Plans and temporary files
plans/
tess.log
**/tess.log
ut=srt*

View File

@@ -1,5 +1,12 @@
0.95 (2025-09-15)
-----------------
- Fix: DVB subtitle extraction improvements for Chinese broadcasts (#224):
- Fix crash in parse_PMT() due to missing bounds checks
- Fix negative timestamps in DVB subtitle output
- Fix crash in ignore_alpha_at_edge() OCR cropping
- Improve DVB subtitle OCR accuracy with image inversion
- Fix --ocrlang to accept Tesseract language names (chi_tra, chi_sim, etc.)
- Add case-insensitive matching for --dvblang parameter
- FIX: Add HEVC/H.265 stream type recognition to prevent crashes on ATSC 3.0 streams
- Fix: ARM64/aarch64 build failure due to c_char type mismatch in nal.rs
- Fix: HardSubX OCR on Rust

View File

@@ -81,6 +81,9 @@ struct cc_subtitle
/** Used for DVB end time in ms */
int time_out;
/** Raw PTS value when this subtitle started (for DVB timing) */
LLONG start_pts;
struct cc_subtitle *next;
struct cc_subtitle *prev;
};

View File

@@ -1713,16 +1713,55 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub)
void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
struct lib_cc_decode *dec_ctx,
struct cc_subtitle *sub)
struct cc_subtitle *sub,
LLONG pre_fts_max)
{
DVBSubContext *ctx = (DVBSubContext *)dec_ctx->private_data;
LLONG current_pts = dec_ctx->timing->current_pts;
if (!enc_ctx)
return;
if (enc_ctx->write_previous) // this condition is used for the first subtitle - write_previous will be 0 first so we don't encode a non-existing previous sub
{
enc_ctx->prev->last_string = NULL; // Reset last recognized sub text
sub->prev->end_time = (dec_ctx->timing->current_pts - dec_ctx->timing->min_pts) / (MPEG_CLOCK_FREQ / 1000); // we set the end time of the previous sub the current pts
if (sub->prev->time_out < sub->prev->end_time - sub->prev->start_time)
enc_ctx->prev->last_string = NULL; // Reset last recognized sub text
// Get the current FTS, which will be the start_time of the new subtitle
LLONG next_start_time = get_fts(dec_ctx->timing, dec_ctx->current_field);
// For DVB subtitles, a subtitle is displayed until the next one appears.
// Use next_start_time as the end_time to ensure subtitle N ends when N+1 starts.
// This prevents any overlap between consecutive subtitles.
if (next_start_time > sub->prev->start_time)
{
sub->prev->end_time = next_start_time;
}
else
{
// PTS jump or timeline reset - next_start is at or before our start.
// Calculate duration from raw PTS, but cap to reasonable maximum (5 seconds)
// to avoid creating subtitles that overlap excessively with subsequent ones.
LLONG duration_ms = 0;
if (sub->prev->start_pts > 0 && current_pts > sub->prev->start_pts)
{
duration_ms = (current_pts - sub->prev->start_pts) / (MPEG_CLOCK_FREQ / 1000);
}
// Cap duration to 4 seconds or timeout if smaller
LLONG max_duration = 4000; // 4 seconds
if (sub->prev->time_out > 0 && sub->prev->time_out < max_duration)
{
max_duration = sub->prev->time_out;
}
if (duration_ms > max_duration)
{
duration_ms = max_duration;
}
sub->prev->end_time = sub->prev->start_time + duration_ms;
}
// Sanity check: if end_time still <= start_time, use minimal duration
if (sub->prev->end_time <= sub->prev->start_time)
{
dbg_print(CCX_DMT_DVB, "DVB timing: end <= start, using start+1\n");
sub->prev->end_time = sub->prev->start_time + 1;
}
// Apply timeout limit if specified
if (sub->prev->time_out > 0 && sub->prev->time_out < sub->prev->end_time - sub->prev->start_time)
{
sub->prev->end_time = sub->prev->start_time + sub->prev->time_out;
}
@@ -1774,7 +1813,10 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
sub->time_out = ctx->time_out;
sub->prev = NULL;
sub->prev = copy_subtitle(sub);
sub->prev->start_time = (dec_ctx->timing->current_pts - dec_ctx->timing->min_pts) / (MPEG_CLOCK_FREQ / 1000); // we set the start time of the previous sub the current pts
// Use get_fts() which properly handles PTS jumps and maintains monotonic timing
sub->prev->start_time = get_fts(dec_ctx->timing, dec_ctx->current_field);
// Store the raw PTS for accurate duration calculation (not affected by PTS jump handling)
sub->prev->start_pts = current_pts;
write_dvb_sub(dec_ctx->prev, sub->prev); // we write the current dvb sub to update decoder context
enc_ctx->write_previous = 1; // we update our boolean value so next time the program reaches this block of code, it encodes the previous sub
@@ -1823,6 +1865,10 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co
p = buf;
p_end = buf + buf_size;
// Capture the max FTS before set_fts() processes any PTS jumps.
// This will be used as the end time for the previous subtitle.
LLONG pre_fts_max = get_fts_max(dec_ctx->timing);
dec_ctx->timing->current_tref = 0;
set_fts(dec_ctx->timing);
@@ -1882,7 +1928,7 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co
break;
case DVBSUB_DISPLAY_SEGMENT: // when we get a display segment, we save the current page
dbg_print(CCX_DMT_DVB, "(DVBSUB_DISPLAY_SEGMENT), SEGMENT LENGTH: %d", segment_length);
dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub);
dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub, pre_fts_max);
got_segment |= 16;
break;
default:
@@ -1898,7 +1944,7 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co
// segments then we need no further data.
if (got_segment == 15)
{
dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub);
dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub, pre_fts_max);
got_segment |= 16;
}
end:

View File

@@ -936,6 +936,15 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx,
{
*min_pts = ctx->demux_ctx->pinfo[p_index].got_important_streams_min_pts[AUDIO];
set_current_pts((*dec_ctx)->timing, *min_pts);
// For DVB subtitles, we need to directly set min_pts because set_fts()
// relies on video frame type detection which doesn't work for DVB-only streams.
// This fixes negative subtitle timestamps.
if ((*dec_ctx)->timing->min_pts == 0x01FFFFFFFFLL)
{
(*dec_ctx)->timing->min_pts = *min_pts;
(*dec_ctx)->timing->pts_set = 2; // MinPtsSet
(*dec_ctx)->timing->sync_pts = *min_pts;
}
set_fts((*dec_ctx)->timing);
}
}
@@ -957,6 +966,13 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx,
else
pts = (*data_node)->pts;
set_current_pts((*dec_ctx)->timing, pts);
// For DVB subtitles, use the first subtitle PTS as min_pts if audio hasn't been seen yet
if ((*dec_ctx)->codec == CCX_CODEC_DVB && (*dec_ctx)->timing->min_pts == 0x01FFFFFFFFLL)
{
(*dec_ctx)->timing->min_pts = pts;
(*dec_ctx)->timing->pts_set = 2; // MinPtsSet
(*dec_ctx)->timing->sync_pts = pts;
}
set_fts((*dec_ctx)->timing);
}
@@ -988,7 +1004,8 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx,
{
if ((*data_node)->bufferdatatype == CCX_DVB_SUBTITLE && (*dec_ctx)->dec_sub.prev->end_time == 0)
{
(*dec_ctx)->dec_sub.prev->end_time = ((*dec_ctx)->timing->current_pts - (*dec_ctx)->timing->min_pts) / (MPEG_CLOCK_FREQ / 1000);
// Use get_fts() which properly handles PTS jumps and maintains monotonic timing
(*dec_ctx)->dec_sub.prev->end_time = get_fts((*dec_ctx)->timing, (*dec_ctx)->current_field);
if ((*enc_ctx) != NULL)
encode_sub((*enc_ctx)->prev, (*dec_ctx)->dec_sub.prev);
(*dec_ctx)->dec_sub.prev->got_output = 0;
@@ -1133,6 +1150,13 @@ int general_loop(struct lib_ccx_ctx *ctx)
{
min_pts = ctx->demux_ctx->pinfo[p_index].got_important_streams_min_pts[AUDIO];
set_current_pts(dec_ctx->timing, min_pts);
// For DVB subtitles, directly set min_pts to fix negative timestamps
if (dec_ctx->timing->min_pts == 0x01FFFFFFFFLL)
{
dec_ctx->timing->min_pts = min_pts;
dec_ctx->timing->pts_set = 2; // MinPtsSet
dec_ctx->timing->sync_pts = min_pts;
}
set_fts(dec_ctx->timing);
}
}
@@ -1145,7 +1169,16 @@ int general_loop(struct lib_ccx_ctx *ctx)
continue;
if (data_node->pts != CCX_NOPTS)
{
set_current_pts(dec_ctx->timing, data_node->pts);
// For DVB subtitles, use the first subtitle PTS as min_pts if audio hasn't been seen yet
if (dec_ctx->codec == CCX_CODEC_DVB && dec_ctx->timing->min_pts == 0x01FFFFFFFFLL)
{
dec_ctx->timing->min_pts = data_node->pts;
dec_ctx->timing->pts_set = 2; // MinPtsSet
dec_ctx->timing->sync_pts = data_node->pts;
}
}
ret = process_data(enc_ctx, dec_ctx, data_node);
if (enc_ctx != NULL)

View File

@@ -233,31 +233,49 @@ fail:
*/
BOX *ignore_alpha_at_edge(png_byte *alpha, unsigned char *indata, int w, int h, PIX *in, PIX **out)
{
int i, j, index, start_y = 0, end_y = 0;
int find_end_x = CCX_FALSE;
int i, j, index, start_x = -1, end_x = -1;
BOX *cropWindow;
for (j = 1; j < w - 1; j++)
// Find the leftmost and rightmost columns with visible (non-transparent) pixels
for (j = 0; j < w; j++)
{
for (i = 0; i < h; i++)
{
index = indata[i * w + (j)];
index = indata[i * w + j];
if (alpha[index] != 0)
{
if (find_end_x == CCX_FALSE)
{
start_y = j;
find_end_x = CCX_TRUE;
}
else
{
end_y = j;
}
if (start_x < 0)
start_x = j;
end_x = j;
break; // Found visible pixel in this column, move to next
}
}
}
cropWindow = boxCreate(start_y, 0, (w - (start_y + (w - end_y))), h - 1);
// Handle edge cases: no visible pixels or invalid dimensions
if (start_x < 0 || end_x < start_x || w <= 0 || h <= 0)
{
// Return the entire image as fallback
cropWindow = boxCreate(0, 0, w, h);
*out = pixClone(in);
return cropWindow;
}
int crop_width = end_x - start_x + 1;
if (crop_width <= 0)
crop_width = w;
cropWindow = boxCreate(start_x, 0, crop_width, h);
*out = pixClipRectangle(in, cropWindow, NULL);
// If clipping failed, return the original image
if (*out == NULL)
{
boxDestroy(&cropWindow);
cropWindow = boxCreate(0, 0, w, h);
*out = pixClone(in);
}
return cropWindow;
}
@@ -361,6 +379,24 @@ char *ocr_bitmap(void *arg, png_color *palette, png_byte *alpha, unsigned char *
// Converting image to grayscale for OCR to avoid issues with transparency
cpix_gs = pixConvertRGBToGray(cpix, 0.0, 0.0, 0.0);
// Invert the grayscale image for better OCR accuracy
// DVB subtitles typically have light text on dark background, but
// Tesseract expects dark text on light background
if (cpix_gs != NULL)
pixInvert(cpix_gs, cpix_gs);
// Apply contrast enhancement to improve OCR accuracy
// This stretches the histogram to use the full range, improving character recognition
if (cpix_gs != NULL)
{
PIX *enhanced = pixContrastNorm(NULL, cpix_gs, 100, 100, 55, 1, 1);
if (enhanced != NULL)
{
pixDestroy(&cpix_gs);
cpix_gs = enhanced;
}
}
if (cpix_gs == NULL)
tess_ret = -1;
else
@@ -396,15 +432,35 @@ char *ocr_bitmap(void *arg, png_color *palette, png_byte *alpha, unsigned char *
}
// Begin color detection
// Using tlt_config.nofontcolor (true when "--nofontcolor" parameter used) to skip color detection if not required
// Using tlt_config.nofontcolor or ccx_options.nofontcolor (true when "--no-fontcolor" parameter used) to skip color detection if not required
// This is also skipped if --no-spupngocr is set since the OCR output won't be used anyway
int text_out_len;
if ((text_out_len = strlen(text_out)) > 0 && !tlt_config.nofontcolor)
if ((text_out_len = strlen(text_out)) > 0 && !tlt_config.nofontcolor && !ccx_options.nofontcolor)
{
float h0 = -100;
int written_tag = 0;
TessResultIterator *ri = 0;
TessPageIteratorLevel level = RIL_WORD;
TessBaseAPISetImage2(ctx->api, color_pix_out);
PIX *color_pix_processed = NULL; // Will hold preprocessed image for cleanup
// Preprocess color_pix_out for Tesseract the same way as cpix_gs
// Tesseract expects dark text on light background, but DVB subtitles typically
// have light text on dark background. Without preprocessing, Tesseract
// produces garbage results or crashes when iterating over words.
color_pix_processed = pixConvertRGBToGray(color_pix_out, 0.0, 0.0, 0.0);
if (color_pix_processed == NULL)
{
goto skip_color_detection;
}
pixInvert(color_pix_processed, color_pix_processed);
PIX *color_pix_enhanced = pixContrastNorm(NULL, color_pix_processed, 100, 100, 55, 1, 1);
if (color_pix_enhanced != NULL)
{
pixDestroy(&color_pix_processed);
color_pix_processed = color_pix_enhanced;
}
TessBaseAPISetImage2(ctx->api, color_pix_processed);
tess_ret = TessBaseAPIRecognize(ctx->api, NULL);
if (tess_ret != 0)
{
@@ -417,13 +473,26 @@ char *ocr_bitmap(void *arg, png_color *palette, png_byte *alpha, unsigned char *
if (!tess_ret && ri != 0)
{
int iteration_count = 0;
const int max_iterations = 10000; // Safety limit to prevent infinite loops
do
{
// Safety check: limit iterations to prevent crashes on malformed data
if (++iteration_count > max_iterations)
{
mprint("Warning: OCR color detection exceeded maximum iterations, skipping.\n");
break;
}
char *word = TessResultIteratorGetUTF8Text(ri, level);
// float conf = TessResultIteratorConfidence(ri,level);
int x1, y1, x2, y2;
if (!TessPageIteratorBoundingBox((TessPageIterator *)ri, level, &x1, &y1, &x2, &y2))
{
if (word)
TessDeleteText(word);
continue;
}
// printf("word: '%s'; \tconf: %.2f; BoundingBox: %d,%d,%d,%d;",word, conf, x1, y1, x2, y2);
// printf("word: '%s';", word);
// {
@@ -466,12 +535,42 @@ char *ocr_bitmap(void *arg, png_color *palette, png_byte *alpha, unsigned char *
/* calculate histogram of image */
int firstpixel = copy->data[0]; // TODO: Verify this border pixel assumption holds
// Bounds check: validate bounding box coordinates
// The bounding box (x1,y1,x2,y2) is relative to the cropped image.
// With crop offset (x,y), the original coordinates are (x+x1, y+y1) to (x+x2, y+y2).
// Ensure we don't access outside the original image bounds.
int orig_x1 = x + x1;
int orig_y1 = y + y1;
int orig_x2 = x + x2;
int orig_y2 = y + y2;
if (orig_x1 < 0 || orig_y1 < 0 || orig_x2 >= w || orig_y2 >= h ||
orig_x1 > orig_x2 || orig_y1 > orig_y2)
{
// Invalid bounding box - skip this word
freep(&histogram);
freep(&mcit);
freep(&iot);
if (word)
TessDeleteText(word);
continue;
}
for (int i = y1; i <= y2; i++)
{
for (int j = x1; j <= x2; j++)
{
if (copy->data[(y + i) * w + (x + j)] != firstpixel)
histogram[copy->data[(y + i) * w + (x + j)]]++;
int idx = (y + i) * w + (x + j);
if (idx >= 0 && idx < w * h)
{
int color_idx = copy->data[idx];
if (color_idx >= 0 && color_idx < copy->nb_colors)
{
if (color_idx != firstpixel)
histogram[color_idx]++;
}
}
}
}
/* sorted in increasing order of intensity */
@@ -751,7 +850,11 @@ char *ocr_bitmap(void *arg, png_color *palette, png_byte *alpha, unsigned char *
text_out = new_text_out;
}
}
TessResultIteratorDelete(ri);
skip_color_detection:
if (ri)
TessResultIteratorDelete(ri);
if (color_pix_processed)
pixDestroy(&color_pix_processed);
}
// End Color Detection
boxDestroy(&crop_points);

View File

@@ -116,6 +116,15 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr
uint16_t pi_length;
// Minimum PMT size: table_id(1) + section_length(2) + program_number(2) +
// version/current(1) + section_number(1) + last_section_number(1) +
// PCR_PID(2) + program_info_length(2) + CRC(4) = 16 bytes
if (len < 16)
{
dbg_print(CCX_DMT_PMT, "PMT packet too short (%d bytes), ignoring\n", len);
return 0;
}
crc = (*(int32_t *)(sbuf + olen - 4));
table_id = buf[0];
@@ -170,6 +179,12 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr
if (!current_next_indicator && pinfo->version != 0xFF) // 0xFF means we don't have one yet
return 0;
// Bounds check: saved_section is 1021 bytes
if (len > 1021)
{
dbg_print(CCX_DMT_PMT, "PMT section too large (%d bytes), truncating to 1021\n", len);
len = 1021;
}
memcpy(pinfo->saved_section, buf, len);
if (pinfo->analysed_PMT_once == CCX_TRUE && pinfo->version == version_number)

View File

@@ -338,7 +338,7 @@ pub enum CcxTxt {
}
#[derive(Debug, Default, EnumString, Clone, Copy, PartialEq, Eq)]
#[strum(serialize_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum Language {
#[default]
Und, // Undefined

View File

@@ -450,7 +450,8 @@ pub struct Options {
/// The name of the language stream for DVB
pub dvblang: Option<Language>,
/// The name of the .traineddata file to be loaded with tesseract
pub ocrlang: Option<Language>,
/// (accepts Tesseract language names directly, e.g., "chi_tra", "eng")
pub ocrlang: Option<String>,
/// The Tesseract OEM mode, could be 0 (default), 1 or 2
pub ocr_oem: i8,
/// The Tesseract PSM mode, could be between 0 and 13. 3 is tesseract default

View File

@@ -158,8 +158,8 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
if let Some(dvblang) = options.dvblang {
(*ccx_s_options).dvblang = string_to_c_char(dvblang.to_ctype().as_str());
}
if let Some(ocrlang) = options.ocrlang {
(*ccx_s_options).ocrlang = string_to_c_char(ocrlang.to_ctype().as_str());
if let Some(ref ocrlang) = options.ocrlang {
(*ccx_s_options).ocrlang = string_to_c_char(ocrlang.as_str());
}
(*ccx_s_options).ocr_oem = options.ocr_oem as _;
(*ccx_s_options).psm = options.psm as _;
@@ -360,12 +360,9 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options {
.expect("Invalid language"),
);
}
// Handle ocrlang (C string to PathBuf)
// Handle ocrlang (C string to String - accepts Tesseract language names directly)
if !(*ccx_s_options).ocrlang.is_null() {
options.ocrlang = Some(
Language::from_str(&c_char_to_string((*ccx_s_options).ocrlang))
.expect("Invalid language"),
);
options.ocrlang = Some(c_char_to_string((*ccx_s_options).ocrlang));
}
options.ocr_oem = (*ccx_s_options).ocr_oem as i8;

View File

@@ -742,11 +742,18 @@ impl OptionsExt for Options {
}
if let Some(ref lang) = args.dvblang {
self.dvblang = Some(Language::from_str(lang.as_str()).unwrap());
self.dvblang = Some(Language::from_str(lang.as_str()).unwrap_or_else(|_| {
fatal!(
cause = ExitCause::MalformedParameter;
"Invalid dvblang value '{}'. Use a 3-letter ISO 639-2 language code (e.g., 'chi', 'eng', 'chs').",
lang
);
}));
}
if let Some(ref ocrlang) = args.ocrlang {
self.ocrlang = Some(Language::from_str(ocrlang.as_str()).unwrap());
// Accept Tesseract language names directly (e.g., "chi_tra", "chi_sim", "eng")
self.ocrlang = Some(ocrlang.clone());
}
if let Some(ref quant) = args.quant {