Repository: python-pillow/Pillow

Issue:
Tag 262 (PhotometricInterpretation) is changed when saving as a TIFF
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?
save an uncompressed tiff file to group 4 compressed tiff file
### What did you expect to happen?
I expected to get a group 4 compressed tiff file and kept all other tags the same as original file
### What actually happened?
I got a group 4 compressed tiff file, but Tag 262 (PhotometricInterpretation) was changed from 0 to 1
### What are your OS, Python and Pillow versions?

* OS: Window 10
* Python: Python 3.6.8
* Pillow:  7.2.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
from PIL import Image

img = Image.open('uncompressed.tiff')
print(img.tag)
img.save('group4.tiff', compression="group4", tiffinfo=img.tag)
img = Image.open('group4.tiff')
print(img.tag)
```

[pillowIssue.zip](https://github.com/python-pillow/Pillow/files/4949280/pillowIssue.zip)



Discussion:
As an explanation of what is happening, the image you have provided has mode 1. This is how we are setting the photometric interpretation when saving.

https://github.com/python-pillow/Pillow/blob/5c9faf043f693e2c25b9aaf20e7e2f970e92b174/src/PIL/TiffImagePlugin.py#L1446
https://github.com/python-pillow/Pillow/blob/5c9faf043f693e2c25b9aaf20e7e2f970e92b174/src/PIL/TiffImagePlugin.py#L1418-L1421

So depending on the mode, it will not always be 1. But I agree that we're not using the same value that was set.

It controls ["the color space of the image data"](https://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html). This is not a random tag that we've just missed - if we were to just flip a switch and set it to 0, it would invert the colors for this case. We could invert the pixels correspondingly, but the value of doing so is not clear - is there any reason you would like it to be the same?
I am facing the same problem as eliu-alltrust and I think it is a bug in pillow.

The reason for the existence of tag 262 is that there is no general agreement on how values in 1-bit images are interpreted. "1" can be seen as "high intensity" = white or as "set a point" = black. Tag 262 specifies which of those two interpretations should be used when reading a file, not whether pixels are to be inverted when writing a file. 

However, the later is what happens in pillow: I changed the hard coded photometrics value in TiffImagePlugin.py line 1421 to 0 and pillow inverted my image data when saving. It should, however, leave the pixels unchanged regardless of photometrics and write tag 262 with the value specified by the user.

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -48,7 +48,7 @@
 from fractions import Fraction
 from numbers import Number, Rational
 
-from . import Image, ImageFile, ImagePalette, TiffTags
+from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
 from ._binary import o8
 from .TiffTags import TYPES
 
@@ -1487,7 +1487,9 @@ def _save(im, fp, filename):
 
     ifd = ImageFileDirectory_v2(prefix=prefix)
 
-    compression = im.encoderinfo.get("compression", im.info.get("compression"))
+    encoderinfo = im.encoderinfo
+    encoderconfig = im.encoderconfig
+    compression = encoderinfo.get("compression", im.info.get("compression"))
     if compression is None:
         compression = "raw"
     elif compression == "tiff_jpeg":
@@ -1505,7 +1507,7 @@ def _save(im, fp, filename):
     ifd[IMAGELENGTH] = im.size[1]
 
     # write any arbitrary tags passed in as an ImageFileDirectory
-    info = im.encoderinfo.get("tiffinfo", {})
+    info = encoderinfo.get("tiffinfo", {})
     logger.debug("Tiffinfo Keys: %s" % list(info))
     if isinstance(info, ImageFileDirectory_v1):
         info = info.to_v2()
@@ -1534,7 +1536,7 @@ def _save(im, fp, filename):
 
     # preserve ICC profile (should also work when saving other formats
     # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
-    icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
+    icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
     if icc:
         ifd[ICCPROFILE] = icc
 
@@ -1550,10 +1552,10 @@ def _save(im, fp, filename):
         (ARTIST, "artist"),
         (COPYRIGHT, "copyright"),
     ]:
-        if name in im.encoderinfo:
-            ifd[key] = im.encoderinfo[name]
+        if name in encoderinfo:
+            ifd[key] = encoderinfo[name]
 
-    dpi = im.encoderinfo.get("dpi")
+    dpi = encoderinfo.get("dpi")
     if dpi:
         ifd[RESOLUTION_UNIT] = 2
         ifd[X_RESOLUTION] = dpi[0]
@@ -1568,7 +1570,18 @@ def _save(im, fp, filename):
     if format != 1:
         ifd[SAMPLEFORMAT] = format
 
-    ifd[PHOTOMETRIC_INTERPRETATION] = photo
+    if PHOTOMETRIC_INTERPRETATION not in ifd:
+        ifd[PHOTOMETRIC_INTERPRETATION] = photo
+    elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
+        if im.mode == "1":
+            inverted_im = im.copy()
+            px = inverted_im.load()
+            for y in range(inverted_im.height):
+                for x in range(inverted_im.width):
+                    px[x, y] = 0 if px[x, y] == 255 else 255
+            im = inverted_im
+        else:
+            im = ImageOps.invert(im)
 
     if im.mode in ["P", "PA"]:
         lut = im.im.getpalette("RGB", "RGB;L")
@@ -1605,8 +1618,8 @@ def _save(im, fp, filename):
             ifd.setdefault(tag, value)
 
     if libtiff:
-        if "quality" in im.encoderinfo:
-            quality = im.encoderinfo["quality"]
+        if "quality" in encoderinfo:
+            quality = encoderinfo["quality"]
             if not isinstance(quality, int) or quality < 0 or quality > 100:
                 raise ValueError("Invalid quality setting")
             if compression != "jpeg":
@@ -1695,7 +1708,7 @@ def _save(im, fp, filename):
         tags = list(atts.items())
         tags.sort()
         a = (rawmode, compression, _fp, filename, tags, types)
-        e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
+        e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
         e.setimage(im.im, (0, 0) + im.size)
         while True:
             # undone, change to self.decodermaxblock:
@@ -1715,7 +1728,7 @@ def _save(im, fp, filename):
         )
 
     # -- helper for multi-page save --
-    if "_debug_multipage" in im.encoderinfo:
+    if "_debug_multipage" in encoderinfo:
         # just to access o32 and o16 (using correct byte order)
         im._debug_multipage = ifd
 


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -447,6 +447,15 @@ def test_exif_frames(self):
             im.seek(1)
             assert im.getexif()[273] == (1408, 1907)
 
+    @pytest.mark.parametrize("mode", ("1", "L"))
+    def test_photometric(self, mode, tmp_path):
+        filename = str(tmp_path / "temp.tif")
+        im = hopper(mode)
+        im.save(filename, tiffinfo={262: 0})
+        with Image.open(filename) as reloaded:
+            assert reloaded.tag_v2[262] == 0
+            assert_image_equal(im, reloaded)
+
     def test_seek(self):
         filename = "Tests/images/pil136.tiff"
         with Image.open(filename) as im: