Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] OCRmyPDF fails due to passing image_dpi of 1
### Description

depending on the image being processed, Paperless sometimes has a habit of passing image_dpi of 1 to OCRmyPDF which causes it to fail, when then it turn causes ghostscript to fail. Manually running similar commands in the log on the images given gives me this:
```
[paperless@paperless ~]$ /usr/local/bin/ocrmypdf --output-type pdfa -l eng --image-dpi 1 /tmp/image004.png /tmp/archive.pdf
Input file is not a PDF, checking if it is an image...
Input file is an image
Input image has no ICC profile, assuming sRGB
Image seems valid. Try converting to PDF...
Successfully converted to PDF, processing...
Scanning contents: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 304.29page/s]
    1 Unrecoverable error: rangecheck in setscreen                                                                                                                                                                 
Operand stack:
    0.0624961  0  --nostringval--

OCR:   0%|                                                                                                                                                                             | 0.0/1.0 [00:00<?, ?page/s]
SubprocessOutputError: Ghostscript rasterizing failed
```

a successful command looks like this:
```
[paperless@paperless ~]$ /usr/local/bin/ocrmypdf --output-type pdfa -l eng --image-dpi 100 /tmp/image004.png /tmp/archive.pdf
Input file is not a PDF, checking if it is an image...
Input file is an image
Input image has no ICC profile, assuming sRGB
Image seems valid. Try converting to PDF...
Successfully converted to PDF, processing...
Scanning contents: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 312.52page/s]
OCR: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.0/1.0 [00:00<00:00,  1.47page/s]
Postprocessing...
PDF/A conversion: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  4.91page/s]
Recompressing JPEGs: 0image [00:00, ?image/s]
Deflating JPEGs: 0image [00:00, ?image/s]
JBIG2: 0item [00:00, ?item/s]
Image optimization ratio: 1.00 savings: 0.0%
Total file size ratio: 1.16 savings: 13.8%
Output file is a PDF/A-2B (as expected)
```

An affected image: https://github.com/paperless-ngx/paperless-ngx/assets/903688/09e45638-b688-49d5-9675-8e69366ec9df

OCRmyPDF was grabbed from pip, not through OS resources / `dnf` command.

As the solution, it may be prudent to cap the minimum `--image-dpi` given to the program.

Related issue: https://github.com/ocrmypdf/OCRmyPDF/issues/1102

### Steps to reproduce

1. Have Paperless parse an e-mail containing a similar image
2. Watch error

### Webserver logs

