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 }