Repository: python-pillow/Pillow

Issue:
Incorrect image data and type after TIFF compression
When saving an image containing float data as a compressed TIFF the output image data and number type is incorrect (here the `float32` becomes `int32`). The output is correct if I specify `compression = None` or if the input image data is of e.g. type `uint8`. If I specify `compression = "something wrong"` I get no error message, the compression defaults to `"compression": "raw"`, but the output image data and type is still wrong.

```python
import numpy as np
from PIL import Image
from tempfile import TemporaryFile

with TemporaryFile() as fp:
    Z = np.array([[np.pi, np.pi], [np.pi, np.pi]], dtype = np.float32)
    img = Image.fromarray(Z)
    img.save(fp, format = "TIFF", compression = "packbits")
    img = Image.open(fp)
    print(img.info)   # {'compression': 'packbits', 'dpi': (1, 1), 'resolution': (1, 1)}
    Z = np.asarray(img)
    print(Z)   # [[1078530011, 1078530011], [1078530011, 1078530011]]
    print(Z.dtype)   # int32
```


Discussion:
Just a note - it is libtiff that is converting an F mode image to I mode. When libtiff is not used, it stays in F mode.

Here is a demonstration without NumPy.
```python
from PIL import Image, TiffImagePlugin

for libtiff in (True, False):
    TiffImagePlugin.WRITE_LIBTIFF = libtiff
    img = Image.new("F", (1, 1), 0.5)
    path = ("libtiff" if libtiff else "without_libtiff")+".tif"
    img.save(path)
    with Image.open(path) as reloaded:
        print(libtiff, reloaded.mode)
```
gives
```
True I
False F
```

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1676,8 +1676,6 @@ def _save(im, fp, filename):
 
         # optional types for non core tags
         types = {}
-        # SAMPLEFORMAT is determined by the image format and should not be copied
-        # from legacy_ifd.
         # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
         # based on the data in the strip.
         # The other tags expect arrays with a certain length (fixed or depending on
@@ -1686,7 +1684,6 @@ def _save(im, fp, filename):
         # SUBIFD may also cause a segfault.
         blocklist += [
             REFERENCEBLACKWHITE,
-            SAMPLEFORMAT,
             STRIPBYTECOUNTS,
             STRIPOFFSETS,
             TRANSFERFUNCTION,
@@ -1702,9 +1699,14 @@ def _save(im, fp, filename):
         legacy_ifd = {}
         if hasattr(im, "tag"):
             legacy_ifd = im.tag.to_v2()
-        for tag, value in itertools.chain(
-            ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items()
-        ):
+
+        # SAMPLEFORMAT is determined by the image format and should not be copied
+        # from legacy_ifd.
+        supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
+        if SAMPLEFORMAT in supplied_tags:
+            del supplied_tags[SAMPLEFORMAT]
+
+        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
             # Libtiff can only process certain core items without adding
             # them to the custom dictionary.
             # Custom items are supported for int, float, unicode, string and byte
@@ -1729,6 +1731,9 @@ def _save(im, fp, filename):
                 else:
                     atts[tag] = value
 
+        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
+            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
+
         logger.debug("Converted items: %s" % sorted(atts.items()))
 
         # libtiff always expects the bytes in native order.


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
@@ -9,7 +9,7 @@
 import pytest
 
 from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
-from PIL.TiffImagePlugin import STRIPOFFSETS, SUBIFD
+from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
 
 from .helper import (
     assert_image_equal,
@@ -825,6 +825,17 @@ def test_sampleformat(self):
 
             assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
 
+    def test_sampleformat_write(self, tmp_path):
+        im = Image.new("F", (1, 1))
+        out = str(tmp_path / "temp.tif")
+        TiffImagePlugin.WRITE_LIBTIFF = True
+        im.save(out)
+        TiffImagePlugin.WRITE_LIBTIFF = False
+
+        with Image.open(out) as reloaded:
+            assert reloaded.mode == "F"
+            assert reloaded.getexif()[SAMPLEFORMAT] == 3
+
     def test_lzw(self):
         with Image.open("Tests/images/hopper_lzw.tif") as im:
             assert im.mode == "RGB"