diff --git a/misc/completion/ocrmypdf.bash b/misc/completion/ocrmypdf.bash index bbd6a5a4..b21c4c1e 100644 --- a/misc/completion/ocrmypdf.bash +++ b/misc/completion/ocrmypdf.bash @@ -6,53 +6,55 @@ set -o errexit __ocrmypdf_arguments() { - local arguments="--help (show help message) ---language (language(s) of the file to be OCRed) ---image-dpi (assume this DPI if input image DPI is unknown) ---output-type (select PDF output options) ---sidecar (write OCR to text file) ---version (print program version and exit) ---jobs (how many worker processes to use) ---quiet (suppress INFO messages) ---verbose (set verbosity level) ---title (set metadata) ---author (set metadata) ---subject (set metadata) ---keywords (set metadata) ---rotate-pages (rotate pages to correct orientation) ---remove-background (attempt to remove background from pages) ---deskew (fix small horizontal alignment skew) ---clean (clean document images before OCR) ---clean-final (clean document images and keep result) ---unpaper-args (a quoted string of arguments to pass to unpaper) ---oversample (oversample images to this DPI) ---remove-vectors (don\'t send vector objects to OCR) ---threshold (threshold images before OCR) ---force-ocr (OCR documents that already have printable text) ---skip-text (skip OCR on any pages that already contain text) ---redo-ocr (redo OCR on any pages that seem to have OCR already) + local arguments="\ +--help (show help message) +--language (language(s) of the file to be OCRed) +--image-dpi (assume this DPI if input image DPI is unknown) +--output-type (select PDF output options) +--sidecar (write OCR to text file) +--version (print program version and exit) +--jobs (how many worker processes to use) +--quiet (suppress INFO messages) +--verbose (set verbosity level) +--title (set metadata) +--author (set metadata) +--subject (set metadata) +--keywords (set metadata) +--rotate-pages (rotate pages to correct orientation) +--remove-background (attempt to remove background from pages) +--deskew (fix small horizontal alignment skew) +--clean (clean document images before OCR) +--clean-final (clean document images and keep result) +--unpaper-args (a quoted string of arguments to pass to unpaper) +--oversample (oversample images to this DPI) +--remove-vectors (don\'t send vector objects to OCR) +--threshold (threshold images before OCR) +--force-ocr (OCR documents that already have printable text) +--skip-text (skip OCR on any pages that already contain text) +--redo-ocr (redo OCR on any pages that seem to have OCR already) --invalidate-digital-signatures (remove digital signatures from PDF) ---skip-big (skip OCR on pages larger than this many MPixels) ---optimize (select optimization level) ---jpeg-quality (JPEG quality [0..100]) ---png-quality (PNG quality [0..100]) ---jbig2-lossy (enable lossy JBIG2 (see docs)) ---pages (apply OCR to only the specified pages) ---max-image-mpixels (image decompression bomb threshold) ---pdf-renderer (select PDF renderer options) ---rotate-pages-threshold (page rotation confidence) ---pdfa-image-compression (set PDF/A image compression options) ---fast-web-view (if file size if above this amount in MB linearize PDF) ---plugin (name of plugin to import) ---keep-temporary-files (keep temporary files (debug) ---tesseract-config (set custom tesseract config file) ---tesseract-pagesegmode (set tesseract --psm) ---tesseract-oem (set tesseract --oem) ---tesseract-thresholding (set tesseract image thresholding) ---tesseract-timeout (maximum number of seconds to wait for OCR) ---user-words (specify location of user words file) ---user-patterns (specify location of user patterns file) ---no-progress-bar (disable the progress bar) +--skip-big (skip OCR on pages larger than this many MPixels) +--optimize (select optimization level) +--jpeg-quality (JPEG quality [0..100]) +--png-quality (PNG quality [0..100]) +--jbig2-lossy (enable lossy JBIG2 (see docs)) +--pages (apply OCR to only the specified pages) +--max-image-mpixels (image decompression bomb threshold) +--pdf-renderer (select PDF renderer options) +--rotate-pages-threshold (page rotation confidence) +--pdfa-image-compression (set PDF/A image compression options) +--fast-web-view (if file size if above this amount in MB linearize PDF) +--plugin (name of plugin to import) +--keep-temporary-files (keep temporary files (debug) +--tesseract-config (set custom tesseract config file) +--tesseract-pagesegmode (set tesseract --psm) +--tesseract-oem (set tesseract --oem) +--tesseract-thresholding (set tesseract image thresholding) +--tesseract-timeout (maximum number of seconds to wait for OCR) +--user-words (specify location of user words file) +--user-patterns (specify location of user patterns file) +--no-progress-bar (disable the progress bar) +--color-conversion-strategy (select color conversion strategy) " COMPREPLY=( $( compgen -W "$arguments" -- "$cur") ) @@ -192,6 +194,20 @@ sauvola (use Sauvola thresholding)" fi } +__ocrmypdf_color-conversion-strategy() +{ + local choices="LeaveColorUnchanged (default) +CMYK (convert to CMYK) +Gray (convert to grayscale) +RGB (convert to RGB) +UseDeviceIndependentColor (convert with device independent color)" + + COMPREPLY=( $( compgen -W "$choices" -- "$cur") ) + # Remove description if only one completion exists + if [[ ${#COMPREPLY[*]} -eq 1 ]]; then + COMPREPLY=( ${COMPREPLY[0]%% *} ) + fi +} __ocrmypdf_check_previous() { @@ -251,6 +267,10 @@ __ocrmypdf_check_previous() _filedir return 0 ;; + --color-conversion-strategy) + __ocrmypdf_color-conversion-strategy + return 0 + ;; esac return 1 diff --git a/misc/completion/ocrmypdf.fish b/misc/completion/ocrmypdf.fish index 12b7ea9c..20cd40e7 100644 --- a/misc/completion/ocrmypdf.fish +++ b/misc/completion/ocrmypdf.fish @@ -129,4 +129,14 @@ complete -c ocrmypdf -r -l user-words -d "specify location of user words file" complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file" complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF" +function __fish_ocrmypdf_color_conversion_strategy + echo -e "LeaveColorUnchanged\t"(_ "do not convert color spaces (default)") + echo -e "CMYK\t"(_ "convert all color spaces to CMYK") + echo -e "Gray\t"(_ "convert all color spaces to grayscale") + echo -e "RGB\t"(_ "convert all color spaces to RGB") + echo -e "UseDeviceIndependentColor\t"(_ "convert all color spaces to ICC-based color spaces") +end + +complete -c ocrmypdf -x -l color-conversion-strategy -a '(__fish_ocrmypdf_color_conversion_strategy)' -d "set color conversion strategy" + complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf; __fish_complete_suffix .PDF; __fish_complete_suffix .jpg; __fish_complete_suffix .png)" diff --git a/src/ocrmypdf/_exec/ghostscript.py b/src/ocrmypdf/_exec/ghostscript.py index 5e5e4fb5..7e40dba2 100644 --- a/src/ocrmypdf/_exec/ghostscript.py +++ b/src/ocrmypdf/_exec/ghostscript.py @@ -28,6 +28,17 @@ except AttributeError: # Pillow 9 shim Transpose = Image # type: ignore + +COLOR_CONVERSION_STRATEGIES = frozenset( + [ + 'CMYK', + 'Gray', + 'LeaveColorUnchanged', + 'RGB', + 'UseDeviceIndependentColor', + ] +) + log = logging.getLogger(__name__) # Most reliable way to get the bitness of Python interpreter, according to Python docs @@ -160,6 +171,7 @@ def generate_pdfa( output_file: os.PathLike, *, compression: str, + color_conversion_strategy: str, pdf_version: str = '1.5', pdfa_part: str = '2', progressbar_class=None, @@ -209,16 +221,16 @@ def generate_pdfa( "-dBATCH", "-dNOPAUSE", "-dSAFER", - "-dCompatibilityLevel=" + str(pdf_version), + f"-dCompatibilityLevel={str(pdf_version)}", "-sDEVICE=pdfwrite", "-dAutoRotatePages=/None", - "-sColorConversionStrategy=" + strategy, + f"-sColorConversionStrategy={color_conversion_strategy}", ] + (['-dPDFSTOPONERROR'] if stop_on_error else []) + compression_args + [ "-dJPEGQ=95", - "-dPDFA=" + pdfa_part, + f"-dPDFA={pdfa_part}", "-dPDFACompatibilityPolicy=1", "-o", "-", diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 4d4eab66..add00082 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -793,6 +793,7 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) - pdfmark=input_ps_stub, output_file=output_file, compression=options.pdfa_image_compression, + color_conversion_strategy=options.color_conversion_strategy, pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3 progressbar_class=( context.plugin_manager.hook.get_progressbar_class() diff --git a/src/ocrmypdf/builtin_plugins/ghostscript.py b/src/ocrmypdf/builtin_plugins/ghostscript.py index 5d6e1374..04f0e9d0 100644 --- a/src/ocrmypdf/builtin_plugins/ghostscript.py +++ b/src/ocrmypdf/builtin_plugins/ghostscript.py @@ -14,6 +14,20 @@ from ocrmypdf.subprocess import check_external_program log = logging.getLogger(__name__) +@hookimpl +def add_options(parser): + gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript") + gs.add_argument( + '--color-conversion-strategy', + action='store', + type=str, + metavar='STRATEGY', + choices=ghostscript.COLOR_CONVERSION_STRATEGIES, + default='LeaveColorUnchanged', + help="Set Ghostscript color conversion strategy", + ) + + @hookimpl def check_options(options): """Check that the options are valid for this plugin.""" @@ -33,6 +47,10 @@ def check_options(options): if options.output_type == 'pdfa': options.output_type = 'pdfa-2' + if options.color_conversion_strategy not in ghostscript.COLOR_CONVERSION_STRATEGIES: + raise ValueError( + f"Invalid color conversion strategy: {options.color_conversion_strategy}" + ) @hookimpl @@ -68,6 +86,7 @@ def generate_pdfa( pdfmark, output_file, compression, + color_conversion_strategy, pdf_version, pdfa_part, progressbar_class, @@ -78,6 +97,7 @@ def generate_pdfa( pdf_pages=[*pdf_pages, pdfmark], output_file=output_file, compression=compression, + color_conversion_strategy=color_conversion_strategy, pdf_version=pdf_version, pdfa_part=pdfa_part, progressbar_class=progressbar_class, diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py index 46f2944e..8ce09696 100644 --- a/src/ocrmypdf/pluginspec.py +++ b/src/ocrmypdf/pluginspec.py @@ -467,6 +467,7 @@ def generate_pdfa( pdfmark: Path, output_file: Path, compression: str, + color_conversion_strategy: str, pdf_version: str, pdfa_part: str, progressbar_class, @@ -489,6 +490,8 @@ def generate_pdfa( possible. For lossless, all images should be converted to FlateEncode (lossless PNG). If an empty string, the PDF generator should make its own decisions about how to encode images. + color_conversion_strategy: A valid Ghostscript color conversion strategy + name. pdf_version: The minimum PDF version that the output file should be. At its own discretion, the PDF/A generator may raise the version, but should not lower it. diff --git a/tests/plugins/gs_feature_elision.py b/tests/plugins/gs_feature_elision.py index dcd4c849..7f6c7d9f 100644 --- a/tests/plugins/gs_feature_elision.py +++ b/tests/plugins/gs_feature_elision.py @@ -28,6 +28,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf pdfmark=pdfmark, output_file=output_file, compression=compression, + color_conversion_strategy='LeaveColorUnchanged', pdf_version=pdf_version, pdfa_part=pdfa_part, progressbar_class=None, diff --git a/tests/plugins/gs_pdfa_failure.py b/tests/plugins/gs_pdfa_failure.py index e9ec9697..df1c305e 100644 --- a/tests/plugins/gs_pdfa_failure.py +++ b/tests/plugins/gs_pdfa_failure.py @@ -30,6 +30,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf pdfmark=pdfmark, output_file=output_file, compression=compression, + color_conversion_strategy='LeaveColorUnchanged', pdf_version=pdf_version, pdfa_part=pdfa_part, progressbar_class=None, diff --git a/tests/plugins/gs_render_failure.py b/tests/plugins/gs_render_failure.py index 56ec3905..bdb83e04 100644 --- a/tests/plugins/gs_render_failure.py +++ b/tests/plugins/gs_render_failure.py @@ -25,6 +25,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf pdfmark=pdfmark, output_file=output_file, compression=compression, + color_conversion_strategy='LeaveColorUnchanged', pdf_version=pdf_version, pdfa_part=pdfa_part, progressbar_class=None, diff --git a/tests/plugins/gs_render_soft_error.py b/tests/plugins/gs_render_soft_error.py index f80e0401..45f0f769 100644 --- a/tests/plugins/gs_render_soft_error.py +++ b/tests/plugins/gs_render_soft_error.py @@ -35,6 +35,7 @@ def generate_pdfa( pdfmark=pdfmark, output_file=output_file, compression=compression, + color_conversion_strategy='LeaveColorUnchanged', pdf_version=pdf_version, pdfa_part=pdfa_part, progressbar_class=None, diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 3ef661bf..d7d0b1f5 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -86,7 +86,8 @@ def test_unset_metadata(output_type, field, resources, outpdf): 'title': b'NFY5f7Ft2DWMkxLhXwxvFf7eWR2KeK3vEDcd', 'author': b'yXaryipxyRk9dVjWjSSaVaNCKeLRgEVzPRMp', 'subject': b't49vimctvnuH7ZeAjAkv52ACvWFjcnm5MPJr', - 'keywords': b's9EeALwUg7urA7fnnhm5EtUyC54sW2WPUzqh'} + 'keywords': b's9EeALwUg7urA7fnnhm5EtUyC54sW2WPUzqh', + } p = run_ocrmypdf( input_file, @@ -364,6 +365,7 @@ def test_prevent_gs_invalid_xml(resources, outdir): options = get_parser().parse_args( args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf'] ) + options.color_conversion_strategy = 'LeaveColorUnchanged' pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf') context = PdfContext( options, outdir, outdir / 'layers.rendered.pdf', pdfinfo, get_plugin_manager([]) @@ -404,6 +406,7 @@ def test_malformed_docinfo(caplog, resources, outdir): 'b.pdf', ] ) + options.color_conversion_strategy = 'LeaveColorUnchanged' pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf') context = PdfContext( options, outdir, outdir / 'layers.rendered.pdf', pdfinfo, get_plugin_manager([])