icns

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

main.go (4746B)


      1 //go:build js && wasm
      2 
      3 // Command wasm exposes the icon encoders and decoders to a web page as the
      4 // global "icns", so a conversion runs in the browser rather than on a
      5 // server. See web/ for a page that uses it.
      6 package main
      7 
      8 import (
      9 	"bytes"
     10 	"errors"
     11 	"fmt"
     12 	"image"
     13 	"image/jpeg"
     14 	"image/png"
     15 	"strings"
     16 	"syscall/js"
     17 
     18 	"github.com/jackmordaunt/icns/v4"
     19 	"github.com/jackmordaunt/icns/v4/ico"
     20 )
     21 
     22 func main() {
     23 	api := js.Global().Get("Object").New()
     24 	api.Set("convert", js.FuncOf(convert))
     25 	api.Set("inspect", js.FuncOf(inspect))
     26 	api.Set("validate", js.FuncOf(validate))
     27 	js.Global().Set("icns", api)
     28 	// The page calls into this, so the program has to stay resident.
     29 	select {}
     30 }
     31 
     32 // convert takes the bytes of an image and the name of a format, and returns
     33 // the same artwork written in that format.
     34 func convert(_ js.Value, args []js.Value) (out any) {
     35 	defer func() {
     36 		if r := recover(); r != nil {
     37 			out = failure(fmt.Errorf("converting: %v", r))
     38 		}
     39 	}()
     40 	if len(args) < 2 {
     41 		return failure(errors.New("convert wants an image and a format"))
     42 	}
     43 	img, _, err := image.Decode(bytes.NewReader(toGo(args[0])))
     44 	if err != nil {
     45 		return failure(fmt.Errorf("reading the image: %w", err))
     46 	}
     47 	buf := bytes.NewBuffer(nil)
     48 	switch format := strings.ToLower(args[1].String()); format {
     49 	case "icns":
     50 		err = icns.Encode(buf, img)
     51 	case "ico":
     52 		err = ico.Encode(buf, img)
     53 	case "png":
     54 		err = png.Encode(buf, img)
     55 	case "jpg", "jpeg":
     56 		err = jpeg.Encode(buf, img, &jpeg.Options{Quality: 100})
     57 	default:
     58 		err = fmt.Errorf("cannot write %s", format)
     59 	}
     60 	if err != nil {
     61 		return failure(err)
     62 	}
     63 	return success(buf.Bytes())
     64 }
     65 
     66 // inspect lists the icons an icns or ico file holds, largest first. Anything
     67 // else holds one image and lists as empty.
     68 func inspect(_ js.Value, args []js.Value) (out any) {
     69 	defer func() {
     70 		if r := recover(); r != nil {
     71 			out = failure(fmt.Errorf("inspecting: %v", r))
     72 		}
     73 	}()
     74 	if len(args) < 1 {
     75 		return failure(errors.New("inspect wants an image"))
     76 	}
     77 	var (
     78 		src   = toGo(args[0])
     79 		icons []string
     80 	)
     81 	if d, err := icns.NewDecoder(bytes.NewReader(src)); err == nil {
     82 		for _, icon := range d.Icons() {
     83 			icons = append(icons, icon.String())
     84 		}
     85 	} else if d, err := ico.NewDecoder(bytes.NewReader(src)); err == nil {
     86 		for _, icon := range d.Icons() {
     87 			icons = append(icons, icon.String())
     88 		}
     89 	}
     90 	list := js.Global().Get("Array").New(len(icons))
     91 	for i, icon := range icons {
     92 		list.SetIndex(i, icon)
     93 	}
     94 	result := js.Global().Get("Object").New()
     95 	result.Set("ok", true)
     96 	result.Set("icons", list)
     97 	return result
     98 }
     99 
    100 // validate reports what the platform that owns the format will make of an
    101 // icon file, as an array of findings ordered by severity.
    102 func validate(_ js.Value, args []js.Value) (out any) {
    103 	defer func() {
    104 		if r := recover(); r != nil {
    105 			out = failure(fmt.Errorf("validating: %v", r))
    106 		}
    107 	}()
    108 	if len(args) < 1 {
    109 		return failure(errors.New("validate wants an icon file"))
    110 	}
    111 	type finding struct{ severity, icon, message string }
    112 	var (
    113 		src      = toGo(args[0])
    114 		findings []finding
    115 	)
    116 	switch {
    117 	case bytes.HasPrefix(src, []byte("icns")):
    118 		problems, err := icns.Validate(bytes.NewReader(src))
    119 		if err != nil {
    120 			return failure(err)
    121 		}
    122 		for _, p := range problems {
    123 			findings = append(findings, finding{p.Severity.String(), p.Icon, p.Message})
    124 		}
    125 	case bytes.HasPrefix(src, []byte{0x00, 0x00, 0x01, 0x00}):
    126 		problems, err := ico.Validate(bytes.NewReader(src))
    127 		if err != nil {
    128 			return failure(err)
    129 		}
    130 		for _, p := range problems {
    131 			findings = append(findings, finding{p.Severity.String(), p.Icon, p.Message})
    132 		}
    133 	default:
    134 		return failure(errors.New("not an icns or ico file"))
    135 	}
    136 	list := js.Global().Get("Array").New(len(findings))
    137 	for i, f := range findings {
    138 		item := js.Global().Get("Object").New()
    139 		item.Set("severity", f.severity)
    140 		item.Set("icon", f.icon)
    141 		item.Set("message", f.message)
    142 		list.SetIndex(i, item)
    143 	}
    144 	result := js.Global().Get("Object").New()
    145 	result.Set("ok", true)
    146 	result.Set("problems", list)
    147 	return result
    148 }
    149 
    150 // toGo copies a Uint8Array into Go memory, which the wasm boundary requires.
    151 func toGo(v js.Value) []byte {
    152 	out := make([]byte, v.Get("length").Int())
    153 	js.CopyBytesToGo(out, v)
    154 	return out
    155 }
    156 
    157 // success and failure build the object every entry point returns, so a
    158 // failure reaches the page as a value rather than as a panic crossing the
    159 // boundary.
    160 func success(data []byte) js.Value {
    161 	buf := js.Global().Get("Uint8Array").New(len(data))
    162 	js.CopyBytesToJS(buf, data)
    163 	out := js.Global().Get("Object").New()
    164 	out.Set("ok", true)
    165 	out.Set("data", buf)
    166 	return out
    167 }
    168 
    169 func failure(err error) js.Value {
    170 	out := js.Global().Get("Object").New()
    171 	out.Set("ok", false)
    172 	out.Set("error", err.Error())
    173 	return out
    174 }