load.go

  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}