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 "io/ioutil"
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.Response
33 resp, err = http.Get(url)
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 if m.cancelAnimation != nil {
56 m.cancelAnimation()
57 }
58
59 // blank out image so it says "loading..."
60 m.image = ""
61
62 return handleLoadMsgStatic(m, msg)
63}
64
65func handleLoadMsgStatic(m Model, msg loadMsg) (Model, tea.Cmd) {
66 defer msg.Close()
67
68 img, err := readerToimage(m.width, m.height, m.url, msg)
69 if err != nil {
70 return m, func() tea.Msg { return errMsg{err} }
71 }
72 m.image = img
73 return m, nil
74}
75
76func imageToString(width, height uint, url string, img image.Image) (string, error) {
77 img = resize.Thumbnail(width, height*2-4, img, resize.Lanczos3)
78 b := img.Bounds()
79 w := b.Max.X
80 h := b.Max.Y
81 p := termenv.ColorProfile()
82 str := strings.Builder{}
83 for y := 0; y < h; y += 2 {
84 for x := w; x < int(width); x = x + 2 {
85 str.WriteString(" ")
86 }
87 for x := 0; x < w; x++ {
88 c1, _ := colorful.MakeColor(img.At(x, y))
89 color1 := p.Color(c1.Hex())
90 c2, _ := colorful.MakeColor(img.At(x, y+1))
91 color2 := p.Color(c2.Hex())
92 str.WriteString(termenv.String("▀").
93 Foreground(color1).
94 Background(color2).
95 String())
96 }
97 str.WriteString("\n")
98 }
99 return str.String(), nil
100}
101
102func readerToimage(width uint, height uint, url string, r io.Reader) (string, error) {
103 if strings.HasSuffix(strings.ToLower(url), ".svg") {
104 return svgToimage(width, height, url, r)
105 }
106
107 img, _, err := imageorient.Decode(r)
108 if err != nil {
109 return "", err
110 }
111
112 return imageToString(width, height, url, img)
113}
114
115func svgToimage(width uint, height uint, url string, r io.Reader) (string, error) {
116 // Original author: https://stackoverflow.com/users/10826783/usual-human
117 // https://stackoverflow.com/questions/42993407/how-to-create-and-export-svg-to-png-jpeg-in-golang
118 // Adapted to use size from SVG, and to use temp file.
119
120 tmpPngFile, err := ioutil.TempFile("", "imgcat.*.png")
121 if err != nil {
122 return "", err
123 }
124 tmpPngPath := tmpPngFile.Name()
125 defer os.Remove(tmpPngPath)
126 defer tmpPngFile.Close()
127
128 // Rasterize the SVG:
129 icon, err := oksvg.ReadIconStream(r)
130 if err != nil {
131 return "", err
132 }
133 w := int(icon.ViewBox.W)
134 h := int(icon.ViewBox.H)
135 icon.SetTarget(0, 0, float64(w), float64(h))
136 rgba := image.NewRGBA(image.Rect(0, 0, w, h))
137 icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())), 1)
138 // Write rasterized image as PNG:
139 err = png.Encode(tmpPngFile, rgba)
140 if err != nil {
141 tmpPngFile.Close()
142 return "", err
143 }
144 tmpPngFile.Close()
145
146 rPng, err := os.Open(tmpPngPath)
147 if err != nil {
148 return "", err
149 }
150 defer rPng.Close()
151
152 img, _, err := imageorient.Decode(rPng)
153 if err != nil {
154 return "", err
155 }
156 return imageToString(width, height, url, img)
157}