icns

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

commit 2e6ce3d4e5d3f3e1a9dbfad30577517cdc3c3850
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Mon, 29 Jan 2018 19:07:54 +0100

[+] Initial.

Diffstat:
A.gitignore | 1+
Acmd/main.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adoc.go | 18++++++++++++++++++
Aicns.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aicon.go | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aiconset.go | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 333 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +*.DS_Store diff --git a/cmd/main.go b/cmd/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "image" + "log" + "os" + "path/filepath" + "strings" + + "github.com/jackmordaunt/icns" + "github.com/spf13/afero" + + "github.com/spf13/pflag" +) + +var fs = afero.NewOsFs() + +func main() { + var ( + input = pflag.StringP("input", "i", "", "Input image for conversion to icns (jpg|png)") + output = pflag.StringP("output", "o", "", "Output path, defaults to <path/to/image>.icns") + ) + pflag.Parse() + in, out := validate(*input, *output) + sourcef, err := fs.Open(in) + if err != nil { + log.Fatalf("opening source image: %v", err) + } + defer sourcef.Close() + img, _, err := image.Decode(sourcef) + if err != nil { + log.Fatalf("decoding image: %v", err) + } + outputf, err := fs.Create(out) + if err != nil { + log.Fatalf("creating icns file: %v", err) + } + defer outputf.Close() + if err := icns.Encode(outputf, img); err != nil { + log.Fatalf("encoding icns") + } +} + +func validate(inputPath, outputPath string) (string, string) { + if inputPath == "" { + pflag.Usage() + os.Exit(0) + } + if outputPath == "" { + outputPath = changeExtensionTo(inputPath, "icns") + } + if filepath.Ext(outputPath) == "" { + outputPath += ".icns" + } + return inputPath, outputPath +} + +func changeExtensionTo(path, ext string) string { + if !strings.HasPrefix(ext, ".") { + ext = "." + ext + } + return filepath.Base(path[:len(path)-len(filepath.Ext(path))] + ext) +} diff --git a/doc.go b/doc.go @@ -0,0 +1,18 @@ +// Package icns implements an encoder for Apple's `.icns` file format. +// Reference: "https://en.wikipedia.org/wiki/Apple_Icon_Image_format". +// +// icns files allow for high resolution icons to make your apps look sexy. +// The most common ways to generate icns files are 1. use `iconutil` which is +// a Mac native cli utility, or 2. use tools that wrap `ImageMagick` which adds +// a large dependency to your project for such a simple use case. +// +// Note: All icons within the icns are sized for high dpi retina screens. +// +// Todo(jackmordaunt): +// - Write tests (only manual testing has been done) +// - How to test the correctness of a file format? +// - Create Decoder (.icns -> image.Image) +// - Encode based on input image format (jpg -> jpg, png -> png) to avoid +// lossy conversions +// - Allow configuraton of resize algorithm +package icns diff --git a/icns.go b/icns.go @@ -0,0 +1,113 @@ +package icns + +import ( + "image" + "io" + + "github.com/nfnt/resize" +) + +// Encode writes img to wr in ICNS format. +// img is assumed to be a rectangle; non-square dimensions will be squared +// without preserving the aspect ratio. +// +// Note, Todo(jackmordaunt): The underlying image encoding used for the icons +// is png, resulting in a lossy conversion if the source image is jpg. +func Encode(wr io.Writer, img image.Image) error { + iconset, err := NewIconSet(img) + if err != nil { + return err + } + if _, err := iconset.WriteTo(wr); err != nil { + return err + } + return nil +} + +// NewIconSet uses the source image to create an IconSet. +// If width != height, the shortest side will be padded with transparent pixels +// to make the icon a square. +func NewIconSet(img image.Image) (*IconSet, error) { + biggest := findNearestSize(img) + icons := []*Icon{} + for _, size := range sizesFrom(biggest) { + iconImg := resize.Resize(size, size, img, resize.NearestNeighbor) + icon := &Icon{ + Type: getType(size), + Image: iconImg, + } + icons = append(icons, icon) + } + iconset := &IconSet{ + Icons: icons, + } + return iconset, nil +} + +// Big-endian. +// https://golang.org/src/image/png/writer.go +func writeUint32(b []uint8, u uint32) { + b[0] = uint8(u >> 24) + b[1] = uint8(u >> 16) + b[2] = uint8(u >> 8) + b[3] = uint8(u >> 0) +} + +var sizes = []uint{ + 1024, + 512, + 256, + 64, + 32, +} + +// findNearestSize finds the biggest icon size we can use for this image. +func findNearestSize(img image.Image) uint { + size := biggestSide(img) + for _, s := range sizes { + if size > s { + return s + } + } + return 0 +} + +func biggestSide(img image.Image) uint { + var size uint + b := img.Bounds() + w, h := uint(b.Max.X), uint(b.Max.Y) + size = w + if h > size { + size = h + } + return size +} + +// returns a slice containing the sizes less than and including max. +func sizesFrom(max uint) []uint { + for ii, s := range sizes { + if s == max { + return sizes[ii:len(sizes)] + } + } + return nil +} + +var types = map[uint]OsType{ + 1024: "ic10", + 512: "ic14", + 256: "ic13", + 128: "ic07", + 64: "ic12", + 32: "ic11", +} + +// should this return error, panic or return a default (but probably incorrect) +// format? For now, failing explicitly is preferable to failing silently. +func getType(size uint) OsType { + v, ok := types[size] + if !ok { + panic("could not select the correct icon type") + } + return v +} diff --git a/icon.go b/icon.go @@ -0,0 +1,71 @@ +package icns + +import ( + "bytes" + "image" + "image/png" + "io" +) + +// OsType is a 4 character identifier used to differentiate icon types. +type OsType string + +// Icon encodes an icns icon. +type Icon struct { + Type OsType + Image image.Image + + header [8]byte + headerSet bool + data []byte +} + +// WriteTo encodes the icon into wr. +func (i *Icon) WriteTo(wr io.Writer) (int64, error) { + var written int64 + if err := i.encodePng(); err != nil { + return written, err + } + size, err := i.writeHeader(wr) + written += size + if err != nil { + return written, err + } + size, err = i.writeData(wr) + written += size + if err != nil { + return written, err + } + return written, nil +} + +func (i *Icon) encodePng() error { + if len(i.data) > 0 { + return nil + } + buf := bytes.NewBuffer(nil) + if err := png.Encode(buf, i.Image); err != nil { + return err + } + i.data = buf.Bytes() + return nil +} + +func (i *Icon) writeHeader(wr io.Writer) (int64, error) { + if !i.headerSet { + defer func() { i.headerSet = true }() + i.header[0] = i.Type[0] + i.header[1] = i.Type[1] + i.header[2] = i.Type[2] + i.header[3] = i.Type[3] + length := uint32(len(i.data) + 8) + writeUint32(i.header[4:8], length) + } + written, err := wr.Write(i.header[:8]) + return int64(written), err +} + +func (i *Icon) writeData(wr io.Writer) (int64, error) { + written, err := wr.Write(i.data) + return int64(written), err +} diff --git a/iconset.go b/iconset.go @@ -0,0 +1,67 @@ +package icns + +import ( + "bytes" + "io" +) + +// IconSet encodes a set of icons into an ICNS file. +type IconSet struct { + Icons []*Icon + + header [8]byte + headerSet bool + data []byte +} + +// WriteTo writes the ICNS file to wr. +func (s *IconSet) WriteTo(wr io.Writer) (int64, error) { + var written int64 + if err := s.encodeIcons(); err != nil { + return written, err + } + size, err := s.writeHeader(wr) + written += size + if err != nil { + return written, err + } + size, err = s.writeData(wr) + written += size + if err != nil { + return written, err + } + return written, nil +} + +func (s *IconSet) encodeIcons() error { + if len(s.data) > 0 { + return nil + } + buf := bytes.NewBuffer(nil) + for _, icon := range s.Icons { + if _, err := icon.WriteTo(buf); err != nil { + return err + } + } + s.data = buf.Bytes() + return nil +} + +func (s *IconSet) writeHeader(wr io.Writer) (int64, error) { + if !s.headerSet { + defer func() { s.headerSet = true }() + s.header[0] = 'i' + s.header[1] = 'c' + s.header[2] = 'n' + s.header[3] = 's' + length := uint32(len(s.data) + 8) + writeUint32(s.header[4:8], length) + } + written, err := wr.Write(s.header[:8]) + return int64(written), err +} + +func (s *IconSet) writeData(wr io.Writer) (int64, error) { + written, err := wr.Write(s.data) + return int64(written), err +}