Add --ghostscript-jpeg-quality and --ghostscript-jpeg-maxdpi
Expose Ghostscript's -dJPEGQ and image downsampling switches as advanced, plugin-scoped options for tuning PDF/A output, without polluting the central OcrOptions registry. The optimizer's existing --jpeg-quality remains the recommended JPEG quality control. - GhostscriptOptions gains jpeg_quality and jpeg_maxdpi fields and CLI args (advanced help text). jpeg_quality=0 is honored as Ghostscript's maximum compression rather than being silently coerced to the default. - _exec.ghostscript.generate_pdfa() forwards both values; when jpeg_maxdpi is set, downsample threshold is pinned at 1.0. - _get_plugin_options falls back to extra_attrs for namespaced fields so plugins can own their options without registering them centrally. - Documentation explains the rationale: Ghostscript is the legacy path (pypdfium + verapdf is preferred in v17+), the optimizer is the supported file-size lever, and lowering quality is almost always a better trade than downsampling.
This commit is contained in:
@@ -419,6 +419,70 @@ curves. In this case, you may want to use a different color conversion
|
|||||||
strategy. The `--color-conversion-strategy` option allows you to select a
|
strategy. The `--color-conversion-strategy` option allows you to select a
|
||||||
different strategy, such as `RGB`.
|
different strategy, such as `RGB`.
|
||||||
|
|
||||||
|
## Advanced Ghostscript tuning
|
||||||
|
|
||||||
|
:::{versionadded} 17.5.0
|
||||||
|
:::
|
||||||
|
|
||||||
|
OCRmyPDF intentionally hides most Ghostscript controls because Ghostscript
|
||||||
|
is a legacy code path. The preferred PDF/A pipeline in v17+ uses pypdfium2
|
||||||
|
as the rasterizer and verapdf to validate speculative PDF/A output, with
|
||||||
|
Ghostscript reserved as a fallback for PDFs that cannot be made compliant
|
||||||
|
without it. OCRmyPDF's separate optimizer (controlled by `--optimize`,
|
||||||
|
`--jpeg-quality`, `--png-quality`, etc.) is the supported way to shrink
|
||||||
|
output PDFs: it gives consistent results across input files, and isolates
|
||||||
|
Ghostscript so it can focus on producing a PDF/A with as few image
|
||||||
|
transformations as possible.
|
||||||
|
|
||||||
|
The two options below are exposed for advanced users who want to tune
|
||||||
|
Ghostscript's intermediate PDF/A output directly. Most users will get
|
||||||
|
more predictable results from the optimizer.
|
||||||
|
|
||||||
|
### `--ghostscript-jpeg-quality Q`
|
||||||
|
|
||||||
|
Sets Ghostscript's `-dJPEGQ` switch for images that Ghostscript chooses
|
||||||
|
to recompress to JPEG while building a PDF/A. `Q=0` requests maximum
|
||||||
|
compression and `Q=100` requests best quality; if the flag is omitted,
|
||||||
|
OCRmyPDF passes `95` (the historical default). This only affects images
|
||||||
|
Ghostscript transcodes — existing JPEGs pass through unchanged on modern
|
||||||
|
Ghostscript releases. For end-to-end JPEG quality tuning, prefer
|
||||||
|
`--jpeg-quality`, which is implemented by the OCRmyPDF optimizer and is
|
||||||
|
applied independently of whatever Ghostscript decides to do.
|
||||||
|
|
||||||
|
Note: setting both `--ghostscript-jpeg-quality` and `--jpeg-quality` can
|
||||||
|
result in double JPEG recompression, since the optimizer may re-encode
|
||||||
|
images that Ghostscript already recompressed. This can degrade quality
|
||||||
|
in subtle ways.
|
||||||
|
|
||||||
|
### `--ghostscript-jpeg-maxdpi DPI`
|
||||||
|
|
||||||
|
Enables Ghostscript's image downsampling and caps color, grayscale, and
|
||||||
|
monochrome image resolution to `DPI`. The downsample threshold is set to
|
||||||
|
`1.0`, so any image whose effective DPI exceeds the cap will be
|
||||||
|
downsampled.
|
||||||
|
|
||||||
|
Reducing JPEG quality is almost always a better trade than downsampling
|
||||||
|
at the same compression budget: a 400 DPI JPEG at modest quality usually
|
||||||
|
looks much better than a 200 DPI JPEG, because the JPEG codec can spend
|
||||||
|
bits where they count. Downsampling is also dangerous for PDFs that
|
||||||
|
combine a low-resolution color image with a high-resolution monochrome
|
||||||
|
mask — capping the mask resolution can produce visible quality loss.
|
||||||
|
For these reasons, prefer `--jpeg-quality` over `--ghostscript-jpeg-maxdpi`
|
||||||
|
unless you specifically want to force a hard DPI cap.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ocrmypdf --output-type pdfa \
|
||||||
|
--ghostscript-jpeg-quality 80 \
|
||||||
|
--ghostscript-jpeg-maxdpi 150 \
|
||||||
|
in.pdf out.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
These options only take effect when Ghostscript is invoked for PDF/A
|
||||||
|
conversion (`--output-type pdfa`, `pdfa-1`, `pdfa-2`, or `pdfa-3`, or
|
||||||
|
when `--output-type auto` falls back to Ghostscript).
|
||||||
|
|
||||||
## PDF/A output modes
|
## PDF/A output modes
|
||||||
|
|
||||||
:::{versionchanged} 17.0.0
|
:::{versionchanged} 17.0.0
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ ocrmypdf --output-type pdf input.pdf output.pdf
|
|||||||
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
|
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Reduce JPEG quality with the optimizer
|
||||||
|
|
||||||
|
This is the recommended way to shrink JPEG content in the output. The
|
||||||
|
optimizer applies regardless of `--output-type`, so it works on both
|
||||||
|
plain PDFs and Ghostscript-produced PDF/A files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ocrmypdf --optimize 2 --jpeg-quality 60 input.pdf output.pdf
|
||||||
|
```
|
||||||
|
|
||||||
### Modify a file in place
|
### Modify a file in place
|
||||||
|
|
||||||
The file will only be overwritten if OCRmyPDF is successful.
|
The file will only be overwritten if OCRmyPDF is successful.
|
||||||
|
|||||||
@@ -183,6 +183,11 @@ v17 addresses through alternative codepaths. When Ghostscript is used:
|
|||||||
`jpeg` or `lossless` to set all images to one type or the other.
|
`jpeg` or `lossless` to set all images to one type or the other.
|
||||||
Ghostscript lacks an option to maintain the input image's format.
|
Ghostscript lacks an option to maintain the input image's format.
|
||||||
(Modern Ghostscript can copy JPEG images without transcoding them.)
|
(Modern Ghostscript can copy JPEG images without transcoding them.)
|
||||||
|
Advanced users can also tune Ghostscript's image recompression with
|
||||||
|
`--ghostscript-jpeg-quality` and `--ghostscript-jpeg-maxdpi`; see
|
||||||
|
[Advanced Ghostscript tuning](advanced.md#advanced-ghostscript-tuning).
|
||||||
|
Most users should prefer `--jpeg-quality` (applied by the OCRmyPDF
|
||||||
|
optimizer) over those Ghostscript-scoped controls.
|
||||||
- Ghostscript's PDF/A conversion removes any XMP metadata that is not
|
- Ghostscript's PDF/A conversion removes any XMP metadata that is not
|
||||||
one of the standard XMP metadata namespaces for PDFs. In particular,
|
one of the standard XMP metadata namespaces for PDFs. In particular,
|
||||||
PRISM Metadata is removed.
|
PRISM Metadata is removed.
|
||||||
|
|||||||
+10
-1
@@ -98,7 +98,16 @@ If `pngquant` is installed, OCRmyPDF will use it to perform quantize
|
|||||||
paletted images to reduce their size.
|
paletted images to reduce their size.
|
||||||
|
|
||||||
The quality of JPEGs may be lowered, on the assumption that a lower
|
The quality of JPEGs may be lowered, on the assumption that a lower
|
||||||
quality image may be suitable for storage after OCR.
|
quality image may be suitable for storage after OCR. Use `--jpeg-quality`
|
||||||
|
to control the optimizer's JPEG quality target. The optimizer is the
|
||||||
|
recommended way to reduce JPEG image sizes: it applies consistently
|
||||||
|
regardless of whether Ghostscript was used to produce a PDF/A.
|
||||||
|
|
||||||
|
If you specifically need to tune Ghostscript's own PDF/A image handling
|
||||||
|
(for example, to force a hard DPI cap), see
|
||||||
|
[Advanced Ghostscript tuning](advanced.md#advanced-ghostscript-tuning)
|
||||||
|
for the separate `--ghostscript-jpeg-quality` and
|
||||||
|
`--ghostscript-jpeg-maxdpi` options.
|
||||||
|
|
||||||
It is not possible to optimize all image types. Uncommon image types may
|
It is not possible to optimize all image types. Uncommon image types may
|
||||||
be skipped by the optimizer.
|
be skipped by the optimizer.
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ __ocrmypdf_arguments()
|
|||||||
--rasterizer (PDF page rasterizer)
|
--rasterizer (PDF page rasterizer)
|
||||||
--rotate-pages-threshold (page rotation confidence)
|
--rotate-pages-threshold (page rotation confidence)
|
||||||
--pdfa-image-compression (set PDF/A image compression options)
|
--pdfa-image-compression (set PDF/A image compression options)
|
||||||
|
--ghostscript-jpeg-quality (Ghostscript JPEG quality during PDF/A [0..100])
|
||||||
|
--ghostscript-jpeg-maxdpi (cap Ghostscript image DPI during PDF/A)
|
||||||
--fast-web-view (if file size if above this amount in MB linearize PDF)
|
--fast-web-view (if file size if above this amount in MB linearize PDF)
|
||||||
--continue-on-soft-render-error (continue after recoverable render errors)
|
--continue-on-soft-render-error (continue after recoverable render errors)
|
||||||
--plugin (name of plugin to import)
|
--plugin (name of plugin to import)
|
||||||
@@ -337,6 +339,7 @@ __ocrmypdf_check_previous()
|
|||||||
|
|
||||||
--title|--author|--subject|--keywords|--unpaper-args|--pages|--plugin|\
|
--title|--author|--subject|--keywords|--unpaper-args|--pages|--plugin|\
|
||||||
--jpeg-quality|--png-quality|--image-dpi|--oversample|--skip-big|--max-image-mpixels|\
|
--jpeg-quality|--png-quality|--image-dpi|--oversample|--skip-big|--max-image-mpixels|\
|
||||||
|
--ghostscript-jpeg-quality|--ghostscript-jpeg-maxdpi|\
|
||||||
--tesseract-timeout|--tesseract-non-ocr-timeout|--tesseract-downsample-above|\
|
--tesseract-timeout|--tesseract-non-ocr-timeout|--tesseract-downsample-above|\
|
||||||
--rotate-pages-threshold|--fast-web-view)
|
--rotate-pages-threshold|--fast-web-view)
|
||||||
# argument required but no completions available
|
# argument required but no completions available
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ function __fish_ocrmypdf_pdfa_compression
|
|||||||
echo -e "lossless\t"(_ "convert color and grayscale images to lossless (PNG)")
|
echo -e "lossless\t"(_ "convert color and grayscale images to lossless (PNG)")
|
||||||
end
|
end
|
||||||
complete -c ocrmypdf -x -l pdfa-image-compression -a '(__fish_ocrmypdf_pdfa_compression)' -d "set PDF/A image compression options"
|
complete -c ocrmypdf -x -l pdfa-image-compression -a '(__fish_ocrmypdf_pdfa_compression)' -d "set PDF/A image compression options"
|
||||||
|
complete -c ocrmypdf -x -l ghostscript-jpeg-quality -d "Ghostscript JPEG quality during PDF/A [0..100]"
|
||||||
|
complete -c ocrmypdf -x -l ghostscript-jpeg-maxdpi -d "cap Ghostscript image DPI during PDF/A"
|
||||||
|
|
||||||
complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use"
|
complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use"
|
||||||
complete -c ocrmypdf -x -l title -d "set metadata"
|
complete -c ocrmypdf -x -l title -d "set metadata"
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ def generate_pdfa(
|
|||||||
*,
|
*,
|
||||||
compression: str,
|
compression: str,
|
||||||
color_conversion_strategy: str,
|
color_conversion_strategy: str,
|
||||||
|
jpeg_quality: int | None = None,
|
||||||
|
jpeg_maxdpi: int | None = None,
|
||||||
pdf_version: str = '1.5',
|
pdf_version: str = '1.5',
|
||||||
pdfa_part: str = '2',
|
pdfa_part: str = '2',
|
||||||
progressbar_class=None,
|
progressbar_class=None,
|
||||||
@@ -318,15 +320,38 @@ def generate_pdfa(
|
|||||||
# Windows has lots of fatal "permission denied" errors
|
# Windows has lots of fatal "permission denied" errors
|
||||||
stop_on_error = False
|
stop_on_error = False
|
||||||
|
|
||||||
# 1. nb no need to specify ProcessColorModel when ColorConversionStrategy
|
# `-dJPEGQ=N` tells Ghostscript to use a JPEG quality of N, IF it decides
|
||||||
|
# to transcode an image to JPEG. When there are existing JPEG images,
|
||||||
|
# Ghostscript uses passthrough mode, so the quality level is not changed.
|
||||||
|
# OCRmyPDF's optimizer separately uses the `--jpeg-quality` command line
|
||||||
|
# option to potentially re-encode JPEG images, regardless of whether
|
||||||
|
# Ghostscript decided to transcode them to JPEG or not.
|
||||||
|
# `jpeg_quality=0` is meaningful to Ghostscript (maximum compression), so
|
||||||
|
# only fall back to the default when the value is None.
|
||||||
|
effective_jpeg_quality = jpeg_quality if jpeg_quality is not None else 95
|
||||||
|
|
||||||
|
# Downsampling images is a blunt-force way to reduce file size and almost
|
||||||
|
# always degrades quality more than lowering JPEG quality at the original
|
||||||
|
# resolution. We expose this for users with very specific needs (e.g.
|
||||||
|
# producing very small files for screen-only viewing); the optimizer is
|
||||||
|
# usually a better choice.
|
||||||
|
downsample_args: list[str] = []
|
||||||
|
if jpeg_maxdpi is not None:
|
||||||
|
downsample_args = [
|
||||||
|
"-dDownsampleColorImages=true",
|
||||||
|
"-dColorImageDownsampleThreshold=1.0",
|
||||||
|
"-dDownsampleGrayImages=true",
|
||||||
|
"-dGrayImageDownsampleThreshold=1.0",
|
||||||
|
"-dDownsampleMonoImages=true",
|
||||||
|
"-dMonoImageDownsampleThreshold=1.0",
|
||||||
|
f"-dColorImageResolution={jpeg_maxdpi}",
|
||||||
|
f"-dGrayImageResolution={jpeg_maxdpi}",
|
||||||
|
f"-dMonoImageResolution={jpeg_maxdpi}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# nb no need to specify ProcessColorModel when ColorConversionStrategy
|
||||||
# is set; see:
|
# is set; see:
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
|
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
|
||||||
# 2. `-dJPEGQ=95` tells Ghostscript to use a JPEG quality of 95, IF it
|
|
||||||
# decides to transcode an image to JPEG. When there are existing JPEG
|
|
||||||
# images, Ghostscript uses passthrough mode, so the quality level is not
|
|
||||||
# changed. OCRmyPDF's optimizer uses the `--jpeg-quality` command line
|
|
||||||
# option to potentially re-encoded JPEG images, regardless of whether
|
|
||||||
# Ghostscript decided to transcode them to JPEG or not.
|
|
||||||
args_gs = (
|
args_gs = (
|
||||||
[
|
[
|
||||||
GS,
|
GS,
|
||||||
@@ -340,8 +365,9 @@ def generate_pdfa(
|
|||||||
]
|
]
|
||||||
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
|
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
|
||||||
+ compression_args
|
+ compression_args
|
||||||
|
+ downsample_args
|
||||||
+ [
|
+ [
|
||||||
"-dJPEGQ=95", # See note above on JPEG quality
|
f"-dJPEGQ={effective_jpeg_quality}", # See note above on JPEG quality
|
||||||
"-dSubsetFonts=false", # Prevents GS from messing up some encodings
|
"-dSubsetFonts=false", # Prevents GS from messing up some encodings
|
||||||
f"-dPDFA={pdfa_part}",
|
f"-dPDFA={pdfa_part}",
|
||||||
"-dPDFACompatibilityPolicy=1",
|
"-dPDFACompatibilityPolicy=1",
|
||||||
|
|||||||
@@ -582,6 +582,13 @@ class OcrOptions(BaseModel):
|
|||||||
value = getattr(self, flat_name)
|
value = getattr(self, flat_name)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
kwargs[field_name] = _convert_value(value)
|
kwargs[field_name] = _convert_value(value)
|
||||||
|
# Plugin-scoped fields that aren't in the central OcrOptions
|
||||||
|
# registry: argparse stores them in extra_attrs under the
|
||||||
|
# namespace_field name.
|
||||||
|
elif flat_name in self.extra_attrs:
|
||||||
|
value = self.extra_attrs[flat_name]
|
||||||
|
if value is not None:
|
||||||
|
kwargs[field_name] = _convert_value(value)
|
||||||
# Also check direct field name (for fields like jbig2_lossy)
|
# Also check direct field name (for fields like jbig2_lossy)
|
||||||
elif field_name in OcrOptions.model_fields:
|
elif field_name in OcrOptions.model_fields:
|
||||||
value = getattr(self, field_name)
|
value = getattr(self, field_name)
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ class GhostscriptOptions(BaseModel):
|
|||||||
pdfa_image_compression: Annotated[
|
pdfa_image_compression: Annotated[
|
||||||
PdfaImageCompression, Field(description="PDF/A image compression method")
|
PdfaImageCompression, Field(description="PDF/A image compression method")
|
||||||
] = PdfaImageCompression.AUTO
|
] = PdfaImageCompression.AUTO
|
||||||
|
jpeg_quality: Annotated[
|
||||||
|
int | None,
|
||||||
|
Field(
|
||||||
|
ge=0,
|
||||||
|
le=100,
|
||||||
|
description=(
|
||||||
|
"JPEG quality (0-100) for Ghostscript image recompression during "
|
||||||
|
"PDF/A generation; None uses Ghostscript's default."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] = None
|
||||||
|
jpeg_maxdpi: Annotated[
|
||||||
|
int | None,
|
||||||
|
Field(
|
||||||
|
ge=1,
|
||||||
|
description=(
|
||||||
|
"Maximum DPI for Ghostscript image downsampling during PDF/A "
|
||||||
|
"generation."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_arguments_to_parser(cls, parser, namespace: str = 'ghostscript'):
|
def add_arguments_to_parser(cls, parser, namespace: str = 'ghostscript'):
|
||||||
@@ -86,6 +107,35 @@ class GhostscriptOptions(BaseModel):
|
|||||||
"skipped. Not supported for --output-type=pdf ; that setting "
|
"skipped. Not supported for --output-type=pdf ; that setting "
|
||||||
"preserves the original compression of all images.",
|
"preserves the original compression of all images.",
|
||||||
)
|
)
|
||||||
|
gs.add_argument(
|
||||||
|
'--ghostscript-jpeg-quality',
|
||||||
|
type=int,
|
||||||
|
metavar='Q',
|
||||||
|
default=None,
|
||||||
|
dest=f'{namespace}_jpeg_quality',
|
||||||
|
help=(
|
||||||
|
"Advanced: Set Ghostscript's -dJPEGQ for images that Ghostscript "
|
||||||
|
"transcodes to JPEG during PDF/A generation. 0 is maximum "
|
||||||
|
"compression; 100 is best quality. If omitted, Ghostscript's "
|
||||||
|
"default is used. This only affects images Ghostscript chooses "
|
||||||
|
"to recompress; for general JPEG quality tuning prefer "
|
||||||
|
"--jpeg-quality, which is applied by the OCRmyPDF optimizer."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
gs.add_argument(
|
||||||
|
'--ghostscript-jpeg-maxdpi',
|
||||||
|
type=int,
|
||||||
|
metavar='DPI',
|
||||||
|
default=None,
|
||||||
|
dest=f'{namespace}_jpeg_maxdpi',
|
||||||
|
help=(
|
||||||
|
"Advanced: Force Ghostscript to downsample color, grayscale, "
|
||||||
|
"and monochrome images in PDF/A output to the given maximum DPI. "
|
||||||
|
"Reducing JPEG quality usually gives better results than "
|
||||||
|
"downsampling at the same file size, and can degrade quality "
|
||||||
|
"of high-resolution monochrome masks."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
@@ -352,6 +402,8 @@ def generate_pdfa(
|
|||||||
output_file=output_file,
|
output_file=output_file,
|
||||||
compression=context.options.ghostscript.pdfa_image_compression,
|
compression=context.options.ghostscript.pdfa_image_compression,
|
||||||
color_conversion_strategy=context.options.ghostscript.color_conversion_strategy,
|
color_conversion_strategy=context.options.ghostscript.color_conversion_strategy,
|
||||||
|
jpeg_quality=context.options.ghostscript.jpeg_quality,
|
||||||
|
jpeg_maxdpi=context.options.ghostscript.jpeg_maxdpi,
|
||||||
pdf_version=pdf_version,
|
pdf_version=pdf_version,
|
||||||
pdfa_part=pdfa_part,
|
pdfa_part=pdfa_part,
|
||||||
progressbar_class=progressbar_class,
|
progressbar_class=progressbar_class,
|
||||||
|
|||||||
+129
-4
@@ -137,6 +137,129 @@ def test_rasterize_low_dpi_one_axis(francais, outdir):
|
|||||||
assert im.info['dpi'] == forced_dpi
|
assert im.info['dpi'] == forced_dpi
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_pdfa_default_jpeg_quality(outdir):
|
||||||
|
"""When jpeg_quality is None, Ghostscript receives -dJPEGQ=95 (default)."""
|
||||||
|
with (
|
||||||
|
patch('ocrmypdf._exec.ghostscript.version', return_value=Version('10.05.1')),
|
||||||
|
patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as run_mock,
|
||||||
|
):
|
||||||
|
run_mock.return_value = subprocess.CompletedProcess(
|
||||||
|
['gs'], returncode=0, stdout='', stderr=''
|
||||||
|
)
|
||||||
|
ghostscript.generate_pdfa(
|
||||||
|
pdf_pages=[outdir / 'input.pdf'],
|
||||||
|
output_file=outdir / 'out.pdf',
|
||||||
|
compression='auto',
|
||||||
|
color_conversion_strategy='LeaveColorUnchanged',
|
||||||
|
)
|
||||||
|
|
||||||
|
args = run_mock.call_args.args[0]
|
||||||
|
assert '-dJPEGQ=95' in args
|
||||||
|
# No downsample switches when jpeg_maxdpi is not set
|
||||||
|
assert not any(a.startswith('-dDownsampleColorImages') for a in args)
|
||||||
|
assert not any(a.startswith('-dColorImageResolution') for a in args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_pdfa_uses_user_jpeg_quality(outdir):
|
||||||
|
with (
|
||||||
|
patch('ocrmypdf._exec.ghostscript.version', return_value=Version('10.05.1')),
|
||||||
|
patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as run_mock,
|
||||||
|
):
|
||||||
|
run_mock.return_value = subprocess.CompletedProcess(
|
||||||
|
['gs'], returncode=0, stdout='', stderr=''
|
||||||
|
)
|
||||||
|
ghostscript.generate_pdfa(
|
||||||
|
pdf_pages=[outdir / 'input.pdf'],
|
||||||
|
output_file=outdir / 'out.pdf',
|
||||||
|
compression='jpeg',
|
||||||
|
color_conversion_strategy='RGB',
|
||||||
|
jpeg_quality=72,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = run_mock.call_args.args[0]
|
||||||
|
assert '-dJPEGQ=72' in args
|
||||||
|
assert '-dJPEGQ=95' not in args
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_pdfa_jpeg_quality_zero_is_max_compression(outdir):
|
||||||
|
"""Explicit jpeg_quality=0 must reach Ghostscript as -dJPEGQ=0.
|
||||||
|
|
||||||
|
Ghostscript accepts 0 as a valid quality value (maximum compression);
|
||||||
|
it must not be silently replaced by the default 95.
|
||||||
|
"""
|
||||||
|
with (
|
||||||
|
patch('ocrmypdf._exec.ghostscript.version', return_value=Version('10.05.1')),
|
||||||
|
patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as run_mock,
|
||||||
|
):
|
||||||
|
run_mock.return_value = subprocess.CompletedProcess(
|
||||||
|
['gs'], returncode=0, stdout='', stderr=''
|
||||||
|
)
|
||||||
|
ghostscript.generate_pdfa(
|
||||||
|
pdf_pages=[outdir / 'input.pdf'],
|
||||||
|
output_file=outdir / 'out.pdf',
|
||||||
|
compression='jpeg',
|
||||||
|
color_conversion_strategy='RGB',
|
||||||
|
jpeg_quality=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = run_mock.call_args.args[0]
|
||||||
|
assert '-dJPEGQ=0' in args
|
||||||
|
assert '-dJPEGQ=95' not in args
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_pdfa_honors_jpeg_maxdpi(outdir):
|
||||||
|
with (
|
||||||
|
patch('ocrmypdf._exec.ghostscript.version', return_value=Version('10.05.1')),
|
||||||
|
patch('ocrmypdf._exec.ghostscript.run_polling_stderr') as run_mock,
|
||||||
|
):
|
||||||
|
run_mock.return_value = subprocess.CompletedProcess(
|
||||||
|
['gs'], returncode=0, stdout='', stderr=''
|
||||||
|
)
|
||||||
|
ghostscript.generate_pdfa(
|
||||||
|
pdf_pages=[outdir / 'input.pdf'],
|
||||||
|
output_file=outdir / 'out.pdf',
|
||||||
|
compression='auto',
|
||||||
|
color_conversion_strategy='LeaveColorUnchanged',
|
||||||
|
jpeg_maxdpi=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = run_mock.call_args.args[0]
|
||||||
|
assert '-dJPEGQ=95' in args
|
||||||
|
assert '-dDownsampleColorImages=true' in args
|
||||||
|
assert '-dColorImageDownsampleThreshold=1.0' in args
|
||||||
|
assert '-dDownsampleGrayImages=true' in args
|
||||||
|
assert '-dGrayImageDownsampleThreshold=1.0' in args
|
||||||
|
assert '-dDownsampleMonoImages=true' in args
|
||||||
|
assert '-dMonoImageDownsampleThreshold=1.0' in args
|
||||||
|
assert '-dColorImageResolution=300' in args
|
||||||
|
assert '-dGrayImageResolution=300' in args
|
||||||
|
assert '-dMonoImageResolution=300' in args
|
||||||
|
|
||||||
|
|
||||||
|
def test_ghostscript_jpeg_options_via_cli(resources, outpdf):
|
||||||
|
"""End-to-end: CLI flags reach the ghostscript plugin namespace."""
|
||||||
|
with patch(
|
||||||
|
'ocrmypdf._exec.ghostscript.generate_pdfa',
|
||||||
|
wraps=ghostscript.generate_pdfa,
|
||||||
|
) as gen_mock:
|
||||||
|
run_ocrmypdf_api(
|
||||||
|
resources / 'francais.pdf',
|
||||||
|
outpdf,
|
||||||
|
'--output-type',
|
||||||
|
'pdfa',
|
||||||
|
'--ghostscript-jpeg-quality',
|
||||||
|
'60',
|
||||||
|
'--ghostscript-jpeg-maxdpi',
|
||||||
|
'150',
|
||||||
|
'--plugin',
|
||||||
|
'tests/plugins/tesseract_noop.py',
|
||||||
|
)
|
||||||
|
assert gen_mock.called
|
||||||
|
call_kwargs = gen_mock.call_args.kwargs
|
||||||
|
assert call_kwargs['jpeg_quality'] == 60
|
||||||
|
assert call_kwargs['jpeg_maxdpi'] == 150
|
||||||
|
|
||||||
|
|
||||||
def test_gs_render_failure(resources, outpdf, caplog):
|
def test_gs_render_failure(resources, outpdf, caplog):
|
||||||
exitcode = run_ocrmypdf_api(
|
exitcode = run_ocrmypdf_api(
|
||||||
resources / 'blank.pdf',
|
resources / 'blank.pdf',
|
||||||
@@ -176,9 +299,9 @@ def test_ghostscript_pdfa_failure(resources, outpdf, caplog):
|
|||||||
'--plugin',
|
'--plugin',
|
||||||
'tests/plugins/gs_pdfa_failure.py',
|
'tests/plugins/gs_pdfa_failure.py',
|
||||||
)
|
)
|
||||||
assert (
|
assert exitcode == ExitCode.pdfa_conversion_failed, (
|
||||||
exitcode == ExitCode.pdfa_conversion_failed
|
"Unexpected return when PDF/A fails"
|
||||||
), "Unexpected return when PDF/A fails"
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ghostscript_feature_elision(resources, outpdf):
|
def test_ghostscript_feature_elision(resources, outpdf):
|
||||||
@@ -439,7 +562,9 @@ class TestGs106JpegCorruptionRepair:
|
|||||||
repaired_bytes_list.append(obj.read_raw_bytes())
|
repaired_bytes_list.append(obj.read_raw_bytes())
|
||||||
|
|
||||||
assert len(repaired_bytes_list) == len(original_bytes_list)
|
assert len(repaired_bytes_list) == len(original_bytes_list)
|
||||||
for orig, repaired_bytes in zip(original_bytes_list, repaired_bytes_list, strict=False):
|
for orig, repaired_bytes in zip(
|
||||||
|
original_bytes_list, repaired_bytes_list, strict=False
|
||||||
|
):
|
||||||
assert orig == repaired_bytes, "Repaired bytes should match original"
|
assert orig == repaired_bytes, "Repaired bytes should match original"
|
||||||
|
|
||||||
# Check that error/warning was logged
|
# Check that error/warning was logged
|
||||||
|
|||||||
Reference in New Issue
Block a user