Repository: python-pillow/Pillow

Issue:
Multi-strip usage when encoding group4 TIF breaks use in PDF
Hi,

since #5514, group4 tiff images are not anymore saved as a single strip. This breaks the use-case of utilizing PIL for encoding images as group4 for inclusion in PDF documents, as the PDF format only supports a single group4 encoded strip per image.

Please allow for a way to force single-strip group4 encoding to restore this functionality, maybe by allowing to specify the maximum strip length?

(I realize this sounds a lot like https://xkcd.com/1172/ :smile:)


Discussion:
If you understand #5514, and acknowledge that this is an obscure use case, is this hack interesting to you at all? It would let you override the values for ROWSPERSTRIP, STRIPBYTECOUNTS and STRIPOFFSETS.
```python
from PIL import Image, TiffImagePlugin

pillow__getitem__ = TiffImagePlugin.ImageFileDirectory_v2.__getitem__
def __getitem__(self, tag):
	overrides = {
		TiffImagePlugin.ROWSPERSTRIP: 500,
		TiffImagePlugin.STRIPBYTECOUNTS: 31500,
		TiffImagePlugin.STRIPOFFSETS: 0
	}
	return overrides.get(tag, pillow__getitem__(self, tag))
TiffImagePlugin.ImageFileDirectory_v2.__getitem__ = __getitem__

im = Image.open("Tests/images/hopper_g4_500.tif")
im.save("out.tif", compression="group4")
```
Perhaps adding a [constant that can be overridden](https://github.com/python-pillow/Pillow/blob/9025ae8332fd0348e9a1c1552d5f9a1f3b765ba1/src/PIL/TiffImagePlugin.py#L58-L60) instead of the [hard-coded 64 KB here](https://github.com/python-pillow/Pillow/blob/9025ae8332fd0348e9a1c1552d5f9a1f3b765ba1/src/PIL/TiffImagePlugin.py#L1622) could be a neat way to address this?
@radarhere thanks a lot for your input! Yes, that hack is indeed interesting to me because even if @kmilos solution is implemented in a future pillow version, we cannot travel back in time and thus I would need a solution for those pillow versions that have #5514 applied but do not yet have a constant that can be overridden.

Unfortunately, I'm afraid that I'm not getting your code snippet to work. Did you test it? I suspect that how it is written, the values will not be overwritten by the IFD writer.

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -58,6 +58,7 @@
 READ_LIBTIFF = False
 WRITE_LIBTIFF = False
 IFD_LEGACY_API = True
+STRIP_SIZE = 65536
 
 II = b"II"  # little-endian (Intel style)
 MM = b"MM"  # big-endian (Motorola style)
@@ -1617,11 +1618,9 @@ def _save(im, fp, filename):
         ifd[COLORMAP] = tuple(v * 256 for v in lut)
     # data orientation
     stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
-    # aim for 64 KB strips when using libtiff writer
+    # aim for given strip size (64 KB by default) when using libtiff writer
     if libtiff:
-        rows_per_strip = (
-            1 if stride == 0 else min((2 ** 16 + stride - 1) // stride, im.size[1])
-        )
+        rows_per_strip = 1 if stride == 0 else min(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
@@ -987,6 +987,20 @@ 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):
+        im = hopper("RGB").resize((256, 256))
+        out = str(tmp_path / "temp.tif")
+
+        TiffImagePlugin.STRIP_SIZE = 2 ** 18
+        try:
+
+            im.save(out, compression="tiff_adobe_deflate")
+
+            with Image.open(out) as im:
+                assert len(im.tag_v2[STRIPOFFSETS]) == 1
+        finally:
+            TiffImagePlugin.STRIP_SIZE = 65536
+
     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
     def test_save_zero(self, compression, tmp_path):
         im = Image.new("RGB", (0, 0))