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 }