Skip to content

Fix ICO save producing an empty file for images smaller than 16x16#9745

Open
gaoflow wants to merge 1 commit into
python-pillow:mainfrom
gaoflow:fix-ico-save-small-image-empty
Open

Fix ICO save producing an empty file for images smaller than 16x16#9745
gaoflow wants to merge 1 commit into
python-pillow:mainfrom
gaoflow:fix-ico-save-small-image-empty

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 30, 2026

Copy link
Copy Markdown

Saving an image smaller than the smallest default ICO size (16×16) as ICO, without an explicit sizes argument, silently produces an empty, unreadable file:

import io
from PIL import Image

buf = io.BytesIO()
Image.new("RGBA", (8, 8), (10, 20, 30, 255)).save(buf, "ICO")
print(len(buf.getvalue()))          # 6  -> just the ICONDIR header, idCount=0
buf.seek(0)
Image.open(buf).load()              # UnidentifiedImageError: cannot identify image file

The 8×8 image is representable as an ICO (save(buf, "ICO", sizes=[(8, 8)]) works fine), but the default-sizes path drops it entirely with no error or warning.

Cause

The default sizes start at (16, 16), and _save skips every candidate size larger than the source image. For an image smaller than 16×16 every default candidate is skipped, so frames stays empty and o16(len(frames)) writes idCount=0 — a 6-byte header-only file.

Fix

Filter the requested sizes up front, and if none fit, fall back to the image's own size (capped at the 256×256 ICO maximum) so a valid, readable icon is always written. This also fixes the related case where every explicitly requested size is larger than the image (previously also an empty file) — consistent with the documented behaviour that oversized requested sizes are ignored.

Tests

test_save_smaller_than_default_sizes parametrizes (1, 1), (8, 8), (15, 15) and the non-square (8, 12), asserting each round-trips to a valid ICO of the right size with info["sizes"] and a corner pixel preserved. test_save_all_sizes_larger_than_image pins the explicit-sizes fallback. All fail on main with UnidentifiedImageError and pass with the fix; the full Tests/test_file_ico.py suite stays green (27 passed); ruff format, ruff check and mypy are clean.

Saving an image smaller than the smallest default size (16x16) as ICO,
without an explicit sizes argument, wrote a header-only 6-byte file with
zero icon entries. Reopening it raised UnidentifiedImageError and the image
was lost silently.

The default sizes start at 16x16 and the save loop skips every candidate
larger than the source image, so for a sub-16 image all candidates are
skipped and no frame is written. Filter the requested sizes up front and, if
none fit, fall back to the image's own size (capped at the 256x256 ICO
maximum) so a valid, readable icon is always written. This also covers the
case where every explicitly requested size is larger than the image.
@codspeed-hq

codspeed-hq Bot commented Jun 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 331 untouched benchmarks


Comparing gaoflow:fix-ico-save-small-image-empty (5bb1b35) with main (6590b1b)

Open in CodSpeed

@radarhere radarhere added the 🤖-assisted AI-assisted label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖-assisted AI-assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants