From c20c9685aae51deeeef560d16bcf6e6039438b6c Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Wed, 30 Aug 2023 23:04:24 -0700 Subject: icat: a program to print images in the terminal. Thanks to WizardCoder for implementing the first version of this code. I had to clean it up quite a bit though. --- icat.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 icat.go (limited to 'icat.go') diff --git a/icat.go b/icat.go new file mode 100644 index 0000000..2b2a847 --- /dev/null +++ b/icat.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "golang.org/x/term" + "image" + _ "image/jpeg" + _ "image/png" + "os" +) + +func load(filename string) (image.Image, error) { + file := os.Stdin + if filename != "-" { + var err error + file, err = os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + } + + img, _, err := image.Decode(file) + if err != nil { + return nil, fmt.Errorf("decode %q: %s", filename, err) + } + + return img, nil +} + +func printImg(filename string) error { + img, err := load(filename) + if err != nil { + return err + } + + cols, lines, err := term.GetSize(1) + if err != nil { + return fmt.Errorf("terminal size: %s", err) + } + lines-- // Leave a line for the status bar. + + // Try not to stretch the image. + if img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { + lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2) + } else { + cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy() + } + + // nearest-neighbor interpolation + for y := 0; y < lines; y++ { + for x := 0; x < cols; x++ { + sx := x*img.Bounds().Dx()/cols + img.Bounds().Min.X + sy := y*img.Bounds().Dy()/lines + img.Bounds().Min.Y + r, g, b, _ := img.At(sx, sy).RGBA() + fmt.Printf("\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) + } + fmt.Println("\033[49m") + } + return nil +} + +func icat(args []string) error { + if len(args) == 0 { + args = []string{"-"} + } + for _, f := range args { + if err := printImg(f); err != nil { + return err + } + } + return nil +} + +func main() { + if err := icat(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "Failed: %s\n", err) + os.Exit(1) + } +} -- cgit v1.3.1 From 3d0c7978699c1280fd830324326c675e6570ed55 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Wed, 30 Aug 2023 23:16:50 -0700 Subject: Load terminal size once at the beginning. --- icat.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index 2b2a847..c26d0d2 100644 --- a/icat.go +++ b/icat.go @@ -28,18 +28,12 @@ func load(filename string) (image.Image, error) { return img, nil } -func printImg(filename string) error { +func printImg(filename string, cols, lines int) error { img, err := load(filename) if err != nil { return err } - cols, lines, err := term.GetSize(1) - if err != nil { - return fmt.Errorf("terminal size: %s", err) - } - lines-- // Leave a line for the status bar. - // Try not to stretch the image. if img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2) @@ -61,11 +55,17 @@ func printImg(filename string) error { } func icat(args []string) error { + cols, lines, err := term.GetSize(1) + if err != nil { + return fmt.Errorf("terminal size: %s", err) + } + lines-- // Leave a line for the status bar. + if len(args) == 0 { args = []string{"-"} } for _, f := range args { - if err := printImg(f); err != nil { + if err := printImg(f, cols, lines); err != nil { return err } } -- cgit v1.3.1 From c1159e5f8434ffb4e98c950f45cd682d73933534 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 31 Aug 2023 08:45:10 -0700 Subject: Use the draw package for resizing the image. This allows me to use the approximate bilinear interpolation, which looks a lot better than my brain-dead nearest-neighbor. --- flake.nix | 2 +- go.mod | 5 ++++- go.sum | 33 +++++++++++++++++++++++++++++++++ icat.go | 9 +++++---- 4 files changed, 43 insertions(+), 6 deletions(-) (limited to 'icat.go') diff --git a/flake.nix b/flake.nix index 81b7d36..2a263d8 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ src = self; - vendorHash = "sha256-WiEK8nq13mdTFnyxDiMRWM1tb60mlZY0fVmtlH5ZWgw="; + vendorHash = "sha256-v47V+DNmAImNZy/TXEuX1b4IVp960QPk0Lb+T4e6brI="; }; defaultPackage.x86_64-linux = packages.x86_64-linux.icat; diff --git a/go.mod b/go.mod index f2b8f47..94d1016 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module icat go 1.20 -require golang.org/x/term v0.11.0 +require ( + golang.org/x/image v0.11.0 + golang.org/x/term v0.11.0 +) require golang.org/x/sys v0.11.0 // indirect diff --git a/go.sum b/go.sum index 6aed7b2..212c42b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,37 @@ +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/icat.go b/icat.go index c26d0d2..ae1b7a0 100644 --- a/icat.go +++ b/icat.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "golang.org/x/image/draw" "golang.org/x/term" "image" _ "image/jpeg" @@ -41,12 +42,12 @@ func printImg(filename string, cols, lines int) error { cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy() } - // nearest-neighbor interpolation + dst := image.NewRGBA(image.Rect(0, 0, cols, lines)) + draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + for y := 0; y < lines; y++ { for x := 0; x < cols; x++ { - sx := x*img.Bounds().Dx()/cols + img.Bounds().Min.X - sy := y*img.Bounds().Dy()/lines + img.Bounds().Min.Y - r, g, b, _ := img.At(sx, sy).RGBA() + r, g, b, _ := dst.At(x, y).RGBA() fmt.Printf("\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) } fmt.Println("\033[49m") -- cgit v1.3.1 From 3d875397ab4f21f06bc490982a62fdd62aa76a91 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 31 Aug 2023 08:46:26 -0700 Subject: Use bufio for output. This is a lot faster for a large image. --- icat.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index ae1b7a0..f2c3f08 100644 --- a/icat.go +++ b/icat.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "fmt" "golang.org/x/image/draw" "golang.org/x/term" @@ -45,12 +46,14 @@ func printImg(filename string, cols, lines int) error { dst := image.NewRGBA(image.Rect(0, 0, cols, lines)) draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + buf := bufio.NewWriter(os.Stdout) + defer buf.Flush() for y := 0; y < lines; y++ { for x := 0; x < cols; x++ { r, g, b, _ := dst.At(x, y).RGBA() - fmt.Printf("\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) + fmt.Fprintf(buf, "\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) } - fmt.Println("\033[49m") + fmt.Fprintln(buf, "\033[49m") } return nil } -- cgit v1.3.1 From 2ef107aee0b4e88b3335b04418ec0e27ad8ebbfc Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 31 Aug 2023 09:50:17 -0700 Subject: Use one stdout buffer for the whole program. --- icat.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index f2c3f08..1a8dd8a 100644 --- a/icat.go +++ b/icat.go @@ -11,6 +11,8 @@ import ( "os" ) +var stdout = bufio.NewWriter(os.Stdout) + func load(filename string) (image.Image, error) { file := os.Stdin if filename != "-" { @@ -46,14 +48,12 @@ func printImg(filename string, cols, lines int) error { dst := image.NewRGBA(image.Rect(0, 0, cols, lines)) draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) - buf := bufio.NewWriter(os.Stdout) - defer buf.Flush() for y := 0; y < lines; y++ { for x := 0; x < cols; x++ { r, g, b, _ := dst.At(x, y).RGBA() - fmt.Fprintf(buf, "\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) + fmt.Fprintf(stdout, "\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) } - fmt.Fprintln(buf, "\033[49m") + fmt.Fprintln(stdout, "\033[49m") } return nil } @@ -68,6 +68,8 @@ func icat(args []string) error { if len(args) == 0 { args = []string{"-"} } + + defer stdout.Flush() for _, f := range args { if err := printImg(f, cols, lines); err != nil { return err -- cgit v1.3.1 From 8896fdcf2c049a5b428ca1c49bf28fc2702c0ae1 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Mon, 17 Mar 2025 16:38:36 -0700 Subject: Print in double resolution using ▀ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- icat.go | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) (limited to 'icat.go') diff --git a/go.mod b/go.mod index 94d1016..9019d01 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module icat +module gitlab.com/rhogenson/icat go 1.20 diff --git a/icat.go b/icat.go index 1a8dd8a..15f10d1 100644 --- a/icat.go +++ b/icat.go @@ -3,12 +3,13 @@ package main import ( "bufio" "fmt" - "golang.org/x/image/draw" - "golang.org/x/term" "image" _ "image/jpeg" _ "image/png" "os" + + "golang.org/x/image/draw" + "golang.org/x/term" ) var stdout = bufio.NewWriter(os.Stdout) @@ -45,16 +46,20 @@ func printImg(filename string, cols, lines int) error { cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy() } - dst := image.NewRGBA(image.Rect(0, 0, cols, lines)) - draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + dst := image.NewRGBA(image.Rect(0, 0, cols, 2*lines)) + draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) - for y := 0; y < lines; y++ { + for y := 0; y < 2*lines; y += 2 { for x := 0; x < cols; x++ { - r, g, b, _ := dst.At(x, y).RGBA() - fmt.Fprintf(stdout, "\033[48;2;%d;%d;%dm ", r>>8, g>>8, b>>8) + hiR, hiG, hiB, _ := dst.At(x, y).RGBA() + loR, loG, loB, _ := dst.At(x, y+1).RGBA() + fmt.Fprintf(stdout, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀", + hiR>>8, hiG>>8, hiB>>8, + loR>>8, loG>>8, loB>>8) } fmt.Fprintln(stdout, "\033[49m") } + fmt.Fprint(stdout, "\033[39m") return nil } -- cgit v1.3.1 From c62789a6c3c2998cdc94d60166a0d269dce8db68 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Mon, 17 Mar 2025 16:44:56 -0700 Subject: Use bilinear scaling --- icat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index 15f10d1..01f0400 100644 --- a/icat.go +++ b/icat.go @@ -47,7 +47,7 @@ func printImg(filename string, cols, lines int) error { } dst := image.NewRGBA(image.Rect(0, 0, cols, 2*lines)) - draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) for y := 0; y < 2*lines; y += 2 { for x := 0; x < cols; x++ { -- cgit v1.3.1 From 5924ab4508d90f074157888899ac52c2af57090a Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Mon, 17 Mar 2025 20:22:28 -0700 Subject: Improve usage --- README.md | 3 +++ icat.go | 46 +++++++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 README.md (limited to 'icat.go') diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dc67b9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# The worst terminal image viewer + +seriously just use img2sixel diff --git a/icat.go b/icat.go index 01f0400..4bcfb45 100644 --- a/icat.go +++ b/icat.go @@ -2,6 +2,8 @@ package main import ( "bufio" + "errors" + "flag" "fmt" "image" _ "image/jpeg" @@ -12,8 +14,6 @@ import ( "golang.org/x/term" ) -var stdout = bufio.NewWriter(os.Stdout) - func load(filename string) (image.Image, error) { file := os.Stdin if filename != "-" { @@ -33,12 +33,7 @@ func load(filename string) (image.Image, error) { return img, nil } -func printImg(filename string, cols, lines int) error { - img, err := load(filename) - if err != nil { - return err - } - +func printImg(img image.Image, cols, lines int) error { // Try not to stretch the image. if img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2) @@ -49,42 +44,55 @@ func printImg(filename string, cols, lines int) error { dst := image.NewRGBA(image.Rect(0, 0, cols, 2*lines)) draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + w := bufio.NewWriter(os.Stdout) + defer w.Flush() for y := 0; y < 2*lines; y += 2 { for x := 0; x < cols; x++ { hiR, hiG, hiB, _ := dst.At(x, y).RGBA() loR, loG, loB, _ := dst.At(x, y+1).RGBA() - fmt.Fprintf(stdout, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀", + fmt.Fprintf(w, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀", hiR>>8, hiG>>8, hiB>>8, loR>>8, loG>>8, loB>>8) } - fmt.Fprintln(stdout, "\033[49m") + fmt.Fprintln(w, "\033[49m") } - fmt.Fprint(stdout, "\033[39m") + fmt.Fprint(w, "\033[39m") return nil } func icat(args []string) error { + if len(args) == 0 { + return errors.New("missing positional argument") + } + if len(args) > 1 { + return errors.New("too many positional arguments") + } + file := args[0] + cols, lines, err := term.GetSize(1) if err != nil { return fmt.Errorf("terminal size: %s", err) } lines-- // Leave a line for the status bar. - if len(args) == 0 { - args = []string{"-"} + img, err := load(file) + if err != nil { + return err } - defer stdout.Flush() - for _, f := range args { - if err := printImg(f, cols, lines); err != nil { - return err - } + if err := printImg(img, cols, lines); err != nil { + return err } return nil } func main() { - if err := icat(os.Args[1:]); err != nil { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: icat FILE\n") + } + flag.Parse() + + if err := icat(flag.Args()); err != nil { fmt.Fprintf(os.Stderr, "Failed: %s\n", err) os.Exit(1) } -- cgit v1.3.1 From 2d057d030f81069a3bc4e6e4c0317dcd68534e78 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Mon, 17 Mar 2025 20:57:00 -0700 Subject: Add more file formats --- icat.go | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'icat.go') diff --git a/icat.go b/icat.go index 4bcfb45..56ab95a 100644 --- a/icat.go +++ b/icat.go @@ -6,11 +6,15 @@ import ( "flag" "fmt" "image" + _ "image/gif" _ "image/jpeg" _ "image/png" "os" + _ "golang.org/x/image/bmp" "golang.org/x/image/draw" + _ "golang.org/x/image/tiff" + _ "golang.org/x/image/webp" "golang.org/x/term" ) -- cgit v1.3.1 From c423ae65575a984a4c1ca1af51ab8c1f3ceac7d9 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Wed, 8 Oct 2025 17:34:23 -0700 Subject: Update import path --- go.mod | 10 +++++----- go.sum | 18 ++++++------------ icat.go | 21 ++++++++++++++++----- 3 files changed, 27 insertions(+), 22 deletions(-) (limited to 'icat.go') diff --git a/go.mod b/go.mod index fdcae13..3ed9d4d 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ -module github.com/rhogenson/icat +module roseh.moe/cmd/icat -go 1.23.0 +go 1.24.0 toolchain go1.24.1 require ( - golang.org/x/image v0.26.0 - golang.org/x/term v0.31.0 + golang.org/x/image v0.32.0 + golang.org/x/term v0.36.0 ) -require golang.org/x/sys v0.32.0 // indirect +require golang.org/x/sys v0.37.0 // indirect diff --git a/go.sum b/go.sum index c68db5e..e9c590b 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,6 @@ -golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= -golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= diff --git a/icat.go b/icat.go index 56ab95a..8faf85f 100644 --- a/icat.go +++ b/icat.go @@ -1,3 +1,4 @@ +// The icat command displays an image to the terminal using block characters. package main import ( @@ -18,6 +19,11 @@ import ( "golang.org/x/term" ) +var ( + x = flag.Int("x", 0, "set image width in columns") + y = flag.Int("y", 0, "set image height in rows") +) + func load(filename string) (image.Image, error) { file := os.Stdin if filename != "-" { @@ -39,7 +45,7 @@ func load(filename string) (image.Image, error) { func printImg(img image.Image, cols, lines int) error { // Try not to stretch the image. - if img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { + if lines == 0 || cols != 0 && img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2) } else { cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy() @@ -73,11 +79,16 @@ func icat(args []string) error { } file := args[0] - cols, lines, err := term.GetSize(1) - if err != nil { - return fmt.Errorf("terminal size: %s", err) + cols := *x + lines := *y + if cols == 0 && lines == 0 { + var err error + cols, lines, err = term.GetSize(1) + if err != nil { + return fmt.Errorf("terminal size: %s", err) + } + lines-- // Leave a line for the status bar. } - lines-- // Leave a line for the status bar. img, err := load(file) if err != nil { -- cgit v1.3.1 From 3ac0a6b87c04374777d9405e3bddc1b29f337886 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 28 May 2026 16:28:19 -0700 Subject: Use the sixel package --- go.mod | 8 ++--- go.sum | 4 +-- icat.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 89 insertions(+), 32 deletions(-) (limited to 'icat.go') diff --git a/go.mod b/go.mod index 3ed9d4d..36cf1c7 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module roseh.moe/cmd/icat -go 1.24.0 - -toolchain go1.24.1 +go 1.26.3 require ( golang.org/x/image v0.32.0 - golang.org/x/term v0.36.0 + roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153 ) -require golang.org/x/sys v0.37.0 // indirect +require golang.org/x/sys v0.37.0 diff --git a/go.sum b/go.sum index e9c590b..e7f3a84 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,5 @@ golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153 h1:Jq7Yv9BeWwBPA5BY238i0SpXDqKudsqvlTZUvbi6ZMg= +roseh.moe/pkg/sixel v0.0.0-20260528225709-a6d059daa153/go.mod h1:DX/c+l3VYm+yXoACY+HXl2gLV5o5iAG6gkHeDddTSrQ= diff --git a/icat.go b/icat.go index 8faf85f..a1922f1 100644 --- a/icat.go +++ b/icat.go @@ -2,7 +2,6 @@ package main import ( - "bufio" "errors" "flag" "fmt" @@ -16,14 +15,61 @@ import ( "golang.org/x/image/draw" _ "golang.org/x/image/tiff" _ "golang.org/x/image/webp" - "golang.org/x/term" + "golang.org/x/sys/unix" + "roseh.moe/pkg/sixel" ) var ( x = flag.Int("x", 0, "set image width in columns") y = flag.Int("y", 0, "set image height in rows") + 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 load(filename string) (image.Image, error) { file := os.Stdin if filename != "-" { @@ -43,30 +89,26 @@ func load(filename string) (image.Image, error) { return img, nil } -func printImg(img image.Image, cols, lines int) error { +func printImg(img image.Image, x, y, pixelX, pixelY int) error { // Try not to stretch the image. - if lines == 0 || cols != 0 && img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 { - lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2) + if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { + y = img.Bounds().Dy() * x / (img.Bounds().Dx() * pixelY / pixelX) } else { - cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy() + x = img.Bounds().Dx() * y * pixelY / pixelX / img.Bounds().Dy() } - dst := image.NewRGBA(image.Rect(0, 0, cols, 2*lines)) - draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) - - w := bufio.NewWriter(os.Stdout) - defer w.Flush() - for y := 0; y < 2*lines; y += 2 { - for x := 0; x < cols; x++ { - hiR, hiG, hiB, _ := dst.At(x, y).RGBA() - loR, loG, loB, _ := dst.At(x, y+1).RGBA() - fmt.Fprintf(w, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀", - hiR>>8, hiG>>8, hiB>>8, - loR>>8, loG>>8, loB>>8) - } - fmt.Fprintln(w, "\033[49m") + dst := image.NewRGBA(image.Rect(0, 0, x, y)) + draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) + + switch *m { + case modeBlock: + sixel.PrintXTerm16(os.Stdout, dst) + case modeBlock24: + sixel.PrintBlock(os.Stdout, dst) + case modeSixel: + sixel.Print(os.Stdout, dst) } - fmt.Fprint(w, "\033[39m") + fmt.Println() return nil } @@ -81,13 +123,30 @@ func icat(args []string) error { cols := *x lines := *y + pixelX := 2 + pixelY := 5 if cols == 0 && lines == 0 { - var err error - cols, lines, err = term.GetSize(1) + ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) if err != nil { - return fmt.Errorf("terminal size: %s", err) + return err } + cols = int(ws.Col) + lines = int(ws.Row) lines-- // Leave a line for the status bar. + cellX, cellY := int(ws.Xpixel)/int(ws.Col), int(ws.Ypixel)/int(ws.Row) + if *m == modeSixel { + cols *= cellX + lines *= cellY + } else { + pixelX, pixelY = cellX, cellY + } + } + switch *m { + case modeSixel: + pixelX, pixelY = 1, 1 + case modeBlock24: + lines *= 2 + pixelX *= 2 } img, err := load(file) @@ -95,7 +154,7 @@ func icat(args []string) error { return err } - if err := printImg(img, cols, lines); err != nil { + if err := printImg(img, cols, lines, pixelX, pixelY); err != nil { return err } return nil -- cgit v1.3.1 From 7c5f38d06afc4041261379ce69fa5df2442ea24e Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 28 May 2026 16:48:49 -0700 Subject: Change to block24 mode by default This will have better compatibility, e.g. with tmux. I also added handling for when Xpixel and Ypixel are zero. --- icat.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index a1922f1..a8768b3 100644 --- a/icat.go +++ b/icat.go @@ -22,7 +22,7 @@ import ( var ( x = flag.Int("x", 0, "set image width in columns") y = flag.Int("y", 0, "set image height in rows") - m = flagPrintMode(flag.CommandLine, "m", modeSixel, "one of 'block', 'block24', or 'sixel'") + m = flagPrintMode(flag.CommandLine, "m", modeBlock24, "one of 'block', 'block24', or 'sixel'") ) type printMode int @@ -134,6 +134,9 @@ func icat(args []string) error { lines = int(ws.Row) lines-- // Leave a line for the status bar. cellX, cellY := int(ws.Xpixel)/int(ws.Col), int(ws.Ypixel)/int(ws.Row) + if cellX == 0 && cellY == 0 { + cellX, cellY = 10, 20 + } if *m == modeSixel { cols *= cellX lines *= cellY -- cgit v1.3.1 From 0c6cb370056bdef55fdfef8d543ee4daf2dadf8a Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 30 May 2026 11:10:44 -0700 Subject: Move median cut into icat --- go.mod | 2 +- go.sum | 4 +-- icat.go | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 4 deletions(-) (limited to 'icat.go') diff --git a/go.mod b/go.mod index 0d8e407..2af2e32 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.3 require ( golang.org/x/image v0.32.0 - roseh.moe/pkg/sixel v0.1.2 + roseh.moe/pkg/sixel v0.2.0 ) require golang.org/x/sys v0.37.0 diff --git a/go.sum b/go.sum index ff70566..d9e7b04 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,5 @@ golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -roseh.moe/pkg/sixel v0.1.2 h1:dSRcqJ+sw8g3UdzFyGT+s9IZsWoR/bOBHoNuQh8HfsI= -roseh.moe/pkg/sixel v0.1.2/go.mod h1:DX/c+l3VYm+yXoACY+HXl2gLV5o5iAG6gkHeDddTSrQ= +roseh.moe/pkg/sixel v0.2.0 h1:WkDXFmX7qk40fd870tsSTbFyKW5WUOy7h9FzXu6Q+xY= +roseh.moe/pkg/sixel v0.2.0/go.mod h1:DX/c+l3VYm+yXoACY+HXl2gLV5o5iAG6gkHeDddTSrQ= diff --git a/icat.go b/icat.go index a8768b3..592a714 100644 --- a/icat.go +++ b/icat.go @@ -6,10 +6,14 @@ import ( "flag" "fmt" "image" + "image/color" _ "image/gif" _ "image/jpeg" _ "image/png" + "math" + "math/rand/v2" "os" + "slices" _ "golang.org/x/image/bmp" "golang.org/x/image/draw" @@ -89,6 +93,116 @@ func load(filename string) (image.Image, error) { return img, nil } +func partition[S ~[]E, E any](a S, i, j, pivotIndex int, cmp func(E, E) int) int { + pivot := a[pivotIndex] + for { + for ; cmp(a[i], pivot) < 0; i++ { + } + for ; cmp(a[j], pivot) > 0; j-- { + } + if i >= j { + return j + } + a[i], a[j] = a[j], a[i] + i++ + j-- + } +} + +func quickSelect[S ~[]E, E any](list S, k int, cmp func(E, E) int) { + left, right := 0, len(list)-1 + for { + if left == right { + return + } + pivotIndex := left + rand.IntN(right-left+1) + pivotIndex = partition(list, left, right, pivotIndex, cmp) + if k <= pivotIndex { + right = pivotIndex + } else { + left = pivotIndex + 1 + } + } +} + +func bucketRange(colors []color.RGBA) color.RGBA { + if len(colors) == 0 { + return color.RGBA{} + } + var minR, minG, minB uint8 = math.MaxUint8, math.MaxUint8, math.MaxUint8 + var maxR, maxG, maxB uint8 + for _, c := range colors { + minR, maxR = min(minR, c.R), max(maxR, c.R) + minG, maxG = min(minG, c.G), max(maxG, c.G) + minB, maxB = min(minB, c.B), max(maxB, c.B) + } + return color.RGBA{R: maxR - minR, G: maxG - minG, B: maxB - minB} +} + +func cutOnce(colors []color.RGBA, bucketRange color.RGBA) [2][]color.RGBA { + if len(colors) == 0 { + return [...][]color.RGBA{colors, colors} + } + rRange, gRange, bRange := bucketRange.R, bucketRange.G, bucketRange.B + if rRange >= gRange && rRange >= bRange { + quickSelect(colors, len(colors)/2, func(x, y color.RGBA) int { return int(x.R) - int(y.R) }) + } else if gRange >= rRange && gRange >= bRange { + quickSelect(colors, len(colors)/2, func(x, y color.RGBA) int { return int(x.G) - int(y.G) }) + } else { + quickSelect(colors, len(colors)/2, func(x, y color.RGBA) int { return int(x.B) - int(y.B) }) + } + return [...][]color.RGBA{colors[:len(colors)/2], colors[len(colors)/2:]} +} + +func colorAvg(colors []color.RGBA) color.RGBA { + var r, g, b int64 + for _, c := range colors { + r += int64(c.R) + g += int64(c.G) + b += int64(c.B) + } + n := int64(len(colors)) + return color.RGBA{R: uint8(r / n), G: uint8(g / n), B: uint8(b / n), A: 0xff} +} + +func medianCut(img image.Image) color.Palette { + var colors []color.RGBA + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + r, g, b, a := img.At(x, y).RGBA() + if a > 0 { + colors = append(colors, color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 0xff}) + } + } + } + buckets := [][]color.RGBA{colors} + bucketRanges := []color.RGBA{{}} + for { + var bestRange uint8 + var bestIdx int + for i, rng := range bucketRanges { + r := max(rng.R, rng.G, rng.B) + if r >= bestRange { + bestRange = r + bestIdx = i + } + } + split := cutOnce(buckets[bestIdx], bucketRanges[bestIdx]) + buckets = slices.Replace(buckets, bestIdx, bestIdx+1, split[:]...) + if len(buckets) == 255 { + break + } + bucketRanges = slices.Replace(bucketRanges, bestIdx, bestIdx+1, bucketRange(split[0]), bucketRange(split[1])) + } + palette := color.Palette{color.Transparent} + for _, b := range buckets { + if len(b) > 0 { + palette = append(palette, colorAvg(b)) + } + } + return palette +} + func printImg(img image.Image, x, y, pixelX, pixelY int) error { // Try not to stretch the image. if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { @@ -106,7 +220,7 @@ func printImg(img image.Image, x, y, pixelX, pixelY int) error { case modeBlock24: sixel.PrintBlock(os.Stdout, dst) case modeSixel: - sixel.Print(os.Stdout, dst) + sixel.Print(os.Stdout, dst, medianCut(dst)) } fmt.Println() return nil -- cgit v1.3.1 From a36546ca9b75ca829464ce1eebeeae12fdb194a5 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 30 May 2026 13:39:26 -0700 Subject: Floyd-Rivest --- bench_test.go | 14 +++++------- icat.go | 72 +++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 54 insertions(+), 32 deletions(-) (limited to 'icat.go') diff --git a/bench_test.go b/bench_test.go index 1047865..1e79a10 100644 --- a/bench_test.go +++ b/bench_test.go @@ -7,16 +7,14 @@ import ( ) func BenchmarkQuickSelect(b *testing.B) { - r := rand.New(rand.NewPCG(0, 0)) - testSlice := make([]int, 3840*2160) - for i := range testSlice { - testSlice[i] = r.Int() - } - myTestSlice := make([]int, len(testSlice)) + rng := rand.New(rand.NewPCG(0, 0)) + myTestCase := make([]int, 3840*2160) for b.Loop() { b.StopTimer() - copy(myTestSlice, testSlice) + for i := range myTestCase { + myTestCase[i] = rng.Int() + } b.StartTimer() - quickSelect(myTestSlice, len(myTestSlice)/2, cmp.Compare) + quickSelect(myTestCase, len(myTestCase)/2, cmp.Compare) } } diff --git a/icat.go b/icat.go index 592a714..3fe9a35 100644 --- a/icat.go +++ b/icat.go @@ -11,7 +11,6 @@ import ( _ "image/jpeg" _ "image/png" "math" - "math/rand/v2" "os" "slices" @@ -93,38 +92,63 @@ func load(filename string) (image.Image, error) { return img, nil } -func partition[S ~[]E, E any](a S, i, j, pivotIndex int, cmp func(E, E) int) int { - pivot := a[pivotIndex] - for { - for ; cmp(a[i], pivot) < 0; i++ { - } - for ; cmp(a[j], pivot) > 0; j-- { - } - if i >= j { - return j - } - a[i], a[j] = a[j], a[i] - i++ - j-- +func sign(n int) float64 { + if n < 0 { + return -1 } + if n > 0 { + return 1 + } + return 0 } -func quickSelect[S ~[]E, E any](list S, k int, cmp func(E, E) int) { - left, right := 0, len(list)-1 - for { - if left == right { - return +func floydRivest[S ~[]E, E any](array S, left, right, k int, cmp func(E, E) int) { + for right > left { + if right-left > 600 { + n := right - left + 1 + i := k - left + 1 + z := math.Log(float64(n)) + s := .5 * math.Exp(2*z/3) + sd := .5 * math.Sqrt(z*s*(float64(n)-s)/float64(n)) * sign(i-n/2) + newLeft := max(left, int(float64(k)-float64(i)*s/float64(n)+sd)) + newRight := min(right, int(float64(k)+float64(n-i)*s/float64(n)+sd)) + floydRivest(array, newLeft, newRight, k, cmp) + } + t := array[k] + i := left + j := right + array[left], array[k] = array[k], array[left] + if cmp(array[right], t) > 0 { + array[right], array[left] = array[left], array[right] } - pivotIndex := left + rand.IntN(right-left+1) - pivotIndex = partition(list, left, right, pivotIndex, cmp) - if k <= pivotIndex { - right = pivotIndex + for i < j { + array[i], array[j] = array[j], array[i] + i++ + j-- + for ; cmp(array[i], t) < 0; i++ { + } + for ; cmp(array[j], t) > 0; j-- { + } + } + if cmp(array[left], t) == 0 { + array[left], array[j] = array[j], array[left] } else { - left = pivotIndex + 1 + j++ + array[j], array[right] = array[right], array[j] + } + if j <= k { + left = j + 1 + } + if k <= j { + right = j - 1 } } } +func quickSelect[S ~[]E, E any](list S, k int, cmp func(E, E) int) { + floydRivest(list, 0, len(list)-1, k, cmp) +} + func bucketRange(colors []color.RGBA) color.RGBA { if len(colors) == 0 { return color.RGBA{} -- cgit v1.3.1 From 5efde7b99a748cae7989f1f6e022bfced4a4b841 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Tue, 9 Jun 2026 22:20:08 -0700 Subject: Apply some rounding --- icat.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index 3fe9a35..9382332 100644 --- a/icat.go +++ b/icat.go @@ -73,6 +73,10 @@ func flagPrintMode(fs *flag.FlagSet, name string, value printMode, usage string) return &value } +func divRound[N ~int64 | ~int](n, d N) N { + return (n + d/2) / d +} + func load(filename string) (image.Image, error) { file := os.Stdin if filename != "-" { @@ -186,7 +190,7 @@ func colorAvg(colors []color.RGBA) color.RGBA { b += int64(c.B) } n := int64(len(colors)) - return color.RGBA{R: uint8(r / n), G: uint8(g / n), B: uint8(b / n), A: 0xff} + return color.RGBA{R: uint8(divRound(r, n)), G: uint8(divRound(g, n)), B: uint8(divRound(b, n)), A: 0xff} } func medianCut(img image.Image) color.Palette { @@ -230,9 +234,9 @@ func medianCut(img image.Image) color.Palette { func printImg(img image.Image, x, y, pixelX, pixelY int) error { // Try not to stretch the image. if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { - y = img.Bounds().Dy() * x / (img.Bounds().Dx() * pixelY / pixelX) + y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) } else { - x = img.Bounds().Dx() * y * pixelY / pixelX / img.Bounds().Dy() + x = divRound(img.Bounds().Dx()*y*pixelY, pixelX*img.Bounds().Dy()) } dst := image.NewRGBA(image.Rect(0, 0, x, y)) -- cgit v1.3.1 From b2b0ff935fa517fd235887ad66b55c07ff0994f9 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 11 Jun 2026 21:14:40 -0700 Subject: Don't resize up every image --- icat.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index 9382332..f7f31df 100644 --- a/icat.go +++ b/icat.go @@ -231,7 +231,9 @@ func medianCut(img image.Image) color.Palette { return palette } -func printImg(img image.Image, x, y, pixelX, pixelY int) error { +func printImg(img image.Image, maxX, maxY, pixelX, pixelY int) error { + x := min(img.Bounds().Dx(), maxX) + y := min(img.Bounds().Dy(), maxY) // Try not to stretch the image. if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) -- cgit v1.3.1 From 38b5c1a8ab5a6e9c1eadc4ebb56b4af9dc0cb7c6 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 11 Jun 2026 21:35:19 -0700 Subject: Don't resize unless necessary --- icat.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index f7f31df..d1f7388 100644 --- a/icat.go +++ b/icat.go @@ -234,23 +234,26 @@ func medianCut(img image.Image) color.Palette { func printImg(img image.Image, maxX, maxY, pixelX, pixelY int) error { x := min(img.Bounds().Dx(), maxX) y := min(img.Bounds().Dy(), maxY) - // Try not to stretch the image. - if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { - y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) - } else { - x = divRound(img.Bounds().Dx()*y*pixelY, pixelX*img.Bounds().Dy()) - } + if x != img.Bounds().Dx() || y != img.Bounds().Dy() { + // Try not to stretch the image. + if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { + y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) + } else { + x = divRound(img.Bounds().Dx()*y*pixelY, pixelX*img.Bounds().Dy()) + } - dst := image.NewRGBA(image.Rect(0, 0, x, y)) - draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) + dst := image.NewRGBA(image.Rect(0, 0, x, y)) + draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) + img = dst + } switch *m { case modeBlock: - sixel.PrintXTerm16(os.Stdout, dst) + sixel.PrintXTerm16(os.Stdout, img) case modeBlock24: - sixel.PrintBlock(os.Stdout, dst) + sixel.PrintBlock(os.Stdout, img) case modeSixel: - sixel.Print(os.Stdout, dst, medianCut(dst)) + sixel.Print(os.Stdout, img, medianCut(img)) } fmt.Println() return nil -- cgit v1.3.1 From 6dba3cc038d8adfc35d893a4ed94aa001fe9c0b0 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 11 Jun 2026 23:31:49 -0700 Subject: Fix a bug where image is stretched with -m block --- icat.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'icat.go') diff --git a/icat.go b/icat.go index d1f7388..2b69170 100644 --- a/icat.go +++ b/icat.go @@ -234,14 +234,15 @@ func medianCut(img image.Image) color.Palette { func printImg(img image.Image, maxX, maxY, pixelX, pixelY int) error { x := min(img.Bounds().Dx(), maxX) y := min(img.Bounds().Dy(), maxY) - if x != img.Bounds().Dx() || y != img.Bounds().Dy() { - // Try not to stretch the image. - if y == 0 || x != 0 && img.Bounds().Dy()*x <= img.Bounds().Dx()*y*pixelY/pixelX { - y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) - } else { - x = divRound(img.Bounds().Dx()*y*pixelY, pixelX*img.Bounds().Dy()) - } + // Try not to stretch the image. + if y == 0 || x != 0 && img.Bounds().Dy()*x*pixelX <= img.Bounds().Dx()*y*pixelY { + y = divRound(img.Bounds().Dy()*x*pixelX, img.Bounds().Dx()*pixelY) + } else { + x = divRound(img.Bounds().Dx()*y*pixelY, pixelX*img.Bounds().Dy()) + } + + if x != img.Bounds().Dx() || y != img.Bounds().Dy() { dst := image.NewRGBA(image.Rect(0, 0, x, y)) draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) img = dst -- cgit v1.3.1