Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] Multi-page TIFFs are truncated on import if they contain an alpha channel
### Description

Currently Paperless only imports the first page of a multi-page TIFF file if it contains an alpha channel.

This seems to be the culprit as only the first page is saved here after processing:
https://github.com/paperless-ngx/paperless-ngx/blob/main/src/paperless_tesseract/parsers.py#L227-L237

I would've submitted a PR, but unfortunately don't have the time in the forseeable future to do it properly. Therefore I worked around the issue by manually removing the alpha channel before import:
```bash
convert -alpha off withalpha.tiff withoutalpha.tiff
```
```bash
find . -name "*.tiff" -exec convert "{}" -alpha off "{}" \;
```

Although, a potential fix could be as simple as the following (not properly tested):
```python
    with Image.open(input_file) as im:
        im_frames = ImageSequence.all_frames(im, lambda f: f.convert("RGB"))
        im_frames[0].save(input_file, save_all=True, append_images=im_frames[1:])
```
Also see

- https://pillow.readthedocs.io/en/stable/handbook/tutorial.html#using-the-imagesequence-iterator-class
- https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#reading-multi-frame-tiff-images


### Steps to reproduce

1. Import new file `multipage.tiff`
2. Check page count of successfully imported file
3. Import new file `multipage-noalpha.tiff`
4. Check page count of successfully imported file


Demo files: [multipage-files.zip](https://github.com/paperless-ngx/paperless-ngx/files/10333025/multipage-files.zip)


### Webserver logs

```bash
Not relevant as no error is logged related to this issue.
```


### Browser logs

_No response_

### Paperless-ngx version

1.11.2

### Host OS

Ubuntu Server 22.10

### Installation method

Docker - official image

### Browser

_No response_

### Configuration changes

_No response_

### Other

_No response_


Discussion:

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
@@ -1,6 +1,7 @@
 import json
 import os
 import re
+import subprocess
 from pathlib import Path
 from typing import Optional
 
@@ -79,6 +80,17 @@ def has_alpha(self, image):
         with Image.open(image) as im:
             return im.mode in ("RGBA", "LA")
 
+    def remove_alpha(self, image_path: str):
+        subprocess.run(
+            [
+                settings.CONVERT_BINARY,
+                "-alpha",
+                "off",
+                image_path,
+                image_path,
+            ],
+        )
+
     def get_dpi(self, image):
         try:
             with Image.open(image) as im:
@@ -230,11 +242,7 @@ def construct_ocrmypdf_parameters(
                     f"Removing alpha layer from {input_file} "
                     "for compatibility with img2pdf",
                 )
-                with Image.open(input_file) as im:
-                    background = Image.new("RGBA", im.size, (255, 255, 255))
-                    background.alpha_composite(im)
-                    background = background.convert("RGB")
-                    background.save(input_file, format=im.format)
+                self.remove_alpha(input_file)
 
             if dpi:
                 self.log("debug", f"Detected DPI for image {input_file}: {dpi}")


Test patch:
diff --git a/src/paperless_tesseract/tests/samples/multi-page-images-alpha-rgb.tiff b/src/paperless_tesseract/tests/samples/multi-page-images-alpha-rgb.tiff
new file mode 100644
Binary files /dev/null and b/src/paperless_tesseract/tests/samples/multi-page-images-alpha-rgb.tiff differ
diff --git a/src/paperless_tesseract/tests/samples/multi-page-images-alpha.tiff b/src/paperless_tesseract/tests/samples/multi-page-images-alpha.tiff
new file mode 100644
Binary files /dev/null and b/src/paperless_tesseract/tests/samples/multi-page-images-alpha.tiff differ
diff --git a/src/paperless_tesseract/tests/samples/multi-page-images.tiff b/src/paperless_tesseract/tests/samples/multi-page-images.tiff
new file mode 100644
Binary files /dev/null and b/src/paperless_tesseract/tests/samples/multi-page-images.tiff differ
diff --git a/src/paperless_tesseract/tests/test_parser.py b/src/paperless_tesseract/tests/test_parser.py
--- a/src/paperless_tesseract/tests/test_parser.py
+++ b/src/paperless_tesseract/tests/test_parser.py
@@ -542,6 +542,69 @@ def test_rotate(self):
             ],
         )
 
+    def test_multi_page_tiff(self):
+        """
+        GIVEN:
+            - Multi-page TIFF image
+        WHEN:
+            - Image is parsed
+        THEN:
+            - Text from all pages extracted
+        """
+        parser = RasterisedDocumentParser(None)
+        parser.parse(
+            os.path.join(self.SAMPLE_FILES, "multi-page-images.tiff"),
+            "image/tiff",
+        )
+        self.assertTrue(os.path.isfile(parser.archive_path))
+        self.assertContainsStrings(
+            parser.get_text().lower(),
+            ["page 1", "page 2", "page 3"],
+        )
+
+    def test_multi_page_tiff_alpha(self):
+        """
+        GIVEN:
+            - Multi-page TIFF image
+            - Image include an alpha channel
+        WHEN:
+            - Image is parsed
+        THEN:
+            - Text from all pages extracted
+        """
+        parser = RasterisedDocumentParser(None)
+        parser.parse(
+            os.path.join(self.SAMPLE_FILES, "multi-page-images-alpha.tiff"),
+            "image/tiff",
+        )
+        self.assertTrue(os.path.isfile(parser.archive_path))
+        self.assertContainsStrings(
+            parser.get_text().lower(),
+            ["page 1", "page 2", "page 3"],
+        )
+
+    def test_multi_page_tiff_alpha_srgb(self):
+        """
+        GIVEN:
+            - Multi-page TIFF image
+            - Image include an alpha channel
+            - Image is srgb colorspace
+        WHEN:
+            - Image is parsed
+        THEN:
+            - Text from all pages extracted
+        """
+        parser = RasterisedDocumentParser(None)
+        parser.parse(
+            os.path.join(self.SAMPLE_FILES, "multi-page-images-alpha-rgb.tiff"),
+            "image/tiff",
+        )
+        self.assertTrue(os.path.isfile(parser.archive_path))
+        self.assertContainsStrings(
+            parser.get_text().lower(),
+            ["page 1", "page 2", "page 3"],
+        )
+
     def test_ocrmypdf_parameters(self):
         parser = RasterisedDocumentParser(None)
         params = parser.construct_ocrmypdf_parameters(