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