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	"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}