commit 38c96fa1f43944821a92515e958111ca72d1df56
parent d35ce8131530787cff96cf044faf4e4deaf6f9b4
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 16 Sep 2026 21:30:02 -0400
icns: fall back to a decodable icon in Decode
Decode failed outright when the largest icon was JPEG 2000 while DecodeAll
quietly skipped such icons, so the same file decoded through one entry point
and not the other. Returning the largest icon we can actually decode makes
the two agree.
Diffstat:
2 files changed, 32 insertions(+), 11 deletions(-)
diff --git a/reader.go b/reader.go
@@ -11,9 +11,9 @@ import (
var jpeg2000header = []byte{0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20}
-// Decode finds the largest icon listed in the icns file and returns it,
-// ignoring all other sizes. The format returned will be PNG. JPEG 2000
-// icons are ignored due to lack of image decoding support.
+// Decode returns the largest decodable icon in the icns file, ignoring all
+// other sizes. JPEG 2000 icons are skipped due to lack of image decoding
+// support, so the result may be smaller than the largest icon present.
func Decode(r io.Reader) (image.Image, error) {
icons, err := decode(r)
if err != nil {
@@ -22,15 +22,17 @@ func Decode(r io.Reader) (image.Image, error) {
sort.Slice(icons, func(ii, jj int) bool {
return icons[ii].OsType.Size > icons[jj].OsType.Size
})
- icon := icons[0]
- if icon.IconDescription.ImageFormat == ImageFormatJPEG2000 {
- return nil, fmt.Errorf("%w: largest icon %s is %s", ErrUnsupportedFormat, icon.OsType, icon.ImageFormat)
- }
- img, _, err := image.Decode(icon.r)
- if err != nil {
- return nil, fmt.Errorf("decoding largest image (icon %s %s): %w", icon.OsType, icon.ImageFormat, err)
+ for _, icon := range icons {
+ if icon.ImageFormat == ImageFormatJPEG2000 {
+ continue
+ }
+ img, _, err := image.Decode(icon.r)
+ if err != nil {
+ return nil, fmt.Errorf("decoding icon %s %s: %w", icon.OsType, icon.ImageFormat, err)
+ }
+ return img, nil
}
- return img, nil
+ return nil, fmt.Errorf("%w: only %s icons present", ErrUnsupportedFormat, ImageFormatJPEG2000)
}
// DecodeAll extracts all icon resolutions present in the icns data that
diff --git a/reader_test.go b/reader_test.go
@@ -92,6 +92,25 @@ func TestDecodeSkipsNonIconElements(t *testing.T) {
}
}
+func TestDecodeFallsBackPastJPEG2000(t *testing.T) {
+ t.Parallel()
+ data := file(
+ element("ic10", jpeg2000header), // Largest, but undecodable.
+ element("ic07", pngBytes(t, 128)),
+ )
+ img, err := Decode(bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := img.Bounds().Dx(); got != 128 {
+ t.Fatalf("Decode returned a %dpx icon, want the 128px PNG", got)
+ }
+ all, err := DecodeAll(bytes.NewReader(data))
+ if err != nil || len(all) != 1 {
+ t.Fatalf("DecodeAll = %d images, %v; want 1, nil", len(all), err)
+ }
+}
+
// FuzzDecode checks that arbitrary input never panics or hangs the decoder.
func FuzzDecode(f *testing.F) {
var valid bytes.Buffer