diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2026-05-30 17:15:44 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2026-05-30 17:15:44 -0700 |
| commit | 98407e4b89bbe60055ac7374bf047019b36f6c2b (patch) | |
| tree | f78e9ea2a7a6b2342fac2c8c5cdb164940814a40 /thumbnailer.go | |
| parent | ad5a27a5863bcdc8ccd29028ad11b4221dd60f10 (diff) | |
| download | imgutil-98407e4b89bbe60055ac7374bf047019b36f6c2b.tar.zst | |
Rename to ils
Diffstat (limited to 'thumbnailer.go')
| -rw-r--r-- | thumbnailer.go | 284 |
1 files changed, 0 insertions, 284 deletions
diff --git a/thumbnailer.go b/thumbnailer.go deleted file mode 100644 index 55a8eb2..0000000 --- a/thumbnailer.go +++ /dev/null @@ -1,284 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "image" - _ "image/gif" - _ "image/jpeg" - _ "image/png" - "io" - "os" - "path/filepath" - "slices" - "strings" - - runewidth "github.com/mattn/go-runewidth" - _ "golang.org/x/image/bmp" - "golang.org/x/image/draw" - _ "golang.org/x/image/tiff" - _ "golang.org/x/image/webp" - "golang.org/x/sys/unix" - "roseh.moe/pkg/sixel" -) - -var ( - x = flag.Int("x", 15, "set image width in columns") - m = flagPrintMode(flag.CommandLine, "m", modeSixel, "one of 'block', 'block24', or 'sixel'") -) - -type printMode int - -const ( - modeInvalid printMode = iota - modeBlock - modeBlock24 - modeSixel -) - -type printModeValue printMode - -func (m *printModeValue) String() string { - switch printMode(*m) { - case modeBlock: - return "block" - case modeBlock24: - return "block24" - case modeSixel: - return "sixel" - default: - return "invalid" - } -} - -func (m *printModeValue) Set(s string) error { - var mode printMode - switch s { - case "block": - mode = modeBlock - case "block24": - mode = modeBlock24 - case "sixel": - mode = modeSixel - default: - return fmt.Errorf("bad mode type %q, should be one of 'block', 'block24', or 'sixel'", s) - } - *m = printModeValue(mode) - return nil -} - -func flagPrintMode(fs *flag.FlagSet, name string, value printMode, usage string) *printMode { - fs.Var((*printModeValue)(&value), name, usage) - return &value -} - -func isDir(filename string) bool { - stat, err := os.Stat(filename) - return err == nil && stat.IsDir() -} - -func isImage(filename string) bool { - f, err := os.Open(filename) - if err != nil { - return false - } - defer f.Close() - _, _, err = image.DecodeConfig(f) - return err == nil -} - -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) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - - img, _, err := image.Decode(f) - if err != nil { - return nil, fmt.Errorf("decode %q: %s", filename, err) - } - - return img, nil -} - -func printImg(w io.Writer, img image.Image) { - switch *m { - case modeBlock: - sixel.PrintXTerm16(w, img) - case modeBlock24: - sixel.PrintBlock(w, img) - case modeSixel: - sixel.Print(w, img, nil) - default: - panic("invalid mode") - } -} - -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 { - 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 _, 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, nil -} - -func thumbnailFiles(w io.Writer, imagePaths []string, width, imagesPerRow, cellX, cellY int) error { - imageWidth := width - padding := 1 - aspectRatio := pixelAspectRatio{1, 1} - switch *m { - case modeBlock: - aspectRatio = pixelAspectRatio{cellX, cellY} - case modeBlock24: - aspectRatio = pixelAspectRatio{2 * cellX, cellY} - case modeSixel: - imageWidth = width * cellX - padding = cellX - } - for row := range slices.Chunk(imagePaths, imagesPerRow) { - rowImage, err := compositeImageRow(row, imageWidth, padding, aspectRatio) - if err != nil { - return err - } - printImg(w, rowImage) - names := make([][]string, len(row)) - maxLen := 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, "") - } - } - } - fmt.Fprintln(w) - } - return nil -} - -func readDir(d string) ([]string, error) { - files, err := os.ReadDir(d) - if err != nil { - return nil, err - } - filenames := make([]string, len(files)) - for i, f := range files { - filenames[i] = filepath.Join(d, f.Name()) - } - return filenames, nil -} - -func thumbnailDir(w io.Writer, dir string, width, imagesPerRow, 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) -} - -func thumbnailer(args []string) error { - cols := *x - if *x <= 0 { - return fmt.Errorf("-x must be greater than 0") - } - ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) - if err != nil { - return fmt.Errorf("terminal size: %s", err) - } - termCols := int(ws.Col) - 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 { - dirs = []string{"."} - } else { - for _, arg := range args { - if isDir(arg) { - dirs = append(dirs, arg) - } else { - files = append(files, arg) - } - } - } - w := bufio.NewWriter(os.Stdout) - defer w.Flush() - if err := thumbnailFiles(w, files, cols, imagesPerRow, cellX, cellY); err != nil { - return err - } - if len(files) > 0 && len(dirs) > 0 { - fmt.Fprintln(w) - } - for i, d := range dirs { - if i > 0 { - fmt.Fprintln(w) - } - if len(dirs) > 1 { - fmt.Fprintf(w, "%s:\n", d) - } - if err := thumbnailDir(w, d, cols, imagesPerRow, cellX, cellY); err != nil { - return err - } - } - - return nil -} - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: thumbnailer [OPTION]... [FILE]...\n") - flag.PrintDefaults() - } - flag.Parse() - - if err := thumbnailer(flag.Args()); err != nil { - fmt.Fprintf(os.Stderr, "Failed: %s\n", err) - os.Exit(1) - } -} |
