Repository: python-pillow/Pillow

Issue:
When I convert TIFF to PDF, the PDF size is 10 times that of TIFF
When I convert TIFF to PDF, the PDF size is 10 times that of TIFF
Why is that?



Discussion:
当我将TIFF转换为PDF时，PDF大小是TIFF的10倍
为什么会这样
```python
#tiff path path='XXX';
image = Image.open(path)
image.save(path, save_all=True)
```
A note for others - according to Google, the previous comment just translates to the first comment.

If I run your code over one of our test images, https://github.com/python-pillow/Pillow/blob/main/Tests/images/hopper.tif, I get [a PDF that is almost 10 times smaller.](https://github.com/python-pillow/Pillow/files/9147937/out.pdf)

So your situation does not apply to all TIFFs. Could you upload a copy of your image?
```python
image = Image.open('tif\\20220720170924738.TIF')
image.save('tif\\dst\\20220720170924738.pdf', save_all=True)
```

source :  20220720170924738.TIF  8.51kb
to Pdf  :  20220720170924738.pdf  180kb

files:  https://github.com/344672699/Pillow/blob/main/20220720170924738.rar


Thank you for your help
python:  3.8
pillow  :  9.1.1 
The compression used in your TIFF image is "group4".

https://en.wikipedia.org/wiki/Group_4_compression
> It is only used for bitonal (black-and-white) images.

When the PDF is saved by Pillow, the "DCTDecode" filter is used.

https://www.gemboxsoftware.com/pdf/docs/GemBox.Pdf.Filters.PdfDCTDecodeFilter.html
> The DCTDecode filter decodes grayscale or color image data that has been encoded in the JPEG baseline format.

For comparison, I tried converting your 9kb TIFF to PDF using ImageMagick. It came out as 12kb, rather than Pillow's 185kb. Looking at that PDF, it used the "CCITTFaxDecode" filter. This looks to also be [using group4 compression](https://shreevatsa.github.io/site/ccitt.html), so that is why it is so similar to your original image size.

Because "group4" compression is dedicated for only black and white images, it doesn't seem surprising that it is smaller. I mentioned that DCTDecode is for JPEG images, and Pillow is converting your image to a JPEG before saving it in the PDF file. If I convert your TIFF image to JPEG images using ImageMagick, they come out as a 63kb and a 113kb image. 63kb + 113kb = 176kb, close to the size of the final PDF.

So the answer to your question is that Pillow is not using the compression method dedicated to black-and-white images, but one that allows for more colours.
感谢你的帮助。讲解的非常详细。谢谢。
不过，我还有个疑问，当我使用pillow时，我可以自定义设置use the "CCITTFaxDecode" filter吗？或者有其他设置方式，使我的PDF减小到像ImageMagick一样的“12kb”吗，或者像ITEXT转换后的PDF一样小也可以。
如果不能，是否意味着，我不能使用pillow了，或者只能接收它转换后的10倍以上大小的PDF？


Thank you for your help. The explanations were very detailed. thank you.

However, I still have a question. When I use pillow, can I customize the "ccittfaxdecode" filter? Or is there any other setting method to reduce my PDF to "12KB" like ImageMagick, or as small as the PDF converted by iText.

If not, does it mean that I can't use pillow, or only receive PDF of more than 10 times its converted size?
At the moment, that option doesn't exist in Pillow, no.

If you would like, this issue can be left open as a request for someone to add the feature.
Alternatively, you may be interested in [img2pdf](https://github.com/josch/img2pdf). The following generates a 10kb file.

```python
import img2pdf

with open("out.pdf", "wb") as f:
    f.write(img2pdf.convert('20220720170924738.tif'))
```

Resolving patch:
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -21,6 +21,7 @@
 ##
 
 import io
+import math
 import os
 import time
 
@@ -123,8 +124,26 @@ def _save(im, fp, filename, save_all=False):
             params = None
             decode = None
 
+            #
+            # Get image characteristics
+
+            width, height = im.size
+
             if im.mode == "1":
-                filter = "DCTDecode"
+                filter = "CCITTFaxDecode"
+                bits = 1
+                params = PdfParser.PdfArray(
+                    [
+                        PdfParser.PdfDict(
+                            {
+                                "K": -1,
+                                "BlackIs1": True,
+                                "Columns": width,
+                                "Rows": height,
+                            }
+                        )
+                    ]
+                )
                 colorspace = PdfParser.PdfName("DeviceGray")
                 procset = "ImageB"  # grayscale
             elif im.mode == "L":
@@ -161,6 +180,14 @@ def _save(im, fp, filename, save_all=False):
 
             if filter == "ASCIIHexDecode":
                 ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
+            elif filter == "CCITTFaxDecode":
+                im.save(
+                    op,
+                    "TIFF",
+                    compression="group4",
+                    # use a single strip
+                    strip_size=math.ceil(im.width / 8) * im.height,
+                )
             elif filter == "DCTDecode":
                 Image.SAVE["JPEG"](im, op, filename)
             elif filter == "FlateDecode":
@@ -170,22 +197,24 @@ def _save(im, fp, filename, save_all=False):
             else:
                 raise ValueError(f"unsupported PDF filter ({filter})")
 
-            #
-            # Get image characteristics
-
-            width, height = im.size
+            stream = op.getvalue()
+            if filter == "CCITTFaxDecode":
+                stream = stream[8:]
+                filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
+            else:
+                filter = PdfParser.PdfName(filter)
 
             existing_pdf.write_obj(
                 image_refs[page_number],
-                stream=op.getvalue(),
+                stream=stream,
                 Type=PdfParser.PdfName("XObject"),
                 Subtype=PdfParser.PdfName("Image"),
                 Width=width,  # * 72.0 / resolution,
                 Height=height,  # * 72.0 / resolution,
-                Filter=PdfParser.PdfName(filter),
+                Filter=filter,
                 BitsPerComponent=bits,
                 Decode=decode,
-                DecodeParams=params,
+                DecodeParms=params,
                 ColorSpace=colorspace,
             )
 
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1684,7 +1684,8 @@ def _save(im, fp, filename):
     stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
     # aim for given strip size (64 KB by default) when using libtiff writer
     if libtiff:
-        rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1])
+        im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
+        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
         # JPEG encoder expects multiple of 8 rows
         if compression == "jpeg":
             rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])


Test patch:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1011,14 +1011,18 @@ def test_save_multistrip(self, compression, tmp_path):
             # Assert that there are multiple strips
             assert len(im.tag_v2[STRIPOFFSETS]) > 1
 
-    def test_save_single_strip(self, tmp_path):
+    @pytest.mark.parametrize("argument", (True, False))
+    def test_save_single_strip(self, argument, tmp_path):
         im = hopper("RGB").resize((256, 256))
         out = str(tmp_path / "temp.tif")
 
-        TiffImagePlugin.STRIP_SIZE = 2**18
+        if not argument:
+            TiffImagePlugin.STRIP_SIZE = 2**18
         try:
-
-            im.save(out, compression="tiff_adobe_deflate")
+            arguments = {"compression": "tiff_adobe_deflate"}
+            if argument:
+                arguments["strip_size"] = 2**18
+            im.save(out, **arguments)
 
             with Image.open(out) as im:
                 assert len(im.tag_v2[STRIPOFFSETS]) == 1
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -43,7 +43,7 @@ def test_monochrome(tmp_path):
 
     # Act / Assert
     outfile = helper_save_as_pdf(tmp_path, mode)
-    assert os.path.getsize(outfile) < 15000
+    assert os.path.getsize(outfile) < 5000
 
 
 def test_greyscale(tmp_path):