commit e4ff63acb36960f8f936895b4bf76510161fca62
parent de819650ba126bd03004da4e787f4c22ca464116
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Mon, 12 Feb 2018 15:11:51 +0100
Merged in testing branch.
Diffstat:
| M | cmd/icnsify/doc.go | | | 2 | +- |
| M | cmd/icnsify/main.go | | | 11 | +++++------ |
| M | doc.go | | | 17 | +++++++++-------- |
| A | error.go | | | 22 | ++++++++++++++++++++++ |
| M | icns.go | | | 111 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- |
| A | icns_test.go | | | 312 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | icon.go | | | 71 | ----------------------------------------------------------------------- |
| D | iconset.go | | | 67 | ------------------------------------------------------------------- |
| M | interpolation.go | | | 20 | -------------------- |
| A | readme.md | | | 61 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | writer.go | | | 146 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
11 files changed, 639 insertions(+), 201 deletions(-)
diff --git a/cmd/icnsify/doc.go b/cmd/icnsify/doc.go
@@ -13,7 +13,7 @@ func usage() {
You can also pipe to stdin and from stdout.
The pipes will be detected automatically, and both --input and --output will be ignored.
- cat icon.png | icns | cat > icon.icns
+ cat icon.png | icnsify | cat > icon.icns
`)
}
diff --git a/cmd/icnsify/main.go b/cmd/icnsify/main.go
@@ -50,15 +50,14 @@ func main() {
defer outputf.Close()
output = outputf
}
- img, _, err := image.Decode(input)
+ img, format, err := image.Decode(input)
if err != nil {
log.Fatalf("decoding image: %v", err)
}
- if err := icns.EncodeWithInterpolationFunction(
- output,
- img,
- algorithm,
- ); err != nil {
+ enc := icns.NewEncoder(output).
+ WithAlgorithm(algorithm).
+ WithFormat(format)
+ if err := enc.Encode(img); err != nil {
log.Fatalf("encoding icns: %v", err)
}
}
diff --git a/doc.go b/doc.go
@@ -6,14 +6,15 @@
// 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.
//
+// With this library you can use pure Go to create icns files from any source
+// image, given that you can decode it into an `image.Image`, without any
+// heavyweight dependencies or subprocessing required. You can also use this
+// library to create icns files on windows and linux.
+//
+// A small CLI app `icnsify` is provided to allow you to create icns files
+// using this library from the command line. It supports piping, which is
+// something `iconutil` does not do, making it substantially easier to wrap.
+//
// Note: All icons within the icns are sized for high dpi retina screens, using
// the appropriate icns OSTypes.
-//
-// Todo(jackmordaunt):
-// - Write tests (only manual testing has been done)
-// - How to test the correctness of a file format?
-// - Create Decoder (.icns -> image.Image)
-// - Register decoder to image.Decode in init func
-// - Encode based on input image format (jpg -> jpg, png -> png) to avoid
-// lossy conversions
package icns
diff --git a/error.go b/error.go
@@ -0,0 +1,22 @@
+package icns
+
+import (
+ "fmt"
+ "image"
+)
+
+// ErrImageTooSmall is returned when the image is too small to process.
+type ErrImageTooSmall struct {
+ need int
+ image image.Image
+}
+
+func (err ErrImageTooSmall) Error() string {
+ b := err.image.Bounds().Max
+ format := "image is too small: %dx%d, need at least %dx%d"
+ return fmt.Sprintf(format, b.X, b.Y, err.need, err.need)
+}
+
+func panicf(format string, values ...interface{}) {
+ panic(fmt.Sprintf(format, values...))
+}
diff --git a/icns.go b/icns.go
@@ -1,38 +1,85 @@
package icns
import (
+ "errors"
"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.
-// Uses nearest neighbor as interpolation algorithm.
-func Encode(wr io.Writer, img image.Image) error {
- iconset, err := NewIconSet(img, NearestNeighbor)
+// Encoder encodes ICNS files from a source image.
+type Encoder struct {
+ Wr io.Writer
+ Algorithm InterpolationFunction
+ Format string
+}
+
+// NewEncoder initialises an encoder.
+func NewEncoder(wr io.Writer) *Encoder {
+ return &Encoder{
+ Wr: wr,
+ }
+}
+
+// WithAlgorithm applies the interpolation function used to resize the image.
+func (enc *Encoder) WithAlgorithm(a InterpolationFunction) *Encoder {
+ enc.Algorithm = a
+ return enc
+}
+
+// WithFormat applies the image format identifier used during registration by
+// image/png and image/jpeg packages.
+func (enc *Encoder) WithFormat(format string) *Encoder {
+ enc.Format = format
+ return enc
+}
+
+// Encode icns with the given configuration.
+func (enc *Encoder) Encode(img image.Image) error {
+ if enc.Wr == nil {
+ return errors.New("cannot write to nil writer")
+ }
+ if img == nil {
+ return errors.New("cannot encode nil image")
+ }
+ iconset, err := NewIconSet(img, enc.Algorithm, enc.Format)
if err != nil {
return err
}
- if _, err := iconset.WriteTo(wr); err != nil {
+ if _, err := iconset.WriteTo(enc.Wr); err != nil {
return err
}
return nil
}
+// 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.
+// Uses nearest neighbor as interpolation algorithm.
+func Encode(wr io.Writer, img image.Image) error {
+ return NewEncoder(wr).Encode(img)
+}
+
// NewIconSet uses the source image to create an IconSet.
// If width != height, the image will be resized using the largest side without
// preserving the aspect ratio.
-func NewIconSet(img image.Image, interp InterpolationFunction) (*IconSet, error) {
+func NewIconSet(img image.Image, interp InterpolationFunction, format string) (*IconSet, error) {
biggest := findNearestSize(img)
+ if biggest == 0 {
+ return nil, ErrImageTooSmall{image: img, need: 16}
+ }
icons := []*Icon{}
for _, size := range sizesFrom(biggest) {
+ t, ok := getType(size)
+ if !ok {
+ continue
+ }
iconImg := resize.Resize(size, size, img, interp)
icon := &Icon{
- Type: getType(size),
- Image: iconImg,
+ Type: t,
+ Image: iconImg,
+ format: format,
}
icons = append(icons, icon)
}
@@ -63,7 +110,7 @@ var sizes = []uint{
func findNearestSize(img image.Image) uint {
size := biggestSide(img)
for _, s := range sizes {
- if size > s {
+ if size >= s {
return s
}
}
@@ -81,31 +128,39 @@ func biggestSide(img image.Image) uint {
return size
}
-// returns a slice containing the sizes less than and including max.
+// sizesFrom returns a slice containing the sizes less than and including max.
func sizesFrom(max uint) []uint {
for ii, s := range sizes {
- if s == max {
+ 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",
-}
+// OsType is a 4 character identifier used to differentiate icon types.
+type OsType string
-// 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")
+// getType returns the type for the given icon size (in px).
+// The boolean indicates whether the type exists.
+func getType(size uint) (OsType, bool) {
+ // 'types' is a map of the OSTypes we care about.
+ // All dimensions are considered as retina.
+ //
+ // Todo(jackmordaunt): Not sure if only retina is sufficient. Should all
+ // types be handled? `iconutil` uses file names to determine whether a
+ // retina image is desired eg: "icon_256x256@2.png", without such a hint
+ // how can you disambiguate 256x256 standard vs 256x256 retina?
+ // Do we even need to consider standard sizes over retina?
+ // For now, just retina types are considered.
+ types := map[uint]OsType{
+ 1024: "ic10",
+ 512: "ic14",
+ 256: "ic13",
+ 128: "ic07",
+ 64: "ic12",
+ 32: "ic11",
}
- return v
+ v, ok := types[size]
+ return v, ok
}
diff --git a/icns_test.go b/icns_test.go
@@ -0,0 +1,312 @@
+package icns
+
+import (
+ "bytes"
+ "image"
+ "image/jpeg"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "testing"
+
+ "github.com/jackmordaunt/deep"
+ "github.com/pkg/errors"
+)
+
+// TestEncode tests for input validation, sanity checks and errors.
+// The validity of the encoding is not tested here.
+// Super large images are not tested because the resizing takes too
+// long for unit testing.
+func TestEncode(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ desc string
+ wr io.Writer
+ img image.Image
+
+ wantErr bool
+ }{
+ {
+ "nil image",
+ ioutil.Discard,
+ nil,
+ true,
+ },
+ {
+ "nil writer",
+ nil,
+ rect(0, 0, 50, 50),
+ true,
+ },
+ {
+ "valid sqaure",
+ ioutil.Discard,
+ rect(0, 0, 50, 50),
+ false,
+ },
+ {
+ "valid non-square",
+ ioutil.Discard,
+ rect(0, 0, 10, 50),
+ false,
+ },
+ {
+ "valid non-square, weird dimensions",
+ ioutil.Discard,
+ rect(0, 0, 17, 77),
+ false,
+ },
+ {
+ "invalid zero img",
+ ioutil.Discard,
+ rect(0, 0, 0, 0),
+ true,
+ },
+ {
+ "invalid small img",
+ ioutil.Discard,
+ rect(0, 0, 1, 1),
+ true,
+ },
+ {
+ "valid square not at origin point",
+ ioutil.Discard,
+ rect(10, 10, 50, 50),
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(st *testing.T) {
+ err := Encode(tt.wr, tt.img)
+ if !tt.wantErr && err != nil {
+ st.Fatalf("unexpected error: %v", err)
+ }
+ })
+ }
+}
+
+func TestSizesFromMax(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ desc string
+ from uint
+ want []uint
+ }{
+ {
+ "small",
+ 100,
+ []uint{64, 32},
+ },
+ {
+ "large",
+ 99999,
+ []uint{1024, 512, 256, 64, 32},
+ },
+ {
+ "smallest",
+ 0,
+ []uint{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(st *testing.T) {
+ got := sizesFrom(tt.from)
+ if !deep.EqualContents(got, tt.want) {
+ st.Errorf("want=%d, got=%d", tt.want, got)
+ }
+ })
+ }
+}
+
+func TestBiggestSide(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ desc string
+ img image.Image
+ want uint
+ }{
+ {
+ "equal",
+ rect(0, 0, 100, 100),
+ 100,
+ },
+ {
+ "right larger",
+ rect(0, 0, 50, 100),
+ 100,
+ },
+ {
+ "left larger",
+ rect(0, 0, 100, 50),
+ 100,
+ },
+ {
+ "off by one",
+ rect(0, 0, 100, 99),
+ 100,
+ },
+ {
+ "empty",
+ rect(0, 0, 0, 0),
+ 0,
+ },
+ {
+ "left empty",
+ rect(0, 0, 0, 10),
+ 10,
+ },
+ {
+ "right empty",
+ rect(0, 0, 10, 0),
+ 10,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(st *testing.T) {
+ got := biggestSide(tt.img)
+ if got != tt.want {
+ st.Errorf("want=%d, got=%d", tt.want, got)
+ }
+ })
+ }
+}
+
+func TestFindNearestSize(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ desc string
+ img image.Image
+ want uint
+ }{
+ {
+ "small",
+ rect(0, 0, 100, 100),
+ 64,
+ },
+ {
+ "very large",
+ rect(0, 0, 123456789, 123456789),
+ 1024,
+ },
+ {
+ "too small",
+ rect(0, 0, 16, 16),
+ 0,
+ },
+ {
+ "off by one",
+ rect(0, 0, 33, 33),
+ 32,
+ },
+ {
+ "exact",
+ rect(0, 0, 256, 256),
+ 256,
+ },
+ {
+ "exact",
+ rect(0, 0, 1024, 1024),
+ 1024,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(st *testing.T) {
+ got := findNearestSize(tt.img)
+ if tt.want != got {
+ st.Errorf("want=%d, got=%d", tt.want, got)
+ }
+ })
+ }
+}
+
+func TestEncodeImage(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ desc string
+
+ img image.Image
+ format string
+
+ want string
+ }{
+ {
+ "png - png",
+ _decode(_png(rect(0, 0, 50, 50))),
+ "png",
+ "png",
+ },
+ {
+ "default png - png",
+ _decode(_png(rect(0, 0, 50, 50))),
+ "",
+ "png",
+ },
+ {
+ "jpg - jpg",
+ _decode(_jpg(rect(0, 0, 50, 50))),
+ "jpeg",
+ "jpeg",
+ },
+ {
+ "default jpg - png",
+ _decode(_jpg(rect(0, 0, 50, 50))),
+ "",
+ "png",
+ },
+ {
+ "invalid format identifier",
+ _decode(_jpg(rect(0, 0, 50, 50))),
+ "asdf",
+ "png",
+ },
+ {
+ "not actually a jpeg: forces a conversion",
+ _decode(_png(rect(0, 0, 50, 50))),
+ "jpeg",
+ "jpeg",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(st *testing.T) {
+ data, err := encodeImage(tt.img, tt.format)
+ if err != nil {
+ st.Fatalf("encoding image: %v", err)
+ }
+ _, f, err := image.Decode(bytes.NewBuffer(data))
+ if err != nil {
+ st.Fatalf("decoding iamge: %v", err)
+ }
+ if f != tt.want {
+ st.Fatalf("formats: want=%s, got=%s", tt.want, f)
+ }
+ })
+ }
+}
+
+func rect(x0, y0, x1, y1 int) image.Image {
+ return image.Rect(x0, y0, x1, y1)
+}
+
+func _png(img image.Image) io.Reader {
+ buf := bytes.NewBuffer(nil)
+ if err := png.Encode(buf, img); err != nil {
+ panic(errors.Wrapf(err, "encoding png"))
+ }
+ return buf
+}
+
+func _jpg(img image.Image) io.Reader {
+ buf := bytes.NewBuffer(nil)
+ if err := jpeg.Encode(buf, img, nil); err != nil {
+ panic(errors.Wrapf(err, "encoding jpeg"))
+ }
+ return buf
+}
+
+func _decode(r io.Reader) image.Image {
+ m, _, err := image.Decode(r)
+ if err != nil {
+ panic(errors.Wrapf(err, "decoding image"))
+ }
+ return m
+}
diff --git a/icon.go b/icon.go
@@ -1,71 +0,0 @@
-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
@@ -1,67 +0,0 @@
-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
-}
diff --git a/interpolation.go b/interpolation.go
@@ -1,9 +1,6 @@
package icns
import (
- "image"
- "io"
-
"github.com/nfnt/resize"
)
@@ -25,20 +22,3 @@ const (
// Lanczos interpolation (a=3)
Lanczos3
)
-
-// EncodeWithInterpolationFunction uses the given interpolation function resize
-// the image before writing out to wr.
-func EncodeWithInterpolationFunction(
- wr io.Writer,
- img image.Image,
- interp InterpolationFunction,
-) error {
- iconset, err := NewIconSet(img, interp)
- if err != nil {
- return err
- }
- if _, err := iconset.WriteTo(wr); err != nil {
- return err
- }
- return nil
-}
diff --git a/readme.md b/readme.md
@@ -0,0 +1,61 @@
+# icns
+
+Easily convert `.jpg` and `.png` to `.icns` with the command line tool `icnsify`, or use the library to convert from any `image.Image` to `.icns`.
+
+`go get github.com/jackmordaunt/icns`
+
+`icns` files allow for high resolution icons to make your apps look sexy. The most common ways to generate icns files are:
+
+1. `iconutil`, which is a Mac native cli utility.
+2. `ImageMagick` which adds a large dependency to your project for such a simple use case.
+
+With this library you can use pure Go to create `icns` files from any source image, given that you can decode it into an `image.Image`, without any heavyweight dependencies or subprocessing required. You can also use it to create icns files on windows and linux (thanks Go).
+
+A small CLI app `icnsify` is provided allowing you to create icns files using this library from the command line. It supports piping, which is something `iconutil` does not do, making it substantially easier to wrap or chuck into a shell pipeline.
+
+Note: All icons within the `icns` are sized for high dpi retina screens, using the appropriate `icns` OSTypes.
+
+## Command Line
+
+Pipe it
+
+`cat icon.png | icnsify | cat > icon.icns`
+
+Standard
+
+`icnsify -i icon.png -o icon.icns`
+
+## Library Usage
+
+```go
+func main() {
+ pngf, err := os.Open("path/to/icon.png")
+ if err != nil {
+ log.Fatalf("opening source image: %v", err)
+ }
+ defer pngf.Close()
+ srcImg, _, err := image.Decode(pngf)
+ if err != nil {
+ log.Fatalf("decoding source image: %v", err)
+ }
+ dest, err := os.Open("path/to/icon.icns")
+ if err != nil {
+ log.Fatalf("opening destination file: %v", err)
+ }
+ defer dest.Close()
+ if err := icns.Encode(dest, srcImg); err != nil {
+ log.Fatalf("encoding icns: %v", err)
+ }
+}
+```
+
+## Roadmap
+
+* [x] Encoder: `image.Image -> .icns`
+* [x] Command Line Interface
+ * [x] Encoding
+ * [x] Pipe support
+ * [ ] Decoding
+* [ ] Implement Decoder: `.icns -> image.Image`
+* [ ] Symmetric test: `decode(encode(img)) == img`
+* [x] Encode based on input image format (jpg -> jpg, png -> png) to avoid lossy conversions
diff --git a/writer.go b/writer.go
@@ -0,0 +1,146 @@
+package icns
+
+import (
+ "bytes"
+ "image"
+ "image/jpeg"
+ "image/png"
+ "io"
+)
+
+// Icon encodes an icns icon.
+type Icon struct {
+ Type OsType
+ Image image.Image
+ format string
+
+ 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.encodeImage(); 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) encodeImage() error {
+ if len(i.data) > 0 {
+ return nil
+ }
+ data, err := encodeImage(i.Image, i.format)
+ if err != nil {
+ return err
+ }
+ i.data = data
+ return nil
+}
+
+func encodeImage(img image.Image, format string) ([]byte, error) {
+ buf := bytes.NewBuffer(nil)
+ switch format {
+ case "jpeg":
+ if err := jpeg.Encode(buf, img, nil); err != nil {
+ return nil, err
+ }
+ default:
+ if err := png.Encode(buf, img); err != nil {
+ return nil, err
+ }
+ }
+ return buf.Bytes(), 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
+}
+
+// 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
+}