icns

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

iconset.go (2736B)


      1 package main
      2 
      3 import (
      4 	"cmp"
      5 	"fmt"
      6 	"image"
      7 	"image/png"
      8 	"log/slog"
      9 	"os"
     10 	"path/filepath"
     11 	"slices"
     12 
     13 	"github.com/jackmordaunt/icns/v4"
     14 	"github.com/jackmordaunt/icns/v4/ico"
     15 )
     16 
     17 // encodeIconSet writes an icon container from an iconset directory, the
     18 // layout iconutil accepts: one PNG per slot, named icon_16x16.png,
     19 // icon_16x16@2x.png and so on. Files that are not named that way are ignored.
     20 func encodeIconSet(dir, out, target string, algorithm icns.InterpolationFunction) error {
     21 	if !containers[target] {
     22 		return fmt.Errorf("an iconset directory makes an icns or an ico, not %s", target)
     23 	}
     24 	entries, err := os.ReadDir(dir)
     25 	if err != nil {
     26 		return fmt.Errorf("reading iconset: %w", err)
     27 	}
     28 	images := make(map[icns.Slot]image.Image, len(entries))
     29 	for _, entry := range entries {
     30 		if entry.IsDir() {
     31 			continue
     32 		}
     33 		slot, ok := icns.ParseSlot(entry.Name())
     34 		if !ok {
     35 			continue
     36 		}
     37 		img, err := readPNG(filepath.Join(dir, entry.Name()))
     38 		if err != nil {
     39 			return err
     40 		}
     41 		images[slot] = img
     42 		slog.Info("found", "slot", slot, "file", entry.Name())
     43 	}
     44 	if len(images) == 0 {
     45 		return fmt.Errorf("no iconset images in %s", dir)
     46 	}
     47 	if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
     48 		return fmt.Errorf("preparing output directory: %w", err)
     49 	}
     50 	f, err := os.Create(out)
     51 	if err != nil {
     52 		return fmt.Errorf("creating output file: %w", err)
     53 	}
     54 	defer f.Close()
     55 	if target == ".ico" {
     56 		if err := ico.NewEncoder(f).WithAlgorithm(algorithm).EncodeSizes(pixelSizes(images)); err != nil {
     57 			return fmt.Errorf("encoding ico: %w", err)
     58 		}
     59 		return nil
     60 	}
     61 	if err := icns.NewEncoder(f).WithAlgorithm(algorithm).EncodeSlots(images); err != nil {
     62 		return fmt.Errorf("encoding icns: %w", err)
     63 	}
     64 	return nil
     65 }
     66 
     67 // pixelSizes maps iconset slots onto the sizes an ico holds. Two slots can be
     68 // the same number of pixels, 16x16@2x and 32x32 both being 32, and the
     69 // drawing made without scaling is the one that belongs at that size.
     70 func pixelSizes(images map[icns.Slot]image.Image) map[uint]image.Image {
     71 	slots := make([]icns.Slot, 0, len(images))
     72 	for slot := range images {
     73 		slots = append(slots, slot)
     74 	}
     75 	// Highest scale first, so a plain drawing displaces a retina one.
     76 	slices.SortFunc(slots, func(a, b icns.Slot) int {
     77 		return cmp.Compare(b.Scale, a.Scale)
     78 	})
     79 	out := make(map[uint]image.Image, len(slots))
     80 	for _, slot := range slots {
     81 		out[slot.Pixels()] = images[slot]
     82 	}
     83 	return out
     84 }
     85 
     86 func readPNG(path string) (image.Image, error) {
     87 	f, err := os.Open(path)
     88 	if err != nil {
     89 		return nil, fmt.Errorf("opening iconset image: %w", err)
     90 	}
     91 	defer f.Close()
     92 	img, err := png.Decode(f)
     93 	if err != nil {
     94 		return nil, fmt.Errorf("decoding %s: %w", filepath.Base(path), err)
     95 	}
     96 	return img, nil
     97 }