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