aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-06-09 20:58:45 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-06-09 20:58:45 -0700
commit0cf646b8ae96774aba513dcde99e7ffc11c0e7f9 (patch)
treeda046cd421ab19f1ed6b5a03466e56c3e77a8eef
parent98407e4b89bbe60055ac7374bf047019b36f6c2b (diff)
downloadimgutil-0cf646b8ae96774aba513dcde99e7ffc11c0e7f9.tar.zst
Improve layout
-rw-r--r--ils.go130
1 files changed, 92 insertions, 38 deletions
diff --git a/ils.go b/ils.go
index 55a8eb2..05ece82 100644
--- a/ils.go
+++ b/ils.go
@@ -12,7 +12,6 @@ import (
"os"
"path/filepath"
"slices"
- "strings"
runewidth "github.com/mattn/go-runewidth"
_ "golang.org/x/image/bmp"
@@ -95,7 +94,7 @@ func imageDimensions(filename string) (width, height int, err error) {
}
defer f.Close()
config, _, err := image.DecodeConfig(f)
- return config.Width, config.Height, nil
+ return config.Width, config.Height, err
}
func load(filename string) (image.Image, error) {
@@ -130,17 +129,22 @@ type pixelAspectRatio struct {
x, y int
}
-func compositeImageRow(imagePaths []string, width, padding int, aspectRatio pixelAspectRatio) (image.Image, error) {
- resultWidth := width*len(imagePaths) + padding*(len(imagePaths)-1)
- maxHeight := 0
- for _, filename := range imagePaths {
+func compositeImageRow(imagePaths []string, width, rowWidth, cellX int, aspectRatio pixelAspectRatio) (image.Image, error) {
+ var logicalWidth, maxHeight int
+ for i, filename := range imagePaths {
+ if i > 0 {
+ logicalWidth += 2 * cellX
+ }
+ fileNameWidth := cellX * runewidth.StringWidth(filepath.Base(filename))
+ logicalWidth += max(width, fileNameWidth)
+
imageWidth, imageHeight, err := imageDimensions(filename)
if err != nil {
return nil, err
}
maxHeight = max(maxHeight, imageHeight*width/(imageWidth*aspectRatio.y/aspectRatio.x))
}
- bounds := image.Rect(0, 0, resultWidth, maxHeight)
+ bounds := image.Rect(0, 0, rowWidth, maxHeight)
result := image.NewRGBA(bounds)
offset := 0
for _, filename := range imagePaths {
@@ -149,50 +153,101 @@ func compositeImageRow(imagePaths []string, width, padding int, aspectRatio pixe
return nil, err
}
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)
+ imageWidth := max(width, cellX*runewidth.StringWidth(filepath.Base(filename)))
+ start := (offset*rowWidth + logicalWidth - 1) / logicalWidth
+ end := ((offset+imageWidth)*rowWidth + logicalWidth - 1) / logicalWidth
+ offsetStart := start + (end-start-width)/2
+ dstBounds := image.Rect(offsetStart, (maxHeight-height)/2, offsetStart+width, (maxHeight-height)/2+height)
draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil)
- offset += width + padding
+ offset += imageWidth + 2*cellX
}
return result, nil
}
-func thumbnailFiles(w io.Writer, imagePaths []string, width, imagesPerRow, cellX, cellY int) error {
- imageWidth := width
- padding := 1
+func splitRows(imagePaths []string, width, rowWidth int) [][]string {
+ var rows [][]string
+ for len(imagePaths) > 0 {
+ row := imagePaths[:0]
+ thisRowWidth := 0
+ for ; len(row) < len(imagePaths); row = row[:len(row)+1] {
+ nextW := max(width, runewidth.StringWidth(filepath.Base(imagePaths[len(row)])))
+ if len(row) > 0 {
+ nextW += 2
+ }
+ if thisRowWidth > 0 && thisRowWidth+nextW > rowWidth {
+ break
+ }
+ thisRowWidth += nextW
+ }
+ imagePaths = imagePaths[len(row):]
+ rows = append(rows, row)
+ }
+ return rows
+}
+
+func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, cellY int) error {
+ padding := 2
aspectRatio := pixelAspectRatio{1, 1}
switch *m {
case modeBlock:
aspectRatio = pixelAspectRatio{cellX, cellY}
+ cellX = 1
case modeBlock24:
aspectRatio = pixelAspectRatio{2 * cellX, cellY}
+ cellX = 1
case modeSixel:
- imageWidth = width * cellX
- padding = cellX
+ padding *= cellX
}
- for row := range slices.Chunk(imagePaths, imagesPerRow) {
- rowImage, err := compositeImageRow(row, imageWidth, padding, aspectRatio)
- if err != nil {
+ rows := splitRows(imagePaths, width, rowWidth)
+ rowImages := make([]chan image.Image, len(rows))
+ for i := range rowImages {
+ rowImages[i] = make(chan image.Image, 1)
+ }
+ errCh := make(chan error, 1)
+ for i, row := range rows {
+ go func() {
+ img, err := compositeImageRow(row, width*cellX, rowWidth*cellX, cellX, aspectRatio)
+ if err != nil {
+ select {
+ case errCh <- err:
+ default:
+ }
+ return
+ }
+ rowImages[i] <- img
+ }()
+ }
+ for i, row := range rows {
+ thisRowWidth := 0
+ for j, filename := range row {
+ if j > 0 {
+ thisRowWidth += 2
+ }
+ thisRowWidth += max(width, runewidth.StringWidth(filepath.Base(filename)))
+ }
+ var rowImage image.Image
+ select {
+ case err := <-errCh:
return err
+ case rowImage = <-rowImages[i]:
}
printImg(w, rowImage)
- names := make([][]string, len(row))
- maxLen := 0
+ fmt.Fprintln(w)
+ offset := 0
+ realOffset := 0
for i, filename := range row {
- names[i] = strings.Split(runewidth.Wrap(filepath.Base(filename), width), "\n")
- maxLen = max(maxLen, len(names[i]))
- }
- for i := 0; i < maxLen; i++ {
- fmt.Fprintln(w)
- for imgIdx, name := range names {
- if imgIdx > 0 {
- fmt.Fprint(w, " ")
- }
- if i < len(name) {
- fmt.Fprintf(w, "%s", runewidth.FillRight(name[i], width))
- } else {
- fmt.Fprintf(w, "%*s", width, "")
- }
+ if i > 0 {
+ offset += 2
}
+ start := (offset*rowWidth + thisRowWidth - 1) / thisRowWidth
+ fmt.Fprintf(w, "%*s", start-realOffset, "")
+ realOffset = start
+ fileNameWidth := runewidth.StringWidth(filepath.Base(filename))
+ offset += max(width, fileNameWidth)
+ end := (offset*rowWidth + thisRowWidth - 1) / thisRowWidth
+ padding := (end - start - fileNameWidth) / 2
+ fmt.Fprintf(w, "%*s%s", padding, "", filepath.Base(filename))
+ realOffset += padding + fileNameWidth
}
fmt.Fprintln(w)
}
@@ -211,13 +266,13 @@ func readDir(d string) ([]string, error) {
return filenames, nil
}
-func thumbnailDir(w io.Writer, dir string, width, imagesPerRow, cellX, cellY int) error {
+func thumbnailDir(w io.Writer, dir string, width, rowWidth, cellX, cellY int) error {
files, err := readDir(dir)
if err != nil {
return err
}
files = slices.DeleteFunc(files, func(filename string) bool { return !isImage(filename) })
- return thumbnailFiles(w, files, width, imagesPerRow, cellX, cellY)
+ return thumbnailFiles(w, files, width, rowWidth, cellX, cellY)
}
func thumbnailer(args []string) error {
@@ -233,7 +288,6 @@ func thumbnailer(args []string) error {
cellX, cellY := int(ws.Xpixel)/int(ws.Col), int(ws.Ypixel)/int(ws.Row)
cols = min(cols, termCols)
- imagesPerRow := (termCols + 1) / (cols + 1)
var files, dirs []string
if len(args) == 0 {
@@ -249,7 +303,7 @@ func thumbnailer(args []string) error {
}
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
- if err := thumbnailFiles(w, files, cols, imagesPerRow, cellX, cellY); err != nil {
+ if err := thumbnailFiles(w, files, cols, termCols, cellX, cellY); err != nil {
return err
}
if len(files) > 0 && len(dirs) > 0 {
@@ -262,7 +316,7 @@ func thumbnailer(args []string) error {
if len(dirs) > 1 {
fmt.Fprintf(w, "%s:\n", d)
}
- if err := thumbnailDir(w, d, cols, imagesPerRow, cellX, cellY); err != nil {
+ if err := thumbnailDir(w, d, cols, termCols, cellX, cellY); err != nil {
return err
}
}