Repository: python-pillow/Pillow (Python)

Issue:
P mode TGA writing broke in 9.1.0
<!--
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?
In https://github.com/CTimmerman/steganography/blob/main/requirements.txt i changed `Pillow>=7.2.0,<9.1.0` to `Pillow>=7.2.0,<=9.1.0` and ran `pip install --upgrade -r requirements.txt && python test\test_steganography.py`

### What did you expect to happen?
```
INFO:root:Testing P TGA...
INFO:root:Cover has 1,992 bits / 249 bytes available. (8-bit header)
INFO:root:Message has 176 bits / 22 bytes: b')[i\xf3\x99\xc1XE'... [41, 91, 105, 243, 153, 193, 88, 69]
INFO:root:Data hidden.
INFO:root:Message okay in P TGA.
```

### What actually happened?
```
INFO:root:Testing P TGA...
INFO:root:Cover has 1,992 bits / 249 bytes available. (8-bit header)
INFO:root:Message has 272 bits / 34 bytes: b'\x08\xf9\xcd\xf4\xfe\x1f\xd0\x94'... [8, 249, 205, 244, 254, 31, 208, 148]
INFO:root:Data hidden.
ERROR:root:image file is truncated (61 bytes not processed)
Traceback (most recent call last):
  File "test\test_steganography.py", line 99, in <module>
    test_fuzz()
  File "test\test_steganography.py", line 61, in test_fuzz
    assert reveal(img) == random_bytes  # nosec
  File "test\../src\steganography.py", line 163, in reveal
    bits = list(get_lowest_bits(cover))
  File "test\../src\steganography.py", line 34, in get_lowest_bits
    for pixel in img.getdata():
  File "C:\Users\C\Documents\code\GitHub\steganography\.venv\lib\site-packages\PIL\Image.py", line 1356, in getdata
    self.load()
  File "C:\Users\C\Documents\code\GitHub\steganography\.venv\lib\site-packages\PIL\ImageFile.py", line 251, in load
    raise OSError(
OSError: image file is truncated (61 bytes not processed)
```
### What are your OS, Python and Pillow versions?

* OS: Microsoft Windows [Version 10.0.19044.1889]
* Python: 3.8.5
* Pillow: 9.1.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.
-->

IrfanView 4.54 calls the file generated by Pillow 9.1.0 invalid, whereas output from the same code with Pillow 9.0.1 is no problem. Same with Pillow itself loading it.

```python
from PIL import Image

img = Image.new('P', (100, 20), color="white")
name = 'debug_pillow_9.1.0.tga'
img.save(name)
# Everything seems okay so far, until one tries to use the saved image that was created with the color parameter!
after = Image.open(name).load()
r'''
Using Pillow>=9.1.0:

Traceback (most recent call last):
  File "test\test_Pillow_9.1.0.py", line 59, in <module>
    after = Image.open(name).load()
  File "C:\Users\C\Documents\code\GitHub\steganography\.venv\lib\site-packages\PIL\ImageFile.py", line 195, in load
    self.im = Image.core.map_buffer(
ValueError: buffer is not large enough
'''
```

Invalid 8-bit BMP files written when not all palette colours are used
### What did you do?

I loaded an image (BMP or PNG) in 8-bit format. (i.e. with a palette of up to 256 colours)
The image did use a palette with _less than_ 256 colours.

Then I edited the image and saved it again, as 8-bit BMP.

EDIT: This may be a similar problem as #6500.

### What did you expect to happen?

The saved image should be viewable by Windows' default image viewer.

### What actually happened?

The saved image was corrupt and could not be displayed.

Reason: The offset calculation for the `bfSize` (file offset 0x02) and `bfOffBits` (file offset 0x0A) assumes that the palette is 256 colours large (0x400 bytes). However there are only 0x200 bytes written for the palette. (4 bytes * 128 colours)

The fields `biClrUsed` (file offset 0x2E) and `biClrImportant` (file offset 0x32) are also incorrectly set to 256 instead of 128.

### What are your OS, Python and Pillow versions?

* OS: Windows 10 1909
* Python: Python 3.7.6
* Pillow: 9.2.0

