| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/base64" |
| 6 | "fmt" |
| 7 | "image" |
| 8 | "image/png" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "sync" |
| 13 | ) |
| 14 | |
| 15 | const maxExternalOpenerIconBytes = 768 << 10 |
| 16 | |
| 17 | var externalOpenerIconCache sync.Map |
| 18 | |
| 19 | func externalOpenerIconDataURL(spec externalOpenerSpec) string { |
| 20 | source := strings.TrimSpace(spec.IconSource) |
| 21 | if source == "" { |
| 22 | return "" |
| 23 | } |
| 24 | cacheKey := source |
| 25 | if info, err := os.Stat(source); err == nil { |
| 26 | cacheKey = fmt.Sprintf("%s:%d:%d", source, info.ModTime().UnixNano(), info.Size()) |
| 27 | } |
| 28 | if cached, ok := externalOpenerIconCache.Load(cacheKey); ok { |
| 29 | return cached.(string) |
| 30 | } |
| 31 | icon := platformExternalOpenerIconDataURL(spec) |
| 32 | if icon != "" { |
| 33 | externalOpenerIconCache.Store(cacheKey, icon) |
| 34 | } |
| 35 | return icon |
| 36 | } |
| 37 | |
| 38 | func externalOpenerIconFileDataURL(path string) string { |
| 39 | info, err := os.Stat(path) |
| 40 | if err != nil || info.IsDir() || info.Size() <= 0 || info.Size() > maxExternalOpenerIconBytes { |
| 41 | return "" |
| 42 | } |
| 43 | data, err := os.ReadFile(path) |
| 44 | if err != nil || len(data) == 0 || len(data) > maxExternalOpenerIconBytes { |
| 45 | return "" |
| 46 | } |
| 47 | ext := strings.ToLower(filepath.Ext(path)) |
| 48 | switch ext { |
| 49 | case ".png": |
| 50 | return externalOpenerPNGDataURL(data) |
| 51 | case ".jpg", ".jpeg": |
| 52 | return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(data) |
| 53 | case ".webp": |
| 54 | return "data:image/webp;base64," + base64.StdEncoding.EncodeToString(data) |
| 55 | case ".svg": |
| 56 | return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString(data) |
| 57 | default: |
| 58 | return "" |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | func externalOpenerPNGDataURL(data []byte) string { |
| 63 | if len(data) == 0 || len(data) > maxExternalOpenerIconBytes { |
| 64 | return "" |
| 65 | } |
| 66 | return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data) |
| 67 | } |
| 68 | |
| 69 | func externalOpenerPNGFromBGRAComposites(black, white []byte, width, height int) []byte { |
| 70 | if width <= 0 || height <= 0 || len(black) != width*height*4 || len(white) != len(black) { |
| 71 | return nil |
| 72 | } |
| 73 | clampByte := func(value int) uint8 { |
| 74 | if value < 0 { |
| 75 | return 0 |
| 76 | } |
| 77 | if value > 255 { |
| 78 | return 255 |
| 79 | } |
| 80 | return uint8(value) |
| 81 | } |
| 82 | imageData := image.NewNRGBA(image.Rect(0, 0, width, height)) |
| 83 | for source := 0; source < len(black); source += 4 { |
| 84 | blackB, blackG, blackR := int(black[source]), int(black[source+1]), int(black[source+2]) |
| 85 | whiteB, whiteG, whiteR := int(white[source]), int(white[source+1]), int(white[source+2]) |
| 86 | diff := func(light, dark int) int { |
| 87 | if light <= dark { |
| 88 | return 0 |
| 89 | } |
| 90 | return int(clampByte(light - dark)) |
| 91 | } |
| 92 | alpha := 255 - (diff(whiteR, blackR)+diff(whiteG, blackG)+diff(whiteB, blackB))/3 |
| 93 | destination := source |
| 94 | if alpha > 0 { |
| 95 | imageData.Pix[destination] = clampByte(blackR * 255 / alpha) |
| 96 | imageData.Pix[destination+1] = clampByte(blackG * 255 / alpha) |
| 97 | imageData.Pix[destination+2] = clampByte(blackB * 255 / alpha) |
| 98 | } |
| 99 | imageData.Pix[destination+3] = uint8(alpha) |
| 100 | } |
| 101 | var encoded bytes.Buffer |
| 102 | if err := png.Encode(&encoded, imageData); err != nil { |
| 103 | return nil |
| 104 | } |
| 105 | return encoded.Bytes() |
| 106 | } |
| 107 |