Fix --jpeg-quality/--jpg-quality being dropped by the CLI (closes #1723)
namespace_to_options() only copied argparse namespace keys that were literal members of OcrOptions.model_fields. The CLI dest for both --jpeg-quality and --jpg-quality was jpeg_quality, but the pydantic field was named jpg_quality (jpeg_quality existed only as a compatibility property, absent from model_fields). The value was silently dropped into extra_attrs, and the optimizer always fell back to its own hardcoded default regardless of the flag. The same alias mismatch also affected the Python API: create_options() uses the same model_fields-matching logic as namespace_to_options(), so ocrmypdf.ocr(jpeg_quality=...) was silently dropped too - only the canonical jpg_quality= kwarg worked. Rather than patch around the mismatch, consolidate on a single canonical name: OcrOptions.jpeg_quality (matching the primary --jpeg-quality CLI flag and the already-consistent naming in OptimizeOptions). jpg_quality becomes a deprecated compatibility property, and ocrmypdf.ocr(jpg_quality=) is a deprecated alias that warns and forwards to jpeg_quality via a new create_options() remap step. --jpg-quality remains a working (already hidden) CLI alias with no code-path divergence, since it now shares an argparse dest that matches the field name directly.
This commit is contained in:
@@ -207,19 +207,19 @@ class OcrOptions(BaseModel):
|
||||
|
||||
# Optimization
|
||||
optimize: int = 1
|
||||
jpg_quality: int | None = None
|
||||
jpeg_quality: int | None = None
|
||||
png_quality: int | None = None
|
||||
|
||||
# Compatibility alias for plugins that expect jpeg_quality
|
||||
# Deprecated compatibility alias for code that still uses the old field name
|
||||
@property
|
||||
def jpeg_quality(self):
|
||||
"""Compatibility alias for jpg_quality."""
|
||||
return self.jpg_quality
|
||||
def jpg_quality(self):
|
||||
"""Deprecated compatibility alias for jpeg_quality."""
|
||||
return self.jpeg_quality
|
||||
|
||||
@jpeg_quality.setter
|
||||
def jpeg_quality(self, value):
|
||||
"""Compatibility alias for jpg_quality."""
|
||||
self.jpg_quality = value
|
||||
@jpg_quality.setter
|
||||
def jpg_quality(self, value):
|
||||
"""Deprecated compatibility alias for jpeg_quality."""
|
||||
self.jpeg_quality = value
|
||||
|
||||
# Output behavior
|
||||
no_overwrite: bool = False
|
||||
@@ -642,12 +642,6 @@ class OcrOptions(BaseModel):
|
||||
value = self.optimize
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
elif namespace == 'optimize' and field_name == 'jpeg_quality':
|
||||
# jpg_quality maps to jpeg_quality
|
||||
if 'jpg_quality' in OcrOptions.model_fields:
|
||||
value = self.jpg_quality
|
||||
if value is not None:
|
||||
kwargs[field_name] = _convert_value(value)
|
||||
|
||||
# Create and cache the plugin options instance
|
||||
instance = model_class(**kwargs)
|
||||
|
||||
+25
-3
@@ -345,6 +345,23 @@ def _remap_language_to_languages(options_kwargs: dict) -> None:
|
||||
del options_kwargs['language']
|
||||
|
||||
|
||||
def _remap_jpg_quality_to_jpeg_quality(options_kwargs: dict) -> None:
|
||||
"""Map the deprecated 'jpg_quality' parameter to 'jpeg_quality'.
|
||||
|
||||
'jpg_quality' was the original API parameter name. 'jpeg_quality' is the
|
||||
canonical OcrOptions field, matching the primary --jpeg-quality CLI flag.
|
||||
Prefer an explicitly-given 'jpeg_quality' if both are set.
|
||||
"""
|
||||
if 'jpg_quality' not in options_kwargs:
|
||||
return
|
||||
old_value = options_kwargs.pop('jpg_quality')
|
||||
if old_value is None:
|
||||
return
|
||||
warn("ocrmypdf.ocr(jpg_quality=...) is deprecated, use jpeg_quality= instead.")
|
||||
if options_kwargs.get('jpeg_quality') is None:
|
||||
options_kwargs['jpeg_quality'] = old_value
|
||||
|
||||
|
||||
def create_options(
|
||||
*, input_file: PathOrIO, output_file: PathOrIO, parser: ArgumentParser, **kwargs
|
||||
) -> OcrOptions:
|
||||
@@ -369,6 +386,9 @@ def create_options(
|
||||
# Map API parameter 'language' to OcrOptions field 'languages'
|
||||
_remap_language_to_languages(options_kwargs)
|
||||
|
||||
# Map deprecated 'jpg_quality' parameter to 'jpeg_quality'
|
||||
_remap_jpg_quality_to_jpeg_quality(options_kwargs)
|
||||
|
||||
# Set input and output files
|
||||
options_kwargs['input_file'] = input_file
|
||||
options_kwargs['output_file'] = output_file
|
||||
@@ -448,7 +468,8 @@ def ocr(
|
||||
redo_ocr: bool | None = None,
|
||||
skip_big: float | None = None,
|
||||
optimize: int | None = None,
|
||||
jpg_quality: int | None = None,
|
||||
jpeg_quality: int | None = None,
|
||||
jpg_quality: int | None = None, # Deprecated, use jpeg_quality instead
|
||||
png_quality: int | None = None,
|
||||
jbig2_lossy: bool | None = None,
|
||||
jbig2_page_group_size: int | None = None,
|
||||
@@ -511,7 +532,8 @@ def ocr( # noqa: D417
|
||||
redo_ocr: bool | None = None, # Legacy, use mode='redo' instead
|
||||
skip_big: float | None = None,
|
||||
optimize: int | None = None,
|
||||
jpg_quality: int | None = None,
|
||||
jpeg_quality: int | None = None,
|
||||
jpg_quality: int | None = None, # Deprecated, use jpeg_quality instead
|
||||
png_quality: int | None = None,
|
||||
jbig2_lossy: bool | None = None, # Deprecated, ignored
|
||||
jbig2_page_group_size: int | None = None, # Deprecated, ignored
|
||||
@@ -883,7 +905,7 @@ def _hocr_to_ocr_pdf( # noqa: D417
|
||||
jobs: int | None = None,
|
||||
use_threads: bool | None = None,
|
||||
optimize: int | None = None,
|
||||
jpg_quality: int | None = None,
|
||||
jpeg_quality: int | None = None,
|
||||
png_quality: int | None = None,
|
||||
jbig2_lossy: bool | None = None, # Deprecated, ignored
|
||||
jbig2_page_group_size: int | None = None, # Deprecated, ignored
|
||||
|
||||
@@ -449,12 +449,12 @@ def convert_to_jbig2(
|
||||
|
||||
|
||||
def _optimize_jpeg(
|
||||
xref: Xref, in_jpg: Path, opt_jpg: Path, jpg_quality: int
|
||||
xref: Xref, in_jpg: Path, opt_jpg: Path, jpeg_quality: int
|
||||
) -> tuple[Xref, Path | None]:
|
||||
with Image.open(in_jpg) as im:
|
||||
save_kwargs: dict[str, Any] = {'optimize': True}
|
||||
if isinstance(jpg_quality, int) and 0 < jpg_quality <= 100:
|
||||
save_kwargs['quality'] = jpg_quality
|
||||
if isinstance(jpeg_quality, int) and 0 < jpeg_quality <= 100:
|
||||
save_kwargs['quality'] = jpeg_quality
|
||||
im.save(opt_jpg, **save_kwargs)
|
||||
|
||||
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
|
||||
@@ -473,7 +473,7 @@ def transcode_jpegs(
|
||||
for xref in jpegs:
|
||||
in_jpg = jpg_name(root, xref)
|
||||
opt_jpg = in_jpg.with_suffix('.opt.jpg')
|
||||
yield xref, in_jpg, opt_jpg, options.jpg_quality
|
||||
yield xref, in_jpg, opt_jpg, options.jpeg_quality
|
||||
|
||||
def finish_jpeg(result: tuple[Xref, Path | None], pbar: ProgressBar):
|
||||
xref, opt_jpg = result
|
||||
@@ -703,8 +703,8 @@ def optimize(
|
||||
safe_symlink(input_file, output_file)
|
||||
return output_file
|
||||
|
||||
if not options.jpg_quality:
|
||||
options.jpg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
|
||||
if not options.jpeg_quality:
|
||||
options.jpeg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
|
||||
if not options.png_quality:
|
||||
options.png_quality = DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
|
||||
|
||||
@@ -766,7 +766,7 @@ def main(infile, outfile, level, jobs=1):
|
||||
output_file=outfile, # Required field
|
||||
jobs=jobs,
|
||||
optimize=int(level),
|
||||
jpg_quality=0, # Use default
|
||||
jpeg_quality=0, # Use default
|
||||
png_quality=0,
|
||||
jbig2_threshold=0.85,
|
||||
quiet=True,
|
||||
|
||||
Reference in New Issue
Block a user