I attached an archive with two tests: [bmp-tests.zip](https://github.com/python-pillow/Pillow/files/9551375/bmp-tests.zip)
- Test 1: BMP image with 256 colours being used (works fine)
- Test 2: BMP image with 128 colours being used (broken after resaving)

```python
import PIL.Image

with PIL.Image.open("Test1_AllColors.bmp") as img1:
    img1.save("t1-good.bmp")

with PIL.Image.open("Test2_128Colors.bmp") as img2:
    img2.save("t2-bad.bmp")
```



Discussion:
I've created PR #6500 to resolve this.

I've added the solution for this to #6500
I can confirm that the fix from the mentioned PR fixes the issue.
Thanks a lot!



Patch:
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 7bb73fc9388..1041ab763d2 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
     header = 40  # or 64 for OS/2 version 2
     image = stride * im.size[1]
 
+    if im.mode == "1":
+        palette = b"".join(o8(i) * 4 for i in (0, 255))
+    elif im.mode == "L":
+        palette = b"".join(o8(i) * 4 for i in range(256))
+    elif im.mode == "P":
+        palette = im.im.getpalette("RGB", "BGRX")
+        colors = len(palette) // 4
+    else:
+        palette = None
+
     # bitmap header
     if bitmap_header:
         offset = 14 + header + colors * 4
@@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True):
 
     fp.write(b"\0" * (header - 40))  # padding (for OS/2 format)
 
-    if im.mode == "1":
-        for i in (0, 255):
-            fp.write(o8(i) * 4)
-    elif im.mode == "L":
-        for i in range(256):
-            fp.write(o8(i) * 4)
-    elif im.mode == "P":
-        fp.write(im.im.getpalette("RGB", "BGRX"))
+    if palette:
+        fp.write(palette)
 
     ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
 
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 59b89e9885f..cd454b755c0 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -193,9 +193,10 @@ def _save(im, fp, filename):
         warnings.warn("id_section has been trimmed to 255 characters")
 
     if colormaptype:
-        colormapfirst, colormaplength, colormapentry = 0, 256, 24
+        palette = im.im.getpalette("RGB", "BGR")
+        colormaplength, colormapentry = len(palette) // 3, 24
     else:
-        colormapfirst, colormaplength, colormapentry = 0, 0, 0
+        colormaplength, colormapentry = 0, 0
 
     if im.mode in ("LA", "RGBA"):
         flags = 8
@@ -210,7 +211,7 @@ def _save(im, fp, filename):
         o8(id_len)
         + o8(colormaptype)
         + o8(imagetype)
-        + o16(colormapfirst)
+        + o16(0)  # colormapfirst
         + o16(colormaplength)
         + o8(colormapentry)
         + o16(0)
@@ -225,7 +226,7 @@ def _save(im, fp, filename):
         fp.write(id_section)
 
     if colormaptype:
-        fp.write(im.im.getpalette("RGB", "BGR"))
+        fp.write(palette)
 
     if rle:
         ImageFile._save(


Test patch:
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index d58666b4476..4c964fbeac7 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -51,6 +51,18 @@ def test_save_to_bytes():
         assert reloaded.format == "BMP"
 
 
+def test_small_palette(tmp_path):
+    im = Image.new("P", (1, 1))
+    colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
+    im.putpalette(colors)
+
+    out = str(tmp_path / "temp.bmp")
+    im.save(out)
+
+    with Image.open(out) as reloaded:
+        assert reloaded.getpalette() == colors
+
+
 def test_save_too_large(tmp_path):
     outfile = str(tmp_path / "temp.bmp")
     with Image.new("RGB", (1, 1)) as im:
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 0c8c9f30485..fff127421d2 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -123,6 +123,18 @@ def test_save(tmp_path):
         assert test_im.size == (100, 100)
 
 
+def test_small_palette(tmp_path):
+    im = Image.new("P", (1, 1))
+    colors = [0, 0, 0]
+    im.putpalette(colors)
+
+    out = str(tmp_path / "temp.tga")
+    im.save(out)
+
+    with Image.open(out) as reloaded:
+        assert reloaded.getpalette() == colors
+
+
 def test_save_wrong_mode(tmp_path):
     im = hopper("PA")
     out = str(tmp_path / "temp.tga")