icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

commit 0cebcdbed776a5835e0115d58e622ec24258f3d2
parent bff19a681cc6bf5b6f291663d0ee0805332ad4d7
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Fri, 18 Sep 2026 16:09:31 -0400

icns: write a table of contents

Apple has written one since 10.7, and a reader after a single size must
otherwise walk every element to find it. It lists the type and total size of
everything that follows, which is the whole file bar the table itself.

Diffstat:
Micns_test.go | 18+++++++++++++++++-
Mwriter.go | 46+++++++++++++++++++++++++++++-----------------
2 files changed, 46 insertions(+), 18 deletions(-)

diff --git a/icns_test.go b/icns_test.go @@ -2,6 +2,7 @@ package icns import ( "bytes" + "encoding/binary" "fmt" "image" "image/color" @@ -133,12 +134,27 @@ func TestEncodedElements(t *testing.T) { got = append(got, el.id) } want := []string{ - "ic10", "ic14", "ic09", "ic13", "ic08", "ic07", "ic12", + "TOC ", "ic10", "ic14", "ic09", "ic13", "ic08", "ic07", "ic12", "ic11", "il32", "l8mk", "is32", "s8mk", } if !reflect.DeepEqual(got, want) { t.Fatalf("elements = %v, want %v", got, want) } + // The table of contents covers everything after it: type and total size. + toc := els[0].payload + if len(toc) != (len(els)-1)*elementHeaderSize { + t.Fatalf("table of contents is %d bytes, want %d", len(toc), (len(els)-1)*elementHeaderSize) + } + for i, el := range els[1:] { + entry := toc[i*elementHeaderSize:] + if id := string(entry[:4]); id != el.id { + t.Errorf("entry %d names %q, want %q", i, id, el.id) + } + size := binary.BigEndian.Uint32(entry[4:8]) + if want := uint32(elementHeaderSize + len(el.payload)); size != want { + t.Errorf("entry %d for %s gives size %d, want %d", i, el.id, size, want) + } + } } // TestLegacyRoundTrip checks that a 16px source survives the colour and mask diff --git a/writer.go b/writer.go @@ -79,32 +79,44 @@ func writeElement(wr io.Writer, el element) (int64, error) { // IconSet encodes a set of icons into an ICNS file. type IconSet struct { Icons []*Icon - - data []byte } // WriteTo writes the ICNS file to wr. func (s *IconSet) WriteTo(wr io.Writer) (int64, error) { - if err := s.encodeIcons(); err != nil { + var elements []element + for _, icon := range s.Icons { + if icon == nil { + continue + } + if err := icon.encode(); err != nil { + return 0, err + } + elements = append(elements, icon.elements...) + } + body := bytes.NewBuffer(nil) + // The table of contents comes first and covers everything after it, so a + // reader can index the file without walking every element. + if _, err := writeElement(body, tableOfContents(elements)); err != nil { return 0, err } + for _, el := range elements { + if _, err := writeElement(body, el); err != nil { + return 0, err + } + } // The file is itself an element enclosing all the others. - return writeElement(wr, element{id: "icns", payload: s.data}) + return writeElement(wr, element{id: "icns", payload: body.Bytes()}) } -func (s *IconSet) encodeIcons() error { - if len(s.data) > 0 { - return nil +// tableOfContents lists each element's type and total size, in order. +func tableOfContents(elements []element) element { + toc := element{ + id: "TOC ", + payload: make([]byte, 0, len(elements)*elementHeaderSize), } - buf := bytes.NewBuffer(nil) - for _, icon := range s.Icons { - if icon == nil { - continue - } - if _, err := icon.WriteTo(buf); err != nil { - return err - } + for _, el := range elements { + toc.payload = append(toc.payload, el.id...) + toc.payload = binary.BigEndian.AppendUint32(toc.payload, uint32(elementHeaderSize+len(el.payload))) } - s.data = buf.Bytes() - return nil + return toc }