aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-06-12 20:59:52 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-06-12 20:59:52 -0700
commit3391135078aecd774e44b4fafc1873d88e240530 (patch)
treedc694def10f56326e9bbfc29b9c9146d801d154a
parent084bd5469beba8fc230bd46f8b951f10a3e9d076 (diff)
downloadimgutil-3391135078aecd774e44b4fafc1873d88e240530.tar.zst
Add a -l flag like real ls
-rw-r--r--ils.go137
1 files changed, 110 insertions, 27 deletions
diff --git a/ils.go b/ils.go
index fe6bc1a..f2301dd 100644
--- a/ils.go
+++ b/ils.go
@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"image"
+ "image/color"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
@@ -89,14 +90,57 @@ func isImage(filename string) bool {
return err == nil
}
-func imageDimensions(filename string) (width, height int, err error) {
+type imageMetadata struct {
+ format, colorModel string
+ width, height int
+}
+
+func (m *imageMetadata) String() string {
+ return fmt.Sprintf("%dx%d %s %s", m.width, m.height, m.format, m.colorModel)
+}
+
+func colorModelName(m color.Model) string {
+ switch m {
+ case color.RGBAModel:
+ return "rgba"
+ case color.RGBA64Model:
+ return "rgba 64 bit"
+ case color.NRGBAModel:
+ return "rgba not alpha premultiplied"
+ case color.NRGBA64Model:
+ return "rgba 64 bit not alpha premultiplied"
+ case color.AlphaModel:
+ return "alpha"
+ case color.Alpha16Model:
+ return "alpha 16 bit"
+ case color.GrayModel:
+ return "grayscale"
+ case color.Gray16Model:
+ return "grayscale 16 bit"
+ case color.CMYKModel:
+ return "cmyk"
+ case color.NYCbCrAModel:
+ return "Y'CbCr with alpha"
+ case color.YCbCrModel:
+ return "Y'CbCr"
+ }
+ if palette, ok := m.(color.Palette); ok {
+ return fmt.Sprintf("palettized %d colors", len(palette))
+ }
+ return fmt.Sprintf("unknown %v", m)
+}
+
+func readMetadata(filename string) (imageMetadata, error) {
f, err := os.Open(filename)
if err != nil {
- return 0, 0, err
+ return imageMetadata{}, err
}
defer f.Close()
- config, _, err := image.DecodeConfig(f)
- return config.Width, config.Height, err
+ config, format, err := image.DecodeConfig(f)
+ if err != nil {
+ return imageMetadata{}, err
+ }
+ return imageMetadata{format: format, colorModel: colorModelName(config.ColorModel), width: config.Width, height: config.Height}, nil
}
func load(filename string) (image.Image, error) {
@@ -144,10 +188,13 @@ type pixelAspectRatio struct {
x, y int
}
-func layoutRow(imagePaths []string, width, rowWidth, cellX int) []int {
+func layoutRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int) []int {
filenameWidths := make([]int, len(imagePaths))
for i, path := range imagePaths {
filenameWidths[i] = cellX * runewidth.StringWidth(escapeString(filepath.Base(path)))
+ if *l {
+ filenameWidths[i] = max(filenameWidths[i], cellX*runewidth.StringWidth(metadata[i].String()))
+ }
}
var logicalWidth int
for i, filenameWidth := range filenameWidths {
@@ -166,15 +213,21 @@ func layoutRow(imagePaths []string, width, rowWidth, cellX int) []int {
return layout
}
-func compositeImageRow(imagePaths []string, layout []int, width, rowWidth, cellX int, aspectRatio pixelAspectRatio) (image.Image, error) {
- var maxHeight int
- for _, filename := range imagePaths {
- imageWidth, imageHeight, err := imageDimensions(filename)
- if err != nil {
- return nil, err
+func compositeImageRow(imagePaths []string, metadata []imageMetadata, width, rowWidth, cellX int, aspectRatio pixelAspectRatio) (image.Image, error) {
+ if len(metadata) == 0 {
+ metadata = make([]imageMetadata, len(imagePaths))
+ for i, filename := range imagePaths {
+ var err error
+ if metadata[i], err = readMetadata(filename); err != nil {
+ return nil, err
+ }
}
- maxHeight = max(maxHeight, divRound(imageHeight*width*aspectRatio.x, imageWidth*aspectRatio.y))
}
+ var maxHeight int
+ for _, fileMetadata := range metadata {
+ maxHeight = max(maxHeight, divRound(fileMetadata.height*width*aspectRatio.x, fileMetadata.width*aspectRatio.y))
+ }
+ layout := layoutRow(imagePaths, metadata, width, rowWidth, cellX)
bounds := image.Rect(0, 0, rowWidth, maxHeight)
result := image.NewRGBA(bounds)
for i, filename := range imagePaths {
@@ -195,13 +248,16 @@ func compositeImageRow(imagePaths []string, layout []int, width, rowWidth, cellX
return result, nil
}
-func splitRows(imagePaths []string, width, rowWidth int) [][]string {
+func splitRows(imagePaths []string, metadata []imageMetadata, 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(escapeString(filepath.Base(imagePaths[len(row)]))))
+ if *l {
+ nextW = max(nextW, len(metadata[len(row)].String()))
+ }
if len(row) > 0 {
nextW += 2
}
@@ -211,6 +267,9 @@ func splitRows(imagePaths []string, width, rowWidth int) [][]string {
thisRowWidth += nextW
}
imagePaths = imagePaths[len(row):]
+ if *l {
+ metadata = metadata[len(row):]
+ }
rows = append(rows, row)
}
return rows
@@ -229,10 +288,23 @@ func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, ce
case modeSixel:
padding *= cellX
}
- rows := splitRows(imagePaths, width, rowWidth)
- layouts := make([][]int, len(rows))
- for i, row := range rows {
- layouts[i] = layoutRow(row, width*cellX, rowWidth*cellX, cellX)
+ var metadata []imageMetadata
+ if *l {
+ metadata = make([]imageMetadata, len(imagePaths))
+ for i, filename := range imagePaths {
+ var err error
+ if metadata[i], err = readMetadata(filename); err != nil {
+ return err
+ }
+ }
+ }
+ rows := splitRows(imagePaths, metadata, width, rowWidth)
+ rowMetadata := make([][]imageMetadata, len(rows))
+ if *l {
+ for i, row := range rows {
+ rowMetadata[i] = metadata[:len(row)]
+ metadata = metadata[len(row):]
+ }
}
rowImages := make([]chan image.Image, len(rows))
for i := range rowImages {
@@ -245,7 +317,7 @@ func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, ce
sem <- struct{}{}
go func() {
defer func() { <-sem }()
- img, err := compositeImageRow(row, layouts[i], width*cellX, rowWidth*cellX, cellX, aspectRatio)
+ img, err := compositeImageRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX, aspectRatio)
if err != nil {
select {
case errCh <- err:
@@ -258,13 +330,6 @@ func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, ce
}
}()
for i, row := range rows {
- thisRowWidth := 0
- for j, filename := range row {
- if j > 0 {
- thisRowWidth += 2
- }
- thisRowWidth += max(width, runewidth.StringWidth(escapeString(filepath.Base(filename))))
- }
var rowImage image.Image
select {
case err := <-errCh:
@@ -273,12 +338,13 @@ func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, ce
}
printImg(w, rowImage)
fmt.Fprintln(w)
+ layouts := layoutRow(row, rowMetadata[i], width*cellX, rowWidth*cellX, cellX)
realOffset := 0
for j, filename := range row {
- start := divRound(layouts[i][j], cellX)
+ start := divRound(layouts[j], cellX)
end := rowWidth
if j+1 < len(row) {
- end = divRound(layouts[i][j+1], cellX)
+ end = divRound(layouts[j+1], cellX)
}
fmt.Fprintf(w, "%*s", start-realOffset, "")
realOffset = start
@@ -288,6 +354,23 @@ func thumbnailFiles(w io.Writer, imagePaths []string, width, rowWidth, cellX, ce
realOffset += padding + fileNameWidth
}
fmt.Fprintln(w)
+ if *l {
+ realOffset = 0
+ for j, metadata := range rowMetadata[i] {
+ start := divRound(layouts[j], cellX)
+ end := rowWidth
+ if j+1 < len(row) {
+ end = divRound(layouts[j+1], cellX)
+ }
+ fmt.Fprintf(w, "%*s", start-realOffset, "")
+ realOffset = start
+ details := metadata.String()
+ padding := max((end-start-len(details))/2, 0)
+ fmt.Fprintf(w, "%*s%s", padding, "", details)
+ realOffset += padding + len(details)
+ }
+ fmt.Fprintln(w)
+ }
}
return nil
}