Fix ICO save producing an empty file for images smaller than 16x16 - #9745
binggao1230 wants to merge 1 commit into
Conversation
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.
I'm not convinced on this point. If the user requests a specific sets of sizes, and we can't provide them, I think an error should be raised, rather than silently giving the user back something other than what they requested. I think the expectation in #2266 agrees with me. I've created #9766 as an alternative. |
|
You're right about the explicit case — if the caller names sizes we can't provide, raising beats silently handing back something else. #9766 is the better shape. One edge case in it though: the default path raises too, when one dimension is under 16 and the other is over 256. Image.new("RGBA", (300, 8)).save("out.ico") # ValueError: All sizes too large for image
Happy to close this in favour of #9766. |
|
So you think 300x8 should save as 256x7? In that scenario, I wonder if it should also save as 128x3, 64x2 and 32x1. |
|
#9766 has been merged. |
Saving an image smaller than the smallest default ICO size (16×16) as ICO, without an explicit
sizesargument, silently produces an empty, unreadable file:The 8×8 image is representable as an ICO (
save(buf, "ICO", sizes=[(8, 8)])works fine), but the default-sizespath drops it entirely with no error or warning.Cause
The default
sizesstart at(16, 16), and_saveskips every candidate size larger than the source image. For an image smaller than 16×16 every default candidate is skipped, soframesstays empty ando16(len(frames))writesidCount=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_sizesparametrizes(1, 1),(8, 8),(15, 15)and the non-square(8, 12), asserting each round-trips to a valid ICO of the right size withinfo["sizes"]and a corner pixel preserved.test_save_all_sizes_larger_than_imagepins the explicit-sizesfallback. All fail onmainwithUnidentifiedImageErrorand pass with the fix; the fullTests/test_file_ico.pysuite stays green (27 passed);ruff format,ruff checkandmypyare clean.