icns

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

commit 335a96d4f5a847f6dba50f5572b012bc2909d756
parent 28e0d609d39c2ef139480defe3df865ca45af813
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Fri, 18 Sep 2026 16:09:33 -0400

cmd/icnsify: convert an iconset directory

Per-slot artwork was reachable only from Go, and an iconset directory is how
that artwork already arrives from a designer or from iconutil. Names that do
not match a slot are ignored rather than refused, so stray files in the
folder do not stop a conversion.

Diffstat:
Acmd/icnsify/iconset.go | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/icnsify/main.go | 5+++++
2 files changed, 71 insertions(+), 0 deletions(-)

diff --git a/cmd/icnsify/iconset.go b/cmd/icnsify/iconset.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "image" + "image/png" + "log/slog" + "os" + "path/filepath" + + "github.com/jackmordaunt/icns/v4" +) + +// encodeIconSet writes an icns from an iconset directory, the layout iconutil +// accepts: one PNG per slot, named icon_16x16.png, icon_16x16@2x.png and so +// on. Files that are not named that way are ignored. +func encodeIconSet(dir, out string, algorithm icns.InterpolationFunction) error { + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("reading iconset: %w", err) + } + images := make(map[icns.Slot]image.Image, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + slot, ok := icns.ParseSlot(entry.Name()) + if !ok { + continue + } + img, err := readPNG(filepath.Join(dir, entry.Name())) + if err != nil { + return err + } + images[slot] = img + slog.Info("found", "slot", slot, "file", entry.Name()) + } + if len(images) == 0 { + return fmt.Errorf("no iconset images in %s", dir) + } + if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil { + return fmt.Errorf("preparing output directory: %w", err) + } + f, err := os.Create(out) + if err != nil { + return fmt.Errorf("creating output file: %w", err) + } + defer f.Close() + if err := icns.NewEncoder(f).WithAlgorithm(algorithm).EncodeSlots(images); err != nil { + return fmt.Errorf("encoding icns: %w", err) + } + return nil +} + +func readPNG(path string) (image.Image, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("opening iconset image: %w", err) + } + defer f.Close() + img, err := png.Decode(f) + if err != nil { + return nil, fmt.Errorf("decoding %s: %w", filepath.Base(path), err) + } + return img, nil +} diff --git a/cmd/icnsify/main.go b/cmd/icnsify/main.go @@ -65,6 +65,11 @@ func run() error { usage() return errUsage } + // A directory is an iconset: artwork per slot rather than one image + // to resize for every size. + if info, err := os.Stat(in); err == nil && info.IsDir() { + return encodeIconSet(in, out, algorithm) + } sourcef, err := os.Open(in) if err != nil { return fmt.Errorf("opening source image: %w", err)