```bash
[2023-11-19 16:42:47,095] [INFO] [paperless.consumer] Consuming image004.png

[2023-11-19 16:42:47,097] [DEBUG] [paperless.consumer] Detected mime type: image/png

[2023-11-19 16:42:47,098] [DEBUG] [paperless.consumer] Parser: RasterisedDocumentParser

[2023-11-19 16:42:47,101] [DEBUG] [paperless.consumer] Parsing image004.png...

[2023-11-19 16:42:47,183] [WARNING] [paperless.parsing.tesseract] Error while getting DPI from image /tmp/paperless/paperless-ngxlg82u9md/image004.png: 'dpi'

[2023-11-19 16:42:47,183] [DEBUG] [paperless.parsing.tesseract] Estimated DPI 1 based on image width 13

[2023-11-19 16:42:47,184] [INFO] [paperless.parsing.tesseract] Removing alpha layer from /tmp/paperless/paperless-ngxlg82u9md/image004.png for compatibility with img2pdf

[2023-11-19 16:42:47,201] [DEBUG] [paperless.parsing.tesseract] Calling OCRmyPDF with args: {'input_file': PosixPath('/tmp/paperless/paperless-ngxlg82u9md/image004.png'), 'output_file': PosixPath('/tmp/paperless/paperless-jtusbnkt/archive.pdf'), 'use_threads': True, 'jobs': '4', 'language': 'eng', 'output_type': 'pdfa', 'progress_bar': False, 'skip_text': True, 'clean': True, 'deskew': True, 'rotate_pages': True, 'rotate_pages_threshold': 12.0, 'sidecar': PosixPath('/tmp/paperless/paperless-jtusbnkt/sidecar.txt'), 'image_dpi': 1}

[2023-11-19 16:42:47,591] [DEBUG] [paperless.parsing.tesseract] Using text from sidecar file

[2023-11-19 16:42:47,592] [WARNING] [paperless.parsing.tesseract] No text was found in /tmp/paperless/paperless-ngxzgm2rsq_/image003.png, the content will be empty.

[2023-11-19 16:42:47,592] [DEBUG] [paperless.consumer] Generating thumbnail for image003.png...

[2023-11-19 16:42:47,597] [DEBUG] [paperless.parsing] Execute: convert -density 300 -scale 500x5000> -alpha remove -strip -auto-orient /tmp/paperless/paperless-7cpe7lpk/archive.pdf[0] /tmp/paperless/paperless-7cpe7lpk/convert.webp

[2023-11-19 16:42:47,627] [ERROR] [paperless.consumer] Error occurred while consuming document image004.png: SubprocessOutputError: Ghostscript rasterizing failed. See logs for more information.

Traceback (most recent call last):

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_exec/ghostscript.py", line 99, in rasterize_pdf

    p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/subprocess/__init__.py", line 62, in run

    proc = subprocess_run(args, env=env, check=check, **kwargs)

  File "/usr/lib64/python3.9/subprocess.py", line 528, in run

    raise CalledProcessError(retcode, process.args,

subprocess.CalledProcessError: Command '['gs', '-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dInterpolateControl=-1', '-sDEVICE=jpeggray', '-dFirstPage=1', '-dLastPage=1', '-r1.000000x1.000000', '-dPDFSTOPONERROR', '-o', '-', '-sstdout=%stderr', '-dAutoRotatePages=/None', '-f', '/tmp/ocrmypdf.io.uustcjiz/origin.pdf']' returned non-zero exit status 255.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):

  File "/opt/paperless/src/paperless_tesseract/parsers.py", line 321, in parse

    ocrmypdf.ocr(**args)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/api.py", line 347, in ocr

    return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 391, in run_pipeline

    optimize_messages = exec_concurrent(context, executor)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 279, in exec_concurrent

    executor(

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_concurrent.py", line 85, in __call__

    self._execute(

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/concurrency.py", line 141, in _execute

    result = future.result()

  File "/usr/lib64/python3.9/concurrent/futures/_base.py", line 439, in result

    return self.__get_result()

  File "/usr/lib64/python3.9/concurrent/futures/_base.py", line 391, in __get_result

    raise self._exception

  File "/usr/lib64/python3.9/concurrent/futures/thread.py", line 58, in run

    result = self.fn(*self.args, **self.kwargs)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 191, in exec_page_sync

    rasterize_preview_out = rasterize_preview(page_context.origin, page_context)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_pipeline.py", line 363, in rasterize_preview

    page_context.plugin_manager.hook.rasterize_pdf_page(

  File "/usr/local/lib/python3.9/site-packages/pluggy/_hooks.py", line 433, in __call__

    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)

  File "/usr/local/lib/python3.9/site-packages/pluggy/_manager.py", line 112, in _hookexec

    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)

  File "/usr/local/lib/python3.9/site-packages/pluggy/_callers.py", line 116, in _multicall

    raise exception.with_traceback(exception.__traceback__)

  File "/usr/local/lib/python3.9/site-packages/pluggy/_callers.py", line 80, in _multicall

    res = hook_impl.function(*args)

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/ghostscript.py", line 51, in rasterize_pdf_page

    ghostscript.rasterize_pdf(

  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_exec/ghostscript.py", line 102, in rasterize_pdf

    raise SubprocessOutputError('Ghostscript rasterizing failed') from e

ocrmypdf.exceptions.SubprocessOutputError: Ghostscript rasterizing failed

The above exception was the direct cause of the following exception:

Traceback (most recent call last):

  File "/opt/paperless/src/documents/consumer.py", line 425, in try_consume_file

    document_parser.parse(self.path, mime_type, self.filename)

  File "/opt/paperless/src/paperless_tesseract/parsers.py", line 344, in parse

    raise ParseError(

documents.parsers.ParseError: SubprocessOutputError: Ghostscript rasterizing failed. See logs for more information.
```


### Browser logs

_No response_

### Paperless-ngx version

1.17.4

### Host OS

Rocky Linux release 9.2 5.14.0-284.30.1.el9_2.x86_64

### Installation method

Bare metal

### Browser

_No response_

### Configuration changes

_No response_

### Other

_No response_


Discussion:
Your provided sample file is fine?  `Estimated DPI 127 based on image width 1054`
Yeah, it's definitely interesting. I'm pretty certain I got the correct image but it's always possible I didn't.

The underlying issue seems to be these lines in the log:
```
[2023-11-19 16:42:47, 183] [WARNING] [paperless.parsing.tesseract] Error while getting DPI from image /tmp/paperless/paperless-ngxlg82u9md/image004.png: 'dpi'
[2023-11-19 16:42:47, 183] [DEBUG] [paperless.parsing.tesseract] Estimated DPI 1 based on image width 13
```
A 13 pixel wide image doesn't make much sense anyway.  From the linked issue, I'm not sure about just setting a floor on the DPI.  It feels arbitrary in what one would choose.
Wonder if it’s a tracking pixel or something with no real value. Even a little favicon or something shouldn’t do this
That may make more sense. It probably shouldn't error out, though, either way. Maybe an acceptable error state is to produce a warning and skip it? An entire stacktrace is probably not ideal.
There isn't really the concept of skipping it, but I could see adding a warning if the DPI is less than <something> that OCR might fail due to a low DPI value.  The error OCRMyPDF raises is too generic to key off and catch.  Ghostscript might fail for a lot of reasons.  How does that sound?
Yeah,not sure how best to handle that. A more helpful error, for sure - a warning about small images or DPI might work, since at least then it won't fill up your task logs.

Resolving patch:
diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py
--- a/src/paperless_tesseract/parsers.py
+++ b/src/paperless_tesseract/parsers.py
@@ -244,6 +244,10 @@ def construct_ocrmypdf_parameters(
                     f"no DPI information is present in this image and "
                     f"OCR_IMAGE_DPI is not set.",
                 )
+            if ocrmypdf_args["image_dpi"] < 70:  # pragma: no cover
+                self.log.warning(
+                    f"Image DPI of {ocrmypdf_args['image_dpi']} is low, OCR may fail",
+                )
 
         if settings.OCR_USER_ARGS and not safe_fallback:
             try:


Test patch: