aboutsummaryrefslogtreecommitdiffstats
path: root/thumbnailer.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-05-30 11:47:22 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-05-30 11:47:22 -0700
commit9d550131dbff2b5004f18043eab55b5f33068609 (patch)
treeebecd8b8fcb24a6da7805c8a123c63d2497c031b /thumbnailer.go
parent7935762e84b09a75670b3a40fae75703368effb4 (diff)
downloadimgutil-9d550131dbff2b5004f18043eab55b5f33068609.tar.zst
Optimize memory usage
Diffstat (limited to 'thumbnailer.go')
-rw-r--r--thumbnailer.go129
1 files changed, 46 insertions, 83 deletions
diff --git a/thumbnailer.go b/thumbnailer.go
index 1236504..55a8eb2 100644
--- a/thumbnailer.go
+++ b/thumbnailer.go
@@ -2,7 +2,6 @@ package main
import (
"bufio"
- "errors"
"flag"
"fmt"
"image"
@@ -10,7 +9,6 @@ import (
_ "image/jpeg"
_ "image/png"
"io"
- "io/fs"
"os"
"path/filepath"
"slices"
@@ -75,51 +73,41 @@ func flagPrintMode(fs *flag.FlagSet, name string, value printMode, usage string)
return &value
}
-func isDir(filename string) (bool, error) {
+func isDir(filename string) bool {
stat, err := os.Stat(filename)
- if err != nil {
- return false, err
- }
- return stat.IsDir(), nil
+ return err == nil && stat.IsDir()
}
-func isLink(filename string) (bool, error) {
- stat, err := os.Lstat(filename)
+func isImage(filename string) bool {
+ f, err := os.Open(filename)
if err != nil {
- return false, err
+ return false
}
- return stat.Mode().Type() == fs.ModeSymlink, nil
-}
-
-var errNotAnImage = errors.New("not an image")
-
-type notAnImageError struct {
- error
+ defer f.Close()
+ _, _, err = image.DecodeConfig(f)
+ return err == nil
}
-func (e *notAnImageError) Is(err error) bool {
- return err == errNotAnImage
+func imageDimensions(filename string) (width, height int, err error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return 0, 0, err
+ }
+ defer f.Close()
+ config, _, err := image.DecodeConfig(f)
+ return config.Width, config.Height, nil
}
func load(filename string) (image.Image, error) {
- file := os.Stdin
- if filename != "-" {
- var err error
- file, err = os.Open(filename)
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- if isLink, _ := isLink(filename); isLink {
- return nil, &notAnImageError{err}
- }
- }
- return nil, err
- }
- defer file.Close()
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, err
}
+ defer f.Close()
- img, _, err := image.Decode(file)
+ img, _, err := image.Decode(f)
if err != nil {
- return nil, &notAnImageError{fmt.Errorf("decode %q: %s", filename, err)}
+ return nil, fmt.Errorf("decode %q: %s", filename, err)
}
return img, nil
@@ -142,30 +130,33 @@ type pixelAspectRatio struct {
x, y int
}
-func compositeImageRow(images []image.Image, width, padding int, aspectRatio pixelAspectRatio) image.Image {
- resultWidth := width*len(images) + padding*(len(images)-1)
+func compositeImageRow(imagePaths []string, width, padding int, aspectRatio pixelAspectRatio) (image.Image, error) {
+ resultWidth := width*len(imagePaths) + padding*(len(imagePaths)-1)
maxHeight := 0
- for _, img := range images {
- maxHeight = max(maxHeight, img.Bounds().Dy()*width/(img.Bounds().Dx()*aspectRatio.y/aspectRatio.x))
+ for _, filename := range imagePaths {
+ 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)
result := image.NewRGBA(bounds)
offset := 0
- for _, img := range images {
+ for _, filename := range imagePaths {
+ img, err := load(filename)
+ if err != nil {
+ 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)
draw.BiLinear.Scale(result, dstBounds, img, img.Bounds(), draw.Src, nil)
offset += width + padding
}
- return result
-}
-
-type img struct {
- i image.Image
- filename string
+ return result, nil
}
-func compositeImages(w io.Writer, images []img, width, imagesPerRow, cellX, cellY int) {
+func thumbnailFiles(w io.Writer, imagePaths []string, width, imagesPerRow, cellX, cellY int) error {
imageWidth := width
padding := 1
aspectRatio := pixelAspectRatio{1, 1}
@@ -178,17 +169,16 @@ func compositeImages(w io.Writer, images []img, width, imagesPerRow, cellX, cell
imageWidth = width * cellX
padding = cellX
}
- for row := range slices.Chunk(images, imagesPerRow) {
- images := make([]image.Image, len(row))
- for i, img := range row {
- images[i] = img.i
+ for row := range slices.Chunk(imagePaths, imagesPerRow) {
+ rowImage, err := compositeImageRow(row, imageWidth, padding, aspectRatio)
+ if err != nil {
+ return err
}
- rowImage := compositeImageRow(images, imageWidth, padding, aspectRatio)
printImg(w, rowImage)
names := make([][]string, len(row))
maxLen := 0
- for i, img := range row {
- names[i] = strings.Split(runewidth.Wrap(img.filename, width), "\n")
+ 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++ {
@@ -206,18 +196,6 @@ func compositeImages(w io.Writer, images []img, width, imagesPerRow, cellX, cell
}
fmt.Fprintln(w)
}
-}
-
-func thumbnailFiles(w io.Writer, args []string, width, imagesPerRow, cellX, cellY int) error {
- images := make([]img, len(args))
- for i, arg := range args {
- image, err := load(arg)
- if err != nil {
- return err
- }
- images[i] = img{image, filepath.Base(arg)}
- }
- compositeImages(w, images, width, imagesPerRow, cellX, cellY)
return nil
}
@@ -238,19 +216,8 @@ func thumbnailDir(w io.Writer, dir string, width, imagesPerRow, cellX, cellY int
if err != nil {
return err
}
- var images []img
- for _, file := range files {
- image, err := load(file)
- if err != nil {
- if errors.Is(err, errNotAnImage) {
- continue
- }
- return err
- }
- images = append(images, img{image, filepath.Base(file)})
- }
- compositeImages(w, images, width, imagesPerRow, cellX, cellY)
- return nil
+ files = slices.DeleteFunc(files, func(filename string) bool { return !isImage(filename) })
+ return thumbnailFiles(w, files, width, imagesPerRow, cellX, cellY)
}
func thumbnailer(args []string) error {
@@ -273,11 +240,7 @@ func thumbnailer(args []string) error {
dirs = []string{"."}
} else {
for _, arg := range args {
- isDir, err := isDir(arg)
- if err != nil {
- return err
- }
- if isDir {
+ if isDir(arg) {
dirs = append(dirs, arg)
} else {
files = append(files, arg)