aboutsummaryrefslogtreecommitdiffstats
path: root/thumbnailer.go
diff options
context:
space:
mode:
Diffstat (limited to 'thumbnailer.go')
-rw-r--r--thumbnailer.go284
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)
- }
-}