load.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. // Based on the implementation by @trashhalo at:
  2. // https://github.com/trashhalo/imgcat
  3. package image
  4. import (
  5. "context"
  6. "image"
  7. "image/png"
  8. "io"
  9. "net/http"
  10. "os"
  11. "strings"
  12. tea "github.com/charmbracelet/bubbletea/v2"
  13. "github.com/disintegration/imageorient"
  14. "github.com/lucasb-eyer/go-colorful"
  15. "github.com/muesli/termenv"
  16. "github.com/nfnt/resize"
  17. "github.com/srwiley/oksvg"
  18. "github.com/srwiley/rasterx"
  19. )
  20. type loadMsg struct {
  21. io.ReadCloser
  22. }
  23. func loadURL(url string) tea.Cmd {
  24. var r io.ReadCloser
  25. var err error
  26. if strings.HasPrefix(url, "http") {
  27. var resp *http.Request
  28. resp, err = http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
  29. r = resp.Body
  30. } else {
  31. r, err = os.Open(url)
  32. }
  33. if err != nil {
  34. return func() tea.Msg {
  35. return errMsg{err}
  36. }
  37. }
  38. return load(r)
  39. }
  40. func load(r io.ReadCloser) tea.Cmd {
  41. return func() tea.Msg {
  42. return loadMsg{r}
  43. }
  44. }
  45. func handleLoadMsg(m Model, msg loadMsg) (Model, tea.Cmd) {
  46. defer msg.Close()
  47. img, err := readerToImage(m.width, m.height, m.url, msg)
  48. if err != nil {
  49. return m, func() tea.Msg { return errMsg{err} }
  50. }
  51. m.image = img
  52. return m, nil
  53. }
  54. func imageToString(width, height uint, img image.Image) (string, error) {
  55. img = resize.Thumbnail(width, height*2-4, img, resize.Lanczos3)
  56. b := img.Bounds()
  57. w := b.Max.X
  58. h := b.Max.Y
  59. p := termenv.ColorProfile()
  60. str := strings.Builder{}
  61. for y := 0; y < h; y += 2 {
  62. for x := w; x < int(width); x = x + 2 {
  63. str.WriteString(" ")
  64. }
  65. for x := range w {
  66. c1, _ := colorful.MakeColor(img.At(x, y))
  67. color1 := p.Color(c1.Hex())
  68. c2, _ := colorful.MakeColor(img.At(x, y+1))
  69. color2 := p.Color(c2.Hex())
  70. str.WriteString(termenv.String("▀").
  71. Foreground(color1).
  72. Background(color2).
  73. String())
  74. }
  75. str.WriteString("\n")
  76. }
  77. return str.String(), nil
  78. }
  79. func readerToImage(width uint, height uint, url string, r io.Reader) (string, error) {
  80. if strings.HasSuffix(strings.ToLower(url), ".svg") {
  81. return svgToImage(width, height, r)
  82. }
  83. img, _, err := imageorient.Decode(r)
  84. if err != nil {
  85. return "", err
  86. }
  87. return imageToString(width, height, img)
  88. }
  89. func svgToImage(width uint, height uint, r io.Reader) (string, error) {
  90. // Original author: https://stackoverflow.com/users/10826783/usual-human
  91. // https://stackoverflow.com/questions/42993407/how-to-create-and-export-svg-to-png-jpeg-in-golang
  92. // Adapted to use size from SVG, and to use temp file.
  93. tmpPngFile, err := os.CreateTemp("", "img.*.png")
  94. if err != nil {
  95. return "", err
  96. }
  97. tmpPngPath := tmpPngFile.Name()
  98. defer os.Remove(tmpPngPath)
  99. defer tmpPngFile.Close()
  100. // Rasterize the SVG:
  101. icon, err := oksvg.ReadIconStream(r)
  102. if err != nil {
  103. return "", err
  104. }
  105. w := int(icon.ViewBox.W)
  106. h := int(icon.ViewBox.H)
  107. icon.SetTarget(0, 0, float64(w), float64(h))
  108. rgba := image.NewRGBA(image.Rect(0, 0, w, h))
  109. icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())), 1)
  110. // Write rasterized image as PNG:
  111. err = png.Encode(tmpPngFile, rgba)
  112. if err != nil {
  113. tmpPngFile.Close()
  114. return "", err
  115. }
  116. tmpPngFile.Close()
  117. rPng, err := os.Open(tmpPngPath)
  118. if err != nil {
  119. return "", err
  120. }
  121. defer rPng.Close()
  122. img, _, err := imageorient.Decode(rPng)
  123. if err != nil {
  124. return "", err
  125. }
  126. return imageToString(width, height, img)
  127. }