File: vip/config.go
   1 package main
   2 
   3 import (
   4     "flag"
   5     "fmt"
   6     "strconv"
   7 )
   8 
   9 // config is the parsed cmd-line options given to the app
  10 type config struct {
  11     Width     int
  12     Filenames []string
  13 }
  14 
  15 const (
  16     widthUsage = "output width in character blocks/rectangles"
  17 )
  18 
  19 // parseFlags is the constructor for type config
  20 func parseFlags(usage string) config {
  21     flag.Usage = func() {
  22         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  23         flag.PrintDefaults()
  24     }
  25 
  26     cfg := config{Width: 80}
  27 
  28     flag.IntVar(&cfg.Width, "w", cfg.Width, "alias for option -width")
  29     flag.IntVar(&cfg.Width, "width", cfg.Width, widthUsage)
  30     flag.Parse()
  31 
  32     cfg.Filenames = flag.Args()
  33 
  34     // handle a leading number like the width option
  35     if len(cfg.Filenames) > 0 {
  36         n, err := strconv.Atoi(cfg.Filenames[0])
  37         if err == nil && n > 0 {
  38             cfg.Width = n
  39             cfg.Filenames = cfg.Filenames[1:]
  40         }
  41     }
  42 
  43     return cfg
  44 }

     File: vip/go.mod
   1 module vip
   2 
   3 go 1.18
   4 
   5 require golang.org/x/image v0.7.0

     File: vip/go.sum
   1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
   2 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
   3 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
   4 golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
   5 golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
   6 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
   7 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
   8 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
   9 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
  10 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
  11 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
  12 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
  13 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
  14 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
  15 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
  16 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
  17 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
  18 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
  19 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
  20 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
  21 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
  22 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
  23 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
  24 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  25 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
  26 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
  27 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
  28 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
  29 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
  30 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
  31 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
  32 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
  33 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

     File: vip/images.go
   1 package main
   2 
   3 import (
   4     "bytes"
   5     "image"
   6 )
   7 
   8 // picture is a true-color picture with size info, and isn't called image
   9 // because this app uses package image from the stdlib, taking away that name
  10 type picture struct {
  11     // Width is the picture's width in pixels
  12     Width uint32
  13 
  14     // Height is the picture's height in pixels
  15     Height uint32
  16 
  17     // Pixels is the repeating RGB byte-sequence of length 3*Width*Height
  18     Pixels []byte
  19 }
  20 
  21 // ensureRGBA turns generic images into RGBA images specifically, since the
  22 // image display logic depends on that specific format
  23 func ensureRGBA(img image.Image) *image.RGBA {
  24     rgba, ok := img.(*image.RGBA)
  25     if ok {
  26         // it's already RGBA
  27         return rgba
  28     }
  29 
  30     bnds := img.Bounds()
  31     rgba = image.NewRGBA(bnds)
  32     for row := bnds.Min.Y; row < bnds.Max.Y; row++ {
  33         for col := bnds.Min.X; col < bnds.Max.X; col++ {
  34             rgba.Set(col, row, img.At(col, row))
  35         }
  36     }
  37     return rgba
  38 }
  39 
  40 // smoothShrink shrinks the picture given by averaging all pixels matching each
  41 // pixel in the shrunk image, resulting in a smooth/low-noise result
  42 func smoothShrink(in picture, w, h int) picture {
  43     if w < 1 || h < 1 {
  44         return picture{}
  45     }
  46     if int(in.Width) <= w && int(in.Height) <= h {
  47         // input pic is smaller than requested output, so there's
  48         // no need to shrink it
  49         return in
  50     }
  51 
  52     // sum all RGB values for each output tile's `catchment region`, as
  53     // well as their hit-counts, to later calculate arithmetic averages
  54 
  55     hits := make([]float64, w*h)
  56     sums := make([]float64, 3*w*h)
  57 
  58     xincr := float64(w) / float64(in.Width)
  59     yincr := float64(h) / float64(in.Height)
  60 
  61     for row := 0; row < int(in.Height); row++ {
  62         for col := 0; col < int(in.Width); col++ {
  63             x := int(xincr * float64(col))
  64             y := int(yincr * float64(row))
  65 
  66             dst := (w*y + x)
  67             // calculate pixel-areas from the source would arguably be more
  68             // efficient, but counting hits is simple and it works; later
  69             // note: that approach leaves a few subtle periodic bands, so
  70             // keep this naive approach for now
  71             hits[dst]++
  72 
  73             dst *= 3
  74             src := 3 * (int(in.Width)*row + col)
  75             // add normalized values (range 0..1) for each color component
  76             sums[dst+0] += float64(in.Pixels[src+0]) / 255
  77             sums[dst+1] += float64(in.Pixels[src+1]) / 255
  78             sums[dst+2] += float64(in.Pixels[src+2]) / 255
  79         }
  80     }
  81 
  82     // shrunk is a picture representing the output tiles, each pixel being
  83     // the average of all source pixels from their corresponding region from
  84     // the original picture
  85     shrunk := picture{
  86         Width:  uint32(w),
  87         Height: uint32(h),
  88         Pixels: make([]byte, 0, 3*w*h),
  89     }
  90     for i := 0; i < 3*w*h; i++ {
  91         avg := 255.0 * sums[i] / hits[i/3]
  92         // avg := 255.0 * sums[i] * (xincr * yincr)
  93         shrunk.Pixels = append(shrunk.Pixels, byte(avg))
  94     }
  95     return shrunk
  96 }
  97 
  98 // noisyShrink is a simpler way to shrink down a pic: the name comes from the
  99 // side-effect of sampling a single pixel from a whole region, resulting in
 100 // high-noise fast-changing pixel colors in the result image
 101 func noisyShrink(in picture, w, h int) picture {
 102     if w < 1 || h < 1 {
 103         return picture{}
 104     }
 105     if int(in.Width) <= w && int(in.Height) <= h {
 106         return in
 107     }
 108 
 109     shrunk := picture{
 110         Width:  uint32(w),
 111         Height: uint32(h),
 112         Pixels: make([]byte, 3*w*h),
 113     }
 114 
 115     xincr := float64(in.Width) / float64(w)
 116     yincr := float64(in.Height) / float64(h)
 117 
 118     for row := 0; row < h; row++ {
 119         for col := 0; col < w; col++ {
 120             x := int(xincr * float64(col))
 121             y := int(yincr * float64(row))
 122 
 123             src := 3 * (int(in.Width)*y + x)
 124             dst := 3 * (w*row + col)
 125             shrunk.Pixels[dst+0] = in.Pixels[src+0]
 126             shrunk.Pixels[dst+1] = in.Pixels[src+1]
 127             shrunk.Pixels[dst+2] = in.Pixels[src+2]
 128         }
 129     }
 130     return shrunk
 131 }
 132 
 133 // guessInputType tries to guess the image format using the first few bytes
 134 // bytes read from its data stream
 135 func guessImageType(start []byte) (string, bool) {
 136     if bytes.HasPrefix(start, []byte{0x89, 'P', 'N', 'G'}) {
 137         return "png", true
 138     }
 139     if bytes.HasPrefix(start, []byte{0xff, 0xd8, 0xff}) {
 140         return "jpeg", true
 141     }
 142     if seemsWEBP(start) {
 143         return "webp", true
 144     }
 145     if bytes.HasPrefix(start, []byte{'B', 'M'}) {
 146         return "bmp", true
 147     }
 148     if bytes.HasPrefix(start, []byte{'I', 'I', '*', 0}) {
 149         return "tiff", true
 150     }
 151     if bytes.HasPrefix(start, []byte{'M', 'M', 0, '*'}) {
 152         return "tiff", true
 153     }
 154     if bytes.HasPrefix(start, []byte{'G', 'I', 'F', '8', '7', 'a'}) {
 155         return "gif", true
 156     }
 157     if bytes.HasPrefix(start, []byte{'G', 'I', 'F', '8', '9', 'a'}) {
 158         return "gif", true
 159     }
 160     return "", false
 161 }
 162 
 163 // seemsWEBP simplifies control-flow for func guessImageType
 164 func seemsWEBP(start []byte) bool {
 165     if !bytes.HasPrefix(start, []byte{'R', 'I', 'F', 'F'}) {
 166         return false
 167     }
 168     if len(start) >= 12 {
 169         return bytes.HasPrefix(start[8:], []byte{'W', 'E', 'B', 'P'})
 170     }
 171     return false
 172 }

     File: vip/info.txt
   1 vip [options] [filenames...]
   2 
   3 VIew Pictures on any truecolor terminal emulator / cmd-line: you may want to
   4 pipe its results to `less -RS`, unless pics are small enough to fit in your
   5 current window size.
   6 
   7 Supported input formats
   8 
   9   - PNG (stills only)
  10   - JPEG
  11   - GIF (stills only)
  12   - BMP
  13   - TIFF
  14   - WEBP
  15 
  16 If your terminal emulator supports inline base64-encoded images, run this app
  17 with option -to=direct for best results.

     File: vip/main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "errors"
   7     "fmt"
   8     "image"
   9     "image/gif"
  10     "image/jpeg"
  11     "image/png"
  12     "io"
  13     "math"
  14     "os"
  15 
  16     "golang.org/x/image/bmp"
  17     "golang.org/x/image/tiff"
  18     "golang.org/x/image/webp"
  19 
  20     _ "embed"
  21 )
  22 
  23 // errDoneWriting is a dummy error to signal the app should quit
  24 var errDoneWriting = errors.New("no more output")
  25 
  26 //go:embed info.txt
  27 var usage string
  28 
  29 // decoders dispatches image-format names to decoder funcs
  30 var decoders = map[string]func(io.Reader) (image.Image, error){
  31     "png":  png.Decode,
  32     "jpeg": jpeg.Decode,
  33     "webp": webp.Decode,
  34     "bmp":  bmp.Decode,
  35     "tiff": tiff.Decode,
  36     "gif":  gif.Decode,
  37 }
  38 
  39 func main() {
  40     // avoid annoying unused-func warnings
  41     _ = noisyShrink
  42 
  43     if run() > 0 {
  44         os.Exit(1)
  45     }
  46 }
  47 
  48 // run returns the number of errors occurred
  49 func run() int {
  50     // f, _ := os.Create("vip.prof")
  51     // defer f.Close()
  52     // pprof.StartCPUProfile(f)
  53     // defer pprof.StopCPUProfile()
  54 
  55     cfg := parseFlags(usage)
  56     w := bufio.NewWriter(os.Stdout)
  57     defer w.Flush()
  58 
  59     // performance-profiling shows no real benefit in avoiding
  60     // the likes of fmt.Fprint, since the overwhelming share of
  61     // time is spent decoding inputs, especially when they're
  62     // beyond a few megabytes
  63 
  64     errors := 0
  65     for i, fname := range cfg.Filenames {
  66         if i > 0 {
  67             fmt.Fprint(w, "\n\n")
  68         }
  69         if len(cfg.Filenames) > 1 {
  70             fmt.Fprintf(w, "%s\n\n", fname)
  71         }
  72 
  73         err := handleFile(w, fname, cfg)
  74         if err != nil {
  75             w.Flush()
  76             fmt.Fprintf(os.Stderr, "%s: %s\n", fname, err.Error())
  77             errors++
  78         }
  79     }
  80 
  81     if len(cfg.Filenames) == 0 {
  82         err := handleReader(w, os.Stdin, cfg)
  83         if err != nil {
  84             w.Flush()
  85             fmt.Fprintln(os.Stderr, err.Error())
  86             errors++
  87         }
  88     }
  89 
  90     return errors
  91 }
  92 
  93 // handleFile defer-closes files immediately after use for func run
  94 func handleFile(w *bufio.Writer, fname string, cfg config) error {
  95     f, err := os.Open(fname)
  96     if err != nil {
  97         return err
  98     }
  99     defer f.Close()
 100     return handleReader(w, f, cfg)
 101 }
 102 
 103 // handleReader reads the input image, transforms/shrinks it, and show it
 104 // as ANSI-colored blocks on the terminal
 105 func handleReader(w *bufio.Writer, r io.Reader, cfg config) error {
 106     defer w.Flush()
 107 
 108     var start [32]byte
 109     n, err := r.Read(start[:])
 110     if err != nil {
 111         return err
 112     }
 113 
 114     kind, ok := guessImageType(start[:n])
 115     if !ok {
 116         return errors.New("unsupported image data")
 117     }
 118 
 119     dec, ok := decoders[kind]
 120     if !ok {
 121         return errors.New("unsupported image data")
 122     }
 123 
 124     // decode input data
 125     img, err := dec(io.MultiReader(bytes.NewReader(start[:n]), r))
 126     if err != nil {
 127         return err
 128     }
 129 
 130     data := ensureRGBA(img)
 131     pic := picture{
 132         Width:  uint32(img.Bounds().Dx()),
 133         Height: uint32(img.Bounds().Dy()),
 134     }
 135     pic.Pixels = make([]byte, 0, 3*pic.Width*pic.Height)
 136     // copy all pixels, ignoring their alpha values
 137     for i := 0; i < len(data.Pix); i += 4 {
 138         pic.Pixels = append(pic.Pixels, data.Pix[i+0])
 139         pic.Pixels = append(pic.Pixels, data.Pix[i+1])
 140         pic.Pixels = append(pic.Pixels, data.Pix[i+2])
 141     }
 142 
 143     // find output height by keeping the aspect-ratio of the original pic
 144     ar := float64(pic.Width) / float64(pic.Height)
 145     h := int(math.Ceil(float64(cfg.Width) / ar))
 146 
 147     // show a shrunk copy of the pic
 148     err = show(w, smoothShrink(pic, int(cfg.Width), h))
 149     if err == errDoneWriting {
 150         return nil
 151     }
 152     return err
 153 }

     File: vip/mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright © 2024 pacman64
   4 
   5 Permission is hereby granted, free of charge, to any person obtaining a copy of
   6 this software and associated documentation files (the “Software”), to deal
   7 in the Software without restriction, including without limitation the rights to
   8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
   9 of the Software, and to permit persons to whom the Software is furnished to do
  10 so, subject to the following conditions:
  11 
  12 The above copyright notice and this permission notice shall be included in all
  13 copies or substantial portions of the Software.
  14 
  15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 SOFTWARE.

     File: vip/output.go
   1 package main
   2 
   3 import "bufio"
   4 
   5 // show shows a picture as ANSI-styled full-color terminal graphics
   6 func show(w *bufio.Writer, pic picture) error {
   7     if pic.Width < 1 || pic.Height < 1 {
   8         return nil
   9     }
  10 
  11     rowWidth := 3 * pic.Width
  12     top := make([]byte, rowWidth)
  13     bottom := make([]byte, rowWidth)
  14 
  15     pixels := pic.Pixels
  16     // loop on pairs of data rows
  17     for len(pixels) >= 2*int(rowWidth) {
  18         copy(top, pixels[:rowWidth])
  19         pixels = pixels[rowWidth:]
  20         copy(bottom, pixels[:rowWidth])
  21         pixels = pixels[rowWidth:]
  22 
  23         err := writeLine(w, top, bottom)
  24         if err != nil {
  25             return err
  26         }
  27     }
  28 
  29     // don't forget the last row for pictures with an odd number of rows
  30     if len(pixels) >= int(rowWidth) {
  31         // use top/last row as is
  32         copy(top, pixels[:rowWidth])
  33         // make bottom row all black
  34         for i := 0; i < len(bottom); i++ {
  35             bottom[i] = 0
  36         }
  37         return writeLine(w, top, bottom)
  38     }
  39     return nil
  40 }
  41 
  42 // writeLine handles emitting a row/line as a combination of 2 adjacent
  43 // image rows/lines, combining them pair-wise into ANSI-styled cells
  44 func writeLine(w *bufio.Writer, top, bottom []byte) error {
  45     for len(top) >= 3 && len(bottom) >= 3 {
  46         writeDoublePixel(w,
  47             top[0], top[1], top[2],
  48             bottom[0], bottom[1], bottom[2],
  49         )
  50         top = top[3:]
  51         bottom = bottom[3:]
  52     }
  53 
  54     n, err := w.WriteString("\x1b[0m\n")
  55     if n < 1 || err != nil {
  56         return errDoneWriting
  57     }
  58     return nil
  59 }
  60 
  61 // writeDoublePixel handles combining 2 pixels on top of each other as a
  62 // single cell, using both background and foreground styles to combine them
  63 func writeDoublePixel(w *bufio.Writer, a, b, c, d, e, f byte) {
  64     // fmt.Fprintf(w, "\x1b[48;2;%d;%d;%dm", a, b, c)
  65     // fmt.Fprintf(w, "\x1b[38;2;%d;%d;%dm▄", d, e, f)
  66 
  67     w.WriteString("\x1b[48;2;")
  68     writeByteDigits(w, a)
  69     w.WriteByte(';')
  70     writeByteDigits(w, b)
  71     w.WriteByte(';')
  72     writeByteDigits(w, c)
  73     w.WriteByte('m')
  74 
  75     w.WriteString("\x1b[38;2;")
  76     writeByteDigits(w, d)
  77     w.WriteByte(';')
  78     writeByteDigits(w, e)
  79     w.WriteByte(';')
  80     writeByteDigits(w, f)
  81     w.WriteString("m▄")
  82 }
  83 
  84 // writeByteDigits emits the unpadded decimal digits of the byte given
  85 func writeByteDigits(w *bufio.Writer, b byte) {
  86     if b >= 100 {
  87         w.WriteByte(b/100 + '0')
  88         b %= 100
  89         w.WriteByte(b/10 + '0')
  90         w.WriteByte(b%10 + '0')
  91         return
  92     }
  93 
  94     if b >= 10 {
  95         w.WriteByte(b/10 + '0')
  96         w.WriteByte(b%10 + '0')
  97         return
  98     }
  99 
 100     w.WriteByte(b + '0')
 101 }