1// Based on the implementation by @trashhalo at:
2// https://github.com/trashhalo/imgcat
3package image
4
5import (
6 "bytes"
7 "context"
8 "encoding/base64"
9 "image"
10 "image/png"
11 "io"
12 "net/http"
13 "os"
14 "strings"
15
16 tea "charm.land/bubbletea/v2"
17 "github.com/disintegration/imageorient"
18 "github.com/lucasb-eyer/go-colorful"
19 "github.com/muesli/termenv"
20 "github.com/nfnt/resize"
21 "github.com/srwiley/oksvg"
22 "github.com/srwiley/rasterx"
23)
24
25type loadMsg struct {
26 io.ReadCloser
27}
28
29func loadURL(url string) tea.Cmd {
30 var r io.ReadCloser
31 var err error
32
33 if strings.HasPrefix(url, "http") {
34 var resp *http.Request
35 resp, err = http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
36 r = resp.Body
37 } else {
38 r, err = os.Open(url)
39 }
40
41 if err != nil {
42 return func() tea.Msg {
43 return errMsg{err}
44 }
45 }
46
47 return load(r)
48}
49
50func load(r io.ReadCloser) tea.Cmd {
51 return func() tea.Msg {
52 return loadMsg{r}
53 }
54}
55
56func handleLoadMsg(m Model, msg loadMsg) (Model, tea.Cmd) {
57 defer msg.Close()
58
59 img, err := readerToImage(m.width, m.height, m.url, msg)
60 if err != nil {
61 return m, func() tea.Msg { return errMsg{err} }
62 }
63 m.image = img
64 return m, nil
65}
66
67func imageToString(width, height uint, img image.Image) (string, error) {
68 img = resize.Thumbnail(width, height*2-4, img, resize.Lanczos3)
69 b := img.Bounds()
70 w := b.Max.X
71 h := b.Max.Y
72 p := termenv.ColorProfile()
73 str := strings.Builder{}
74 for y := 0; y < h; y += 2 {
75 for x := w; x < int(width); x = x + 2 {
76 str.WriteString(" ")
77 }
78 for x := range w {
79 c1, _ := colorful.MakeColor(img.At(x, y))
80 color1 := p.Color(c1.Hex())
81 c2, _ := colorful.MakeColor(img.At(x, y+1))
82 color2 := p.Color(c2.Hex())
83 str.WriteString(termenv.String("▀").
84 Foreground(color1).
85 Background(color2).
86 String())
87 }
88 str.WriteString("\n")
89 }
90 return str.String(), nil
91}
92
93func readerToImage(width uint, height uint, url string, r io.Reader) (string, error) {
94 if strings.HasSuffix(strings.ToLower(url), ".svg") {
95 return svgToImage(width, height, r)
96 }
97
98 img, _, err := imageorient.Decode(r)
99 if err != nil {
100 return "", err
101 }
102
103 return imageToString(width, height, img)
104}
105
106func svgToImage(width uint, height uint, r io.Reader) (string, error) {
107 // Original author: https://stackoverflow.com/users/10826783/usual-human
108 // https://stackoverflow.com/questions/42993407/how-to-create-and-export-svg-to-png-jpeg-in-golang
109 // Adapted to use size from SVG, and to use temp file.
110
111 tmpPngFile, err := os.CreateTemp("", "img.*.png")
112 if err != nil {
113 return "", err
114 }
115 tmpPngPath := tmpPngFile.Name()
116 defer os.Remove(tmpPngPath)
117 defer tmpPngFile.Close()
118
119 // Rasterize the SVG:
120 icon, err := oksvg.ReadIconStream(r)
121 if err != nil {
122 return "", err
123 }
124 w := int(icon.ViewBox.W)
125 h := int(icon.ViewBox.H)
126 icon.SetTarget(0, 0, float64(w), float64(h))
127 rgba := image.NewRGBA(image.Rect(0, 0, w, h))
128 icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())), 1)
129 // Write rasterized image as PNG:
130 err = png.Encode(tmpPngFile, rgba)
131 if err != nil {
132 tmpPngFile.Close()
133 return "", err
134 }
135 tmpPngFile.Close()
136
137 rPng, err := os.Open(tmpPngPath)
138 if err != nil {
139 return "", err
140 }
141 defer rPng.Close()
142
143 img, _, err := imageorient.Decode(rPng)
144 if err != nil {
145 return "", err
146 }
147 return imageToString(width, height, img)
148}
149
150// ImageFromBase64 renders an image from base64-encoded data.
151func ImageFromBase64(width, height uint, data, mediaType string) (string, error) {
152 decoded, err := base64.StdEncoding.DecodeString(data)
153 if err != nil {
154 return "", err
155 }
156
157 r := bytes.NewReader(decoded)
158
159 if strings.Contains(mediaType, "svg") {
160 return svgToImage(width, height, r)
161 }
162
163 img, _, err := imageorient.Decode(r)
164 if err != nil {
165 return "", err
166 }
167
168 return imageToString(width, height, img)
169}