fix: write BMP DIB entries in ICO so Windows exe shows correct icon
Pillow's ICO saver stores all sizes as PNG-compressed ("Vista icon"
format). PyInstaller's Windows resource-updater cannot embed
PNG-compressed entries for small sizes and silently falls back to its
default gear icon.
Fix build_icons.py to write the ICO manually: BMP DIB for sizes ≤ 128 px,
PNG only for the 256 px entry (where Windows Explorer expects PNG).
Regenerate icons/app.ico with the new code.
Also set upx=False in bcc.spec for the Windows/Linux EXE; UPX is another
known cause of icon resources being stripped from PE files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ else:
|
|||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=True,
|
upx=False, # UPX can strip icon resources from the PE on Windows
|
||||||
upx_exclude=[],
|
upx_exclude=[],
|
||||||
runtime_tmpdir=None,
|
runtime_tmpdir=None,
|
||||||
console=False,
|
console=False,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 113 KiB |
+63
-6
@@ -32,22 +32,79 @@ def build_ico():
|
|||||||
print("Pillow not installed — skipping .ico generation (pip install pillow)")
|
print("Pillow not installed — skipping .ico generation (pip install pillow)")
|
||||||
return
|
return
|
||||||
|
|
||||||
sizes = [16, 32, 48, 64, 128, 256]
|
import io, struct
|
||||||
imgs = []
|
|
||||||
|
sizes = [16, 24, 32, 48, 64, 128, 256]
|
||||||
|
images: list[tuple[int, object]] = []
|
||||||
for s in sizes:
|
for s in sizes:
|
||||||
p = SRC / f"icon-{s}.png"
|
p = SRC / f"icon-{s}.png"
|
||||||
if not p.exists():
|
if not p.exists():
|
||||||
print(f" Missing {p.name}, skipping")
|
print(f" Missing {p.name}, skipping")
|
||||||
continue
|
continue
|
||||||
imgs.append(Image.open(p).convert("RGBA"))
|
images.append((s, Image.open(p).convert("RGBA")))
|
||||||
|
|
||||||
if not imgs:
|
if not images:
|
||||||
print(" No source PNGs found — cannot build .ico")
|
print(" No source PNGs found — cannot build .ico")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Write the ICO manually so that entries ≤ 128 px use uncompressed BMP DIB
|
||||||
|
# and the 256 px entry uses PNG. Pillow's ICO saver stores all sizes as
|
||||||
|
# PNG-compressed ("Vista icon" format), which PyInstaller's Windows
|
||||||
|
# resource-updater cannot embed — it silently falls back to its default icon.
|
||||||
|
|
||||||
|
def bmp_dib(img: object) -> bytes:
|
||||||
|
"""Return a BMP DIB (BITMAPINFOHEADER + BGRA rows + AND mask)."""
|
||||||
|
w, h = img.size
|
||||||
|
# biHeight is doubled: top half = XOR mask (color), bottom = AND mask
|
||||||
|
hdr = struct.pack(
|
||||||
|
"<IiiHHIIiiII",
|
||||||
|
40, # biSize
|
||||||
|
w,
|
||||||
|
h * 2, # biHeight (doubled per ICO convention)
|
||||||
|
1, # biPlanes
|
||||||
|
32, # biBitCount
|
||||||
|
0, # biCompression (BI_RGB)
|
||||||
|
0, # biSizeImage (0 ok for BI_RGB)
|
||||||
|
0, 0, 0, 0,
|
||||||
|
)
|
||||||
|
pix = img.load()
|
||||||
|
rows = bytearray()
|
||||||
|
for y in range(h - 1, -1, -1): # bottom-up
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b, a = pix[x, y]
|
||||||
|
rows += bytes([b, g, r, a])
|
||||||
|
# AND mask: 1 bit/pixel, rows padded to 32-bit boundary, all 0 (use alpha)
|
||||||
|
mask_row = ((w + 31) // 32) * 4
|
||||||
|
and_mask = bytes(mask_row * h)
|
||||||
|
return hdr + bytes(rows) + and_mask
|
||||||
|
|
||||||
|
def png_bytes(img: object) -> bytes:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
blobs: list[tuple[int, bytes]] = []
|
||||||
|
for s, img in images:
|
||||||
|
blobs.append((s, bmp_dib(img) if s < 256 else png_bytes(img)))
|
||||||
|
|
||||||
|
n = len(blobs)
|
||||||
|
dir_offset = 6 + n * 16 # ICONDIR (6) + n × ICONDIRENTRY (16)
|
||||||
|
|
||||||
|
out = bytearray()
|
||||||
|
out += struct.pack("<HHH", 0, 1, n) # ICONDIR
|
||||||
|
|
||||||
|
cur = dir_offset
|
||||||
|
for s, blob in blobs:
|
||||||
|
w = h = s % 256 # 256 is stored as 0 in the byte field
|
||||||
|
out += struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(blob), cur)
|
||||||
|
cur += len(blob)
|
||||||
|
|
||||||
|
for _, blob in blobs:
|
||||||
|
out += blob
|
||||||
|
|
||||||
dest = OUT / "app.ico"
|
dest = OUT / "app.ico"
|
||||||
imgs[-1].save(dest, format="ICO", append_images=imgs[:-1])
|
dest.write_bytes(out)
|
||||||
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024 + 1} KB)")
|
print(f" Generated {dest.relative_to(ROOT)} ({len(out) // 1024 + 1} KB)")
|
||||||
|
|
||||||
|
|
||||||
def build_icns():
|
def build_icns():
|
||||||
|
|||||||
Reference in New Issue
Block a user