107 lines
No EOL
2.3 KiB
Go
107 lines
No EOL
2.3 KiB
Go
package tui
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/mattn/go-sixel"
|
|
)
|
|
|
|
func FetchAndProcessThumbnail(url string) (string, error) {
|
|
if url == "" {
|
|
return "", fmt.Errorf("empty URL")
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to fetch image: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("failed to fetch image: status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read image data: %w", err)
|
|
}
|
|
|
|
img, _, err := image.Decode(bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to decode image: %w", err)
|
|
}
|
|
|
|
// Consistent thumbnail size
|
|
width := 40
|
|
height := 15
|
|
|
|
img = imaging.Resize(img, width*2, height*2, imaging.Lanczos)
|
|
|
|
// Try iTerm2 protocol first
|
|
if isITerm() {
|
|
return getITermImageSequence(img), nil
|
|
}
|
|
|
|
// Try Sixel next
|
|
var buf bytes.Buffer
|
|
enc := sixel.NewEncoder(&buf)
|
|
err = enc.Encode(img)
|
|
if err == nil {
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// Fall back to ASCII art
|
|
return imageToASCII(img), nil
|
|
}
|
|
|
|
func imageToASCII(img image.Image) string {
|
|
bounds := img.Bounds()
|
|
width := 40
|
|
height := 15
|
|
chars := []string{"█", "▓", "▒", "░", " "}
|
|
|
|
var ascii strings.Builder
|
|
stepX := float64(bounds.Dx()) / float64(width)
|
|
stepY := float64(bounds.Dy()) / float64(height)
|
|
|
|
for y := 0; y < height; y++ {
|
|
for x := 0; x < width; x++ {
|
|
px := int(float64(x) * stepX)
|
|
py := int(float64(y) * stepY)
|
|
c := color.GrayModel.Convert(img.At(px+bounds.Min.X, py+bounds.Min.Y)).(color.Gray)
|
|
index := int(c.Y) * (len(chars) - 1) / 255
|
|
ascii.WriteString(chars[index])
|
|
}
|
|
ascii.WriteString("\n")
|
|
}
|
|
|
|
return ascii.String()
|
|
}
|
|
|
|
func getITermImageSequence(img image.Image) string {
|
|
var buf bytes.Buffer
|
|
imaging.Encode(&buf, img, imaging.PNG)
|
|
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
|
|
|
return fmt.Sprintf("\033]1337;File=inline=1;width=40ch;height=15;preserveAspectRatio=1:%s\a", encoded)
|
|
}
|
|
|
|
func isITerm() bool {
|
|
return os.Getenv("TERM_PROGRAM") == "iTerm.app"
|
|
} |