load.go

  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}