Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] some PDFs raise a NotImplementedError
### Description

Since 1.9.2 (I skipped 1.9 and 1.9.1) account statements of german dkb bank (also ones previously working fine) get stuck in processing.
There are some statements about barcode separation, maybe something broken there, as there is no barcode visible in the PDF.
Unfortunately also "reprinting" the PDF with a tool usually fixing issues for files which cannot be processed in papaerless is not working here.
Unfortunately I cannot share samples as its bank data....

Have barcode separation enabled:
PAPERLESS_CONSUMER_ENABLE_BARCODES=true
PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT

### Steps to reproduce

1. get the document consumed
2. process gets stuck in frontend
![image](https://user-images.githubusercontent.com/82444848/193867252-7cbf145f-ed99-4661-85de-4f1685c2a725.png)
3. shows up as queued for quite some time
![image](https://user-images.githubusercontent.com/82444848/193867603-6c67a193-04b5-40cc-bd0a-2581dbc98e21.png)
4. ends up as failed task

### Webserver logs

```bash
Kontoauszug_1023735317_Nr_2022_003_per_2022_08_01.pdf
HISTORY

Id:
79423d05e71d4a10b2457bf23f91cd60
Name:
Kontoauszug_1023735317_Nr_2022_003_per_2022_08_01.pdf
Func:
documents.tasks.consume_file
Hook:
-
Args:
/tmp/paperless/paperless-upload-0n2e8jty
Kwargs:
{'override_filename': 'Kontoauszug_1023735317_Nr_2022_003_per_2022_08_01.pdf', 'override_title': None, 'override_correspondent_id': None, 'override_document_type_id': None, 'override_tag_ids': None, 'task_id': 'dc8d5b5b-5eec-4395-b29a-22f4ee8df01a', 'override_created': None}
Result:
Not sure how to handle PDF image of this type : Traceback (most recent call last):
File "/usr/src/paperless/src/src/django-q/django_q/cluster.py", line 454, in worker
res = f(*task["args"], **task["kwargs"])
File "/usr/src/paperless/src/documents/tasks.py", line 99, in consume_file
pdf_filepath, separators = barcodes.scan_file_for_separating_barcodes(path)
File "/usr/src/paperless/src/documents/barcodes.py", line 126, in scan_file_for_separating_barcodes
pillow_img = pdfimage.as_pil_image()
File "/usr/local/lib/python3.9/site-packages/pikepdf/models/image.py", line 742, in as_pil_image
im = self._extract_transcoded()
File "/usr/local/lib/python3.9/site-packages/pikepdf/models/image.py", line 606, in _extract_transcoded
if self.mode in {'DeviceN', 'Separation'}:
File "/usr/local/lib/python3.9/site-packages/pikepdf/models/image.py", line 301, in mode
raise NotImplementedError(
NotImplementedError: Not sure how to handle PDF image of this type
Group:
-
Started:
Oct. 4, 2022, 5:52 p.m.
Stopped:
Oct. 4, 2022, 5:56 p.m.
```


### Paperless-ngx version

1.9.2

### Host OS

Synology with Docker

### Installation method

Docker - ghcr.io image
webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest

### Browser

Chrome

### Configuration changes

barcodes for page separation was enabled, problem is resolved after commenting it out in compose file:
#PAPERLESS_CONSUMER_ENABLE_BARCODES=true
#PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT

### Other

_No response_


Discussion:
Sometimes there are significant differences between the lsio image and ours. Unfortunately they are no longer supporting their image so you will need to switch to ours, helpful guide here: https://paperless-ngx.readthedocs.io/en/latest/setup.html#migrating-from-linuxserver-io-docker-image

If you can confirm this issue with our image then please let us know, of course.
sorry, provided the wrong docker image reference. Issue is updated, I am using the ghcr.io one
webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest

Can the bug be reopened, pls?
Ah, ok thanks for clarifying. Still odd, not a bug Ive seen before. Usually it is helpful to have an example file if you can find one without sensitive data. Perhaps other folks will have more insights
Probably related: pikepdf/pikepdf/issues/237

It seems some PDF images are not convertible to PIL images for pyzbar.

Resolving patch:
diff --git a/src/documents/barcodes.py b/src/documents/barcodes.py
--- a/src/documents/barcodes.py
+++ b/src/documents/barcodes.py
@@ -9,6 +9,7 @@
 
 import magic
 from django.conf import settings
+from pdf2image import convert_from_path
 from pikepdf import Page
 from pikepdf import Pdf
 from pikepdf import PdfImage
@@ -19,6 +20,10 @@
 logger = logging.getLogger("paperless.barcodes")
 
 
+class BarcodeImageFormatError(Exception):
+    pass
+
+
 @lru_cache(maxsize=8)
 def supported_file_type(mime_type) -> bool:
     """
@@ -108,6 +113,33 @@ def scan_file_for_separating_barcodes(filepath: str) -> Tuple[Optional[str], Lis
     which separate the file into new files
     """
 
+    def _pikepdf_barcode_scan(pdf_filepath: str):
+        with Pdf.open(pdf_filepath) as pdf:
+            for page_num, page in enumerate(pdf.pages):
+                for image_key in page.images:
+                    pdfimage = PdfImage(page.images[image_key])
+
+                    if "/CCITTFaxDecode" in pdfimage.filters:
+                        raise BarcodeImageFormatError()
+
+                    # Not all images can be transcoded to a PIL image, which
+                    # is what pyzbar expects to receive
+                    pillow_img = pdfimage.as_pil_image()
+
+                    detected_barcodes = barcode_reader(pillow_img)
+
+                    if settings.CONSUMER_BARCODE_STRING in detected_barcodes:
+                        separator_page_numbers.append(page_num)
+
+    def _pdf2image_barcode_scan(pdf_filepath: str):
+        # use a temporary directory in case the file os too big to handle in memory
+        with tempfile.TemporaryDirectory() as path:
+            pages_from_path = convert_from_path(pdf_filepath, output_folder=path)
+            for current_page_number, page in enumerate(pages_from_path):
+                current_barcodes = barcode_reader(page)
+                if settings.CONSUMER_BARCODE_STRING in current_barcodes:
+                    separator_page_numbers.append(current_page_number)
+
     separator_page_numbers = []
     pdf_filepath = None
 
@@ -118,17 +150,17 @@ def scan_file_for_separating_barcodes(filepath: str) -> Tuple[Optional[str], Lis
         if mime_type == "image/tiff":
             pdf_filepath = convert_from_tiff_to_pdf(filepath)
 
-        pdf = Pdf.open(pdf_filepath)
-
-        for page_num, page in enumerate(pdf.pages):
-            for image_key in page.images:
-                pdfimage = PdfImage(page.images[image_key])
-                pillow_img = pdfimage.as_pil_image()
+        try:
+            _pikepdf_barcode_scan(pdf_filepath)
+        except Exception as e:
 
-                detected_barcodes = barcode_reader(pillow_img)
+            logger.warning(
+                f"Exception using pikepdf for barcodes, falling back to pdf2image: {e}",
+            )
+            # Reset this incase pikepdf got part way through
+            separator_page_numbers = []
+            _pdf2image_barcode_scan(pdf_filepath)
 
-                if settings.CONSUMER_BARCODE_STRING in detected_barcodes:
-                    separator_page_numbers.append(page_num)
     else:
         logger.warning(
             f"Unsupported file format for barcode reader: {str(mime_type)}",


Test patch:
diff --git a/src/documents/tests/samples/barcodes/barcode-fax-image.pdf b/src/documents/tests/samples/barcodes/barcode-fax-image.pdf
new file mode 100644
Binary files /dev/null and b/src/documents/tests/samples/barcodes/barcode-fax-image.pdf differ
diff --git a/src/documents/tests/test_barcodes.py b/src/documents/tests/test_barcodes.py
--- a/src/documents/tests/test_barcodes.py
+++ b/src/documents/tests/test_barcodes.py
@@ -3,6 +3,7 @@
 import tempfile
 from unittest import mock
 
+import pikepdf
 from django.conf import settings
 from django.test import override_settings
 from django.test import TestCase
@@ -218,6 +219,86 @@ def test_scan_file_for_separating_barcodes_upsidedown(self):
         self.assertEqual(pdf_file, test_file)
         self.assertListEqual(separator_page_numbers, [1])
 
+    def test_scan_file_for_separating_barcodes_pillow_transcode_error(self):
+        """
+        GIVEN:
+            - A PDF containing an image which cannot be transcoded to a PIL image
+        WHEN:
+            - The image tries to be transcoded to a PIL image, but fails
+        THEN:
+            - The barcode reader is still called
+        """
+
+        def _build_device_n_pdf(self, save_path: str):
+            # Based on the pikepdf tests
+            # https://github.com/pikepdf/pikepdf/blob/abb35ebe17d579d76abe08265e00cf8890a12a95/tests/test_image_access.py
+            pdf = pikepdf.new()
+            pdf.add_blank_page(page_size=(72, 72))
+            imobj = pikepdf.Stream(
+                pdf,
+                bytes(range(0, 256)),
+                BitsPerComponent=8,
+                ColorSpace=pikepdf.Array(
+                    [
+                        pikepdf.Name.DeviceN,
+                        pikepdf.Array([pikepdf.Name.Black]),
+                        pikepdf.Name.DeviceCMYK,
+                        pikepdf.Stream(
+                            pdf,
+                            b"{0 0 0 4 -1 roll}",  # Colorspace conversion function
+                            FunctionType=4,
+                            Domain=[0.0, 1.0],
+                            Range=[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
+                        ),
+                    ],
+                ),
+                Width=16,
+                Height=16,
+                Type=pikepdf.Name.XObject,
+                Subtype=pikepdf.Name.Image,
+            )
+            pim = pikepdf.PdfImage(imobj)
+            self.assertEqual(pim.mode, "DeviceN")
+            self.assertTrue(pim.is_device_n)
+
+            pdf.pages[0].Contents = pikepdf.Stream(pdf, b"72 0 0 72 0 0 cm /Im0 Do")
+            pdf.pages[0].Resources = pikepdf.Dictionary(
+                XObject=pikepdf.Dictionary(Im0=imobj),
+            )
+            pdf.save(save_path)
+
+        with tempfile.NamedTemporaryFile(suffix="pdf") as device_n_pdf:
+            # Build an offending file
+            _build_device_n_pdf(self, str(device_n_pdf.name))
+            with mock.patch("documents.barcodes.barcode_reader") as reader:
+                reader.return_value = list()
+
+                _, _ = barcodes.scan_file_for_separating_barcodes(
+                    str(device_n_pdf.name),
+                )
+
+                reader.assert_called()
+
+    def test_scan_file_for_separating_barcodes_fax_decode(self):
+        """
+        GIVEN:
+            - A PDF containing an image encoded as CCITT Group 4 encoding
+        WHEN:
+            - Barcode processing happens with the file
+        THEN:
+            - The barcode is still detected
+        """
+        test_file = os.path.join(
+            self.BARCODE_SAMPLE_DIR,
+            "barcode-fax-image.pdf",
+        )
+        pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
+            test_file,
+        )
+
+        self.assertEqual(pdf_file, test_file)
+        self.assertListEqual(separator_page_numbers, [1])
+
     def test_scan_file_for_separating_qr_barcodes(self):
         test_file = os.path.join(
             self.BARCODE_SAMPLE_DIR,