//go:build js package main import ( "archive/zip" "bytes" "encoding/base64" "fmt" "image" "image/png" "strings" "syscall/js" "golang.org/x/image/draw" ) type density struct { dir string size int } var densities = []density{ {"mipmap-mdpi", 48}, {"mipmap-hdpi", 72}, {"mipmap-xhdpi", 96}, {"mipmap-xxhdpi", 144}, {"mipmap-xxxhdpi", 192}, } func main() { js.Global().Set("prepareZip", js.FuncOf(prepareZip)) <-make(chan struct{}) } func prepareZip(this js.Value, args []js.Value) interface{} { // args[0] is an array of {name: string, data: string (base64 PNG data)} files := args[0] length := files.Length() if length == 0 { js.Global().Call("alert", "No files selected.") return nil } var buf bytes.Buffer zw := zip.NewWriter(&buf) for i := 0; i < length; i++ { file := files.Index(i) name := file.Get("name").String() dataURL := file.Get("data").String() // Strip data URL prefix (data:image/png;base64,) commaIdx := strings.Index(dataURL, ",") if commaIdx < 0 { setStatus(fmt.Sprintf("Invalid data for %s", name)) return nil } b64Data := dataURL[commaIdx+1:] imgData, err := base64.StdEncoding.DecodeString(b64Data) if err != nil { setStatus(fmt.Sprintf("Failed to decode %s: %v", name, err)) return nil } src, err := png.Decode(bytes.NewReader(imgData)) if err != nil { setStatus(fmt.Sprintf("Failed to decode PNG %s: %v", name, err)) return nil } // Strip extension from name for the base name baseName := name if strings.HasSuffix(strings.ToLower(baseName), ".png") { baseName = baseName[:len(baseName)-4] } for _, d := range densities { resized := resizeImage(src, d.size) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, resized); err != nil { setStatus(fmt.Sprintf("Failed to encode %s/%s.png: %v", d.dir, baseName, err)) return nil } path := fmt.Sprintf("%s/%s.png", d.dir, baseName) w, err := zw.Create(path) if err != nil { setStatus(fmt.Sprintf("Failed to create zip entry %s: %v", path, err)) return nil } if _, err := w.Write(pngBuf.Bytes()); err != nil { setStatus(fmt.Sprintf("Failed to write zip entry %s: %v", path, err)) return nil } } } if err := zw.Close(); err != nil { setStatus(fmt.Sprintf("Failed to finalize zip: %v", err)) return nil } // Convert zip bytes to a JS Uint8Array and trigger download zipBytes := buf.Bytes() jsArray := js.Global().Get("Uint8Array").New(len(zipBytes)) js.CopyBytesToJS(jsArray, zipBytes) // Create Blob and download blobParts := js.Global().Get("Array").New() blobParts.Call("push", jsArray) blobOpts := js.Global().Get("Object").New() blobOpts.Set("type", "application/zip") blob := js.Global().Get("Blob").New(blobParts, blobOpts) url := js.Global().Get("URL").Call("createObjectURL", blob) anchor := js.Global().Get("document").Call("createElement", "a") anchor.Set("href", url) anchor.Set("download", "android-icons.zip") js.Global().Get("document").Get("body").Call("appendChild", anchor) anchor.Call("click") anchor.Call("remove") js.Global().Get("URL").Call("revokeObjectURL", url) setStatus(fmt.Sprintf("Done! Prepared icons for %d image(s).", length)) return nil } func resizeImage(src image.Image, size int) image.Image { dst := image.NewNRGBA(image.Rect(0, 0, size, size)) draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) return dst } func setStatus(msg string) { querySelector("#status").Set("innerText", msg) } func querySelector(query string) js.Value { return js.Global().Get("document").Call("querySelector", query) }