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}