commit 2e6ce3d4e5d3f3e1a9dbfad30577517cdc3c3850
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Mon, 29 Jan 2018 19:07:54 +0100
[+] Initial.
Diffstat:
| A | .gitignore | | | 1 | + |
| A | cmd/main.go | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | doc.go | | | 18 | ++++++++++++++++++ |
| A | icns.go | | | 113 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | icon.go | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | iconset.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
+}