aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-05-28 16:00:06 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-05-28 16:00:06 -0700
commit8dc1a723880da87a079bc1a7fe4236aebaf3ff3a (patch)
treec7f7421fe09a9b58badda340ab1c5fcf78c5265a
parentc9228ffa367405907265fc96228e7bf046ff7f11 (diff)
downloadimgutil-8dc1a723880da87a079bc1a7fe4236aebaf3ff3a.tar.zst
Factor out the rendering to a library
-rw-r--r--go.mod3
-rw-r--r--go.sum2
-rw-r--r--thumbnailer.go159
3 files changed, 13 insertions, 151 deletions
diff --git a/go.mod b/go.mod
index ecbd1d1..0855626 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,12 @@
module roseh.moe/cmd/thumbnailer
-go 1.25.0
+go 1.26.3
require (
github.com/mattn/go-runewidth v0.0.23
golang.org/x/image v0.28.0
golang.org/x/sys v0.45.0
+ roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153
)
require github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
diff --git a/go.sum b/go.sum
index d525430..5b539f1 100644
--- a/go.sum
+++ b/go.sum
@@ -6,3 +6,5 @@ golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153 h1:Jq7Yv9BeWwBPA5BY238i0SpXDqKudsqvlTZUvbi6ZMg=
+roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153/go.mod h1:DX/c+l3VYm+yXoACY+HXl2gLV5o5iAG6gkHeDddTSrQ=
diff --git a/thumbnailer.go b/thumbnailer.go
index c0d7c9e..cd14316 100644
--- a/thumbnailer.go
+++ b/thumbnailer.go
@@ -6,8 +6,6 @@ import (
"flag"
"fmt"
"image"
- "image/color"
- "image/color/palette"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
@@ -24,6 +22,7 @@ import (
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"golang.org/x/sys/unix"
+ "roseh.moe/pkg/sixel"
)
var (
@@ -126,140 +125,14 @@ func load(filename string) (image.Image, error) {
return img, nil
}
-type xtermColor int8
-
-func (c xtermColor) RGBA() (r, g, b, a uint32) {
- var col color.RGBA
- switch c {
- case 0:
- col = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
- case 1:
- col = color.RGBA{R: 0xcd, G: 0x00, B: 0x00, A: 0xff}
- case 2:
- col = color.RGBA{R: 0x00, G: 0xcd, B: 0x00, A: 0xff}
- case 3:
- col = color.RGBA{R: 0xcd, G: 0xcd, B: 0x00, A: 0xff}
- case 4:
- col = color.RGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff}
- case 5:
- col = color.RGBA{R: 0xcd, G: 0x00, B: 0xcd, A: 0xff}
- case 6:
- col = color.RGBA{R: 0x00, G: 0xcd, B: 0xcd, A: 0xff}
- case 7:
- col = color.RGBA{R: 0xe5, G: 0xe5, B: 0xe5, A: 0xff}
- case 60:
- col = color.RGBA{R: 0x7f, G: 0x7f, B: 0x7f, A: 0xff}
- case 61:
- col = color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
- case 62:
- col = color.RGBA{R: 0x00, G: 0xff, B: 0x00, A: 0xff}
- case 63:
- col = color.RGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}
- case 64:
- col = color.RGBA{R: 0x5c, G: 0x5c, B: 0xff, A: 0xff}
- case 65:
- col = color.RGBA{R: 0xff, G: 0x00, B: 0xff, A: 0xff}
- case 66:
- col = color.RGBA{R: 0x00, G: 0xff, B: 0xff, A: 0xff}
- case 67:
- col = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
- default:
- panic("not an xterm color")
- }
- return col.RGBA()
-}
-
-var xtermPalette = color.Palette{xtermColor(0), xtermColor(1), xtermColor(2), xtermColor(3), xtermColor(4), xtermColor(5), xtermColor(6), xtermColor(7), xtermColor(60), xtermColor(61), xtermColor(62), xtermColor(63), xtermColor(64), xtermColor(65), xtermColor(66), xtermColor(67)}
-
-func printImgXterm(w io.Writer, img image.Image) {
- for y := 0; y < img.Bounds().Dy(); y += 2 {
- if y > 0 {
- fmt.Fprintln(w)
- }
- for x := 0; x < img.Bounds().Dx(); x++ {
- hi := img.At(x, y)
- if hi == color.Transparent {
- fmt.Fprint(w, "\033[49m ")
- } else {
- lo := 9
- if y+1 < img.Bounds().Dy() && img.At(x, y+1) != color.Transparent {
- lo = int(img.At(x, y+1).(xtermColor))
- }
- fmt.Fprintf(w, "\033[%dm\033[%dm▀", 30+hi.(xtermColor), 40+lo)
- }
- }
- fmt.Fprint(w, "\033[39m\033[49m")
- }
-}
-
-func isFullyTransparent(c color.Color) bool {
- _, _, _, a := c.RGBA()
- return a == 0
-}
-
-func printImgBlock24(w io.Writer, img image.Image) {
- for y := 0; y < img.Bounds().Dy(); y += 2 {
- if y > 0 {
- fmt.Fprintln(w)
- }
- for x := 0; x < img.Bounds().Dx(); x++ {
- hi := img.At(x, y)
- if isFullyTransparent(hi) {
- fmt.Fprint(w, "\033[49m ")
- } else {
- if y+1 < img.Bounds().Dy() && !isFullyTransparent(img.At(x, y+1)) {
- r, g, b, _ := img.At(x, y+1).RGBA()
- fmt.Fprintf(w, "\033[48;2;%d;%d;%dm", r>>8, g>>8, b>>8)
- } else {
- fmt.Fprint(w, "\033[49m")
- }
- r, g, b, _ := hi.RGBA()
- fmt.Fprintf(w, "\033[38;2;%d;%d;%dm▀", r>>8, g>>8, b>>8)
- }
- }
- fmt.Fprint(w, "\033[39m\033[49m")
- }
-}
-
-func scale100(c uint32) int {
- return int(c) * 100 / 0xffff
-}
-
-func printImgSixel(w io.Writer, img image.PalettedImage) {
- fmt.Fprint(w, "\033P7;1q")
- for i, c := range img.ColorModel().(color.Palette)[1:] {
- r, g, b, _ := c.RGBA()
- fmt.Fprintf(w, "#%d;2;%d;%d;%d", i, scale100(r), scale100(g), scale100(b))
- }
- for y := 0; y < img.Bounds().Dy(); y++ {
- if y > 0 {
- if y%6 == 0 {
- fmt.Fprint(w, "-")
- } else {
- fmt.Fprint(w, "$")
- }
- }
- for x := 0; x < img.Bounds().Dx(); x++ {
- c := img.ColorIndexAt(x, y)
- if c == 0 {
- fmt.Fprint(w, "?")
- } else {
- char := '?' + 1<<(y%6)
- fmt.Fprintf(w, "#%d%c", c-1, char)
- }
- }
- }
- fmt.Fprint(w, "\033\\")
-}
-
func printImg(w io.Writer, img image.Image) {
switch *m {
case modeBlock:
- printImgXterm(w, img)
+ sixel.PrintXTerm16(w, img)
case modeBlock24:
- printImgBlock24(w, img)
+ sixel.PrintBlock(w, img)
case modeSixel:
- printImgSixel(w, img.(image.PalettedImage))
+ sixel.Print(w, img)
default:
panic("invalid mode")
}
@@ -269,30 +142,19 @@ type pixelAspectRatio struct {
x, y int
}
-func compositeImageRow(images []image.Image, width, padding int, aspectRatio pixelAspectRatio, palette color.Palette) image.Image {
+func compositeImageRow(images []image.Image, width, padding int, aspectRatio pixelAspectRatio) image.Image {
resultWidth := width*len(images) + padding*(len(images)-1)
maxHeight := 0
for _, img := range images {
maxHeight = max(maxHeight, img.Bounds().Dy()*width/(img.Bounds().Dx()*aspectRatio.y/aspectRatio.x))
}
bounds := image.Rect(0, 0, resultWidth, maxHeight)
- var result draw.Image
- if len(palette) > 0 {
- result = image.NewPaletted(bounds, append([]color.Color{color.Transparent}, palette...))
- } else {
- result = image.NewRGBA(bounds)
- }
+ result := image.NewRGBA(bounds)
offset := 0
for _, img := range images {
height := img.Bounds().Dy() * width / (img.Bounds().Dx() * aspectRatio.y / aspectRatio.x)
dstBounds := image.Rect(offset, (maxHeight-height)/2, offset+width, (maxHeight-height)/2+height)
- if len(palette) > 0 {
- tmp := image.NewRGBA(image.Rect(0, 0, width, height))
- draw.BiLinear.Scale(tmp, tmp.Bounds(), img, img.Bounds(), draw.Src, nil)
- draw.FloydSteinberg.Draw(result, dstBounds, tmp, image.Point{})
- } else {
- draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil)
- }
+ draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil)
offset += width + padding
}
return result
@@ -306,25 +168,22 @@ type img struct {
func compositeImages(w io.Writer, images []img, width, imagesPerRow, cellX, cellY int) {
imageWidth := width
padding := 1
- var colorPalette color.Palette
aspectRatio := pixelAspectRatio{1, 1}
switch *m {
case modeBlock:
- colorPalette = xtermPalette
- aspectRatio = pixelAspectRatio{2 * cellX, cellY}
+ aspectRatio = pixelAspectRatio{cellX, cellY}
case modeBlock24:
aspectRatio = pixelAspectRatio{2 * cellX, cellY}
case modeSixel:
imageWidth = width * cellX
padding = cellX
- colorPalette = palette.WebSafe
}
for row := range slices.Chunk(images, imagesPerRow) {
images := make([]image.Image, len(row))
for i, img := range row {
images[i] = img.i
}
- rowImage := compositeImageRow(images, imageWidth, padding, aspectRatio, colorPalette)
+ rowImage := compositeImageRow(images, imageWidth, padding, aspectRatio)
printImg(w, rowImage)
names := make([][]string, len(row))
maxLen := 0