Repository: python-pillow/Pillow

Issue:
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:

Resolving patch:
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
--- 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
--- 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
--- 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
--- 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")