├── .gitignore ├── README ├── STYLE ├── canvas.go ├── doc.go ├── image.go ├── log.go ├── main.go ├── util.go └── win.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | imgv 3 | *.prof 4 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | A minimalistic image viewer written in Go. 2 | 3 | The list of image formats supported is currently at the mercy of whatever is 4 | supported by the Go standard library. Currently, those formats are png, jpg and 5 | gif. 6 | 7 | Please see `imgv --help` for more options. 8 | 9 | Quick Usage 10 | =========== 11 | go get github.com/BurntSushi/imgv 12 | imgv image.png image.gif image.jpg 13 | 14 | Installation 15 | ============ 16 | go get github.com/BurntSushi/imgv 17 | 18 | Dependencies 19 | ============ 20 | Go 21 | XGB 22 | xgbutil 23 | -------------------------------------------------------------------------------- /STYLE: -------------------------------------------------------------------------------- 1 | I like to keep all my code to 80 columns or less. I have plenty of screen real 2 | estate, but enjoy 80 columns so that I can have multiple code windows open side 3 | to side and not be plagued by the ugly auto-wrapping of a text editor. 4 | 5 | If you don't oblige me, I will fix any patch you submit to abide 80 columns. 6 | 7 | Note that this style restriction does not preclude gofmt, but introduces a few 8 | peculiarities. The first is that gofmt will occasionally add spacing (typically 9 | to comments) that ends up going over 80 columns. Either shorten the comment or 10 | put it on its own line. 11 | 12 | The second and more common hiccup is when a function definition extends beyond 13 | 80 columns. If one adds line breaks to keep it below 80 columns, gofmt will 14 | indent all subsequent lines in a function definition to the same indentation 15 | level of the function body. This results in a less-than-ideal separation 16 | between function definition and function body. To remedy this, simply add a 17 | line break like so: 18 | 19 | func RestackWindowExtra(xu *xgbutil.XUtil, win xproto.Window, stackMode int, 20 | sibling xproto.Window, source int) error { 21 | 22 | return ClientEvent(xu, win, "_NET_RESTACK_WINDOW", source, int(sibling), 23 | stackMode) 24 | } 25 | 26 | Something similar should also be applied to long 'if' or 'for' conditionals, 27 | although it would probably be preferrable to break up the conditional to 28 | smaller chunks with a few helper variables. 29 | 30 | -------------------------------------------------------------------------------- /canvas.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/BurntSushi/xgbutil" 8 | "github.com/BurntSushi/xgbutil/xgraphics" 9 | ) 10 | 11 | // chans is a group of channels used to communicate with the canvas goroutine. 12 | type chans struct { 13 | // imgChan is sent values whenever an image has finished loading. 14 | // An image has finished loading when its been converted to an 15 | // xgraphics.Image type, and an X pixmap with the image contents has been 16 | // created. 17 | imgChan chan imageLoaded 18 | 19 | // drawChan is sent a function that transforms the current origin point 20 | // and paints the image generated by that origin. 21 | drawChan chan func(pt image.Point) image.Point 22 | 23 | // resizeToImageChan, when pinged, will resize the window to fit the 24 | // current image exactly. 25 | resizeToImageChan chan struct{} 26 | 27 | // prevImg can be pinged to cycle to the previous image. It wraps. 28 | prevImg chan struct{} 29 | 30 | // nextImg can be pinged to cycle to the next image. It wraps. 31 | nextImg chan struct{} 32 | 33 | // imgLoadChans act as synchronization points for the image generated 34 | // goroutines. That is, an image doesn't start loading until its 35 | // corresponding channel in the imgLoadChans slice is pinged. 36 | imgLoadChans []chan struct{} 37 | 38 | // The pan{Start,Step,End}Chan types facilitate panning. They correspond 39 | // to "drag start", "drag step", and "drag end." 40 | panStartChan chan image.Point 41 | panStepChan chan image.Point 42 | panEndChan chan image.Point 43 | } 44 | 45 | // imageLoaded in the kind of value sent from each image generation goroutine 46 | // when the image has finished loading. 47 | type imageLoaded struct { 48 | img *vimage 49 | index int 50 | } 51 | 52 | // canvas is meant to be run as a single goroutine that maintains the state 53 | // of the image viewer. It manipulates state by reading values from the channels 54 | // defined in the 'chans' type. 55 | func canvas(X *xgbutil.XUtil, window *window, names []string, nimgs int) chans { 56 | imgChan := make(chan imageLoaded, 0) 57 | drawChan := make(chan func(pt image.Point) image.Point, 0) 58 | resizeToImageChan := make(chan struct{}, 0) 59 | prevImg := make(chan struct{}, 0) 60 | nextImg := make(chan struct{}, 0) 61 | 62 | imgLoadChans := make([]chan struct{}, nimgs) 63 | for i := range imgLoadChans { 64 | imgLoadChans[i] = make(chan struct{}, 0) 65 | } 66 | 67 | panStartChan := make(chan image.Point, 0) 68 | panStepChan := make(chan image.Point, 0) 69 | panEndChan := make(chan image.Point, 0) 70 | panStart, panOrigin := image.Point{}, image.Point{} 71 | 72 | chans := chans{ 73 | imgChan: imgChan, 74 | drawChan: drawChan, 75 | resizeToImageChan: resizeToImageChan, 76 | prevImg: prevImg, 77 | nextImg: nextImg, 78 | 79 | imgLoadChans: imgLoadChans, 80 | 81 | panStartChan: panStartChan, 82 | panStepChan: panStepChan, 83 | panEndChan: panEndChan, 84 | } 85 | 86 | imgs := make([]*vimage, nimgs) 87 | window.setupEventHandlers(chans) 88 | current := 0 89 | origin := image.Point{0, 0} 90 | 91 | setImage := func(i int, pt image.Point) { 92 | if i >= len(imgs) { 93 | i = 0 94 | } 95 | if i < 0 { 96 | i = len(imgs) - 1 97 | } 98 | if current != i { 99 | window.ClearAll() 100 | } 101 | 102 | current = i 103 | if imgs[i] == nil { 104 | window.nameSet(fmt.Sprintf("%s - Loading...", names[i])) 105 | 106 | if imgLoadChans[i] != nil { 107 | imgLoadChans[i] <- struct{}{} 108 | imgLoadChans[i] = nil 109 | } 110 | return 111 | } 112 | 113 | origin = originTrans(pt, window, imgs[current]) 114 | show(window, imgs[i], origin) 115 | } 116 | 117 | go func() { 118 | for { 119 | select { 120 | case img := <-imgChan: 121 | imgs[img.index] = img.img 122 | 123 | // If this is the current image, show it! 124 | if current == img.index { 125 | show(window, imgs[current], origin) 126 | } 127 | case funpt := <-drawChan: 128 | setImage(current, funpt(origin)) 129 | case <-resizeToImageChan: 130 | window.Resize(imgs[current].Bounds().Dx(), 131 | imgs[current].Bounds().Dy()) 132 | case <-prevImg: 133 | setImage(current-1, image.Point{0, 0}) 134 | case <-nextImg: 135 | setImage(current+1, image.Point{0, 0}) 136 | case pt := <-panStartChan: 137 | panStart = pt 138 | panOrigin = origin 139 | case pt := <-panStepChan: 140 | xd, yd := panStart.X-pt.X, panStart.Y-pt.Y 141 | setImage(current, 142 | image.Point{xd + panOrigin.X, yd + panOrigin.Y}) 143 | case <-panEndChan: 144 | panStart, panOrigin = image.Point{}, image.Point{} 145 | } 146 | } 147 | }() 148 | 149 | return chans 150 | } 151 | 152 | // originTrans translates the origin with respect to the current image and the 153 | // current canvas size. This makes sure we never incorrect position the image. 154 | // (i.e., panning never goes too far, and whenever the canvas is bigger than 155 | // the image, the origin is *always* (0, 0). 156 | func originTrans(pt image.Point, win *window, img *vimage) image.Point { 157 | // If there's no valid image, then always return (0, 0). 158 | if img == nil { 159 | return image.Point{0, 0} 160 | } 161 | 162 | // Quick aliases. 163 | ww, wh := win.Geom.Width(), win.Geom.Height() 164 | dw := img.Bounds().Dx() - ww 165 | dh := img.Bounds().Dy() - wh 166 | 167 | // Set the allowable range of the origin point of the image. 168 | // i.e., never less than (0, 0) and never greater than the width/height 169 | // of the image that isn't viewable at any given point (which is determined 170 | // by the canvas size). 171 | pt.X = min(img.Bounds().Min.X+dw, max(pt.X, 0)) 172 | pt.Y = min(img.Bounds().Min.Y+dh, max(pt.Y, 0)) 173 | 174 | // Validate origin point. If the width/height of an image is smaller than 175 | // the canvas width/height, then the image origin cannot change in x/y 176 | // direction. 177 | if img.Bounds().Dx() < ww { 178 | pt.X = 0 179 | } 180 | if img.Bounds().Dy() < wh { 181 | pt.Y = 0 182 | } 183 | 184 | return pt 185 | } 186 | 187 | // show translates the given origin point, paints the appropriate part of the 188 | // current image to the canvas, and sets the name of the window. 189 | // (Painting only paints the sub-image that is viewable.) 190 | func show(win *window, img *vimage, pt image.Point) { 191 | // If there's no valid image, don't bother trying to show it. 192 | // (We're hopefully loading the image now.) 193 | if img == nil { 194 | return 195 | } 196 | 197 | // Translate the origin to reflect the size of the image and canvas. 198 | pt = originTrans(pt, win, img) 199 | 200 | // Now paint the sub-image to the window. 201 | win.paint(img.SubImage(image.Rect(pt.X, pt.Y, 202 | pt.X+win.Geom.Width(), pt.Y+win.Geom.Height())).(*xgraphics.Image)) 203 | 204 | // Always set the name of the window when we update it with a new image. 205 | win.nameSet(fmt.Sprintf("%s (%dx%d)", 206 | img.name, img.Bounds().Dx(), img.Bounds().Dy())) 207 | } 208 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | imgv is a simple image viewer that only works with X and is written in Go. It 3 | supports image formats that can be decoded by the Go standard library 4 | (currently jpeg, gif and png). It supports panning the image but does not (yet) 5 | support zooming. 6 | 7 | Usage: 8 | imgv [flags] image-file [image-file ...] 9 | 10 | The flags are: 11 | --height pixels, --width pixels 12 | The 'height' and 'width' flags allow one to specify the initial size 13 | of the image window. The image window can still change size afterwards. 14 | --auto-resize 15 | If set, the image window will be automatically resized to the first 16 | image displayed. This overrides the 'height' and 'width' options. 17 | --increment pixels 18 | The amount of pixels to pan an image at each step when using the 19 | keyboard shortcuts. 20 | --keybindings 21 | If set, a list of all key bindings (and mouse bindings) set by imgv is 22 | printed. A small description of what each key binding does is included. 23 | -v 24 | If set, more output will be printed to stderr. Useful for debugging. 25 | --profile prof-file-name 26 | If set, a CPU profile will be saved to prof-file-name. This is for 27 | development purposes only. 28 | 29 | Details 30 | 31 | imgv is about as simple as it gets for an image viewer. It only supports 32 | displaying the image and panning around the image when parts of it are not 33 | viewable. It does not support zooming or any kind of image manipulation. 34 | 35 | My two primary future goals are to support zooming and to increase 36 | performance. (I'll rely on the Go standard library to write new image format 37 | decoders). 38 | 39 | I didn't include zooming in the initial release because it adds a surprising 40 | amount of complexity and has broad-sweeping performance implications depending 41 | upon its implementation. 42 | 43 | High-level overview 44 | 45 | imgv starts up by attempting to decode all images specified on the command 46 | line. After all images are decoded, the first image is converted to an 47 | xgbutil/xgraphics.Image type and drawn on to an X pixmap. At this point, the 48 | first image is then painted to the window. 49 | 50 | When the next image is requested to be displayed, it is then converted to an 51 | xgbutil/xgraphics.Image type and drawn to an X pixmap on demand. Then it is 52 | painted to the window. 53 | 54 | Performance 55 | 56 | It has a somewhat concurrent design, and will benefit from parallelism 57 | (particularly at startup). Also, the underlying library used (XGB) benefits 58 | from parallelism. 59 | 60 | The high-level overview given above may sound a bit weird (i.e., why decode all 61 | images before showing the first?). My reason is that this was the quickest and 62 | simplest way to get something working, since decoding an image has a reasonable 63 | chance of failure. (There is additional complexity involved in handling failure 64 | at the concurrent level.) Decoding will take advantage of parallelism and is 65 | typically fairly quick (unless a lot of images are specified). 66 | 67 | Perhaps the biggest performance implication is what is done on-demand when a 68 | new image must be loaded. If it has already been converted and painted to an X 69 | pixmap, this process is nearly instant. If its the first loading, then it must 70 | be converted to an xgbutil/xgraphics.Image type and drawn to an X pixmap before 71 | it can be painted to a window. 72 | 73 | Conversion to the xgbutil/xgraphics.Image type is, by far, the bottleneck. The 74 | process includes transforming every pixel in the decoded image to the correct 75 | image byte order (currently BGRA), which is the format expected by X (in common 76 | configurations). While this is fairly quick for small images, it can be quite 77 | slow for larger images. 78 | 79 | The ideal solution, assuming image conversion itself cannot be sped up, seems 80 | to be to process image conversions in the background with the hope that they 81 | will be ready (or close to ready) when they are requested. The big problem with 82 | this approach is when a lot of images are specified. What if the image 83 | requested by the user won't even start loading for a long time because other 84 | image conversions are hogging the CPU? I'm not sure how to solve that, other 85 | than perhaps an ugly hack using runtime.Gosched. 86 | 87 | Another direction that could be taken is to only convert the pieces of the 88 | image that are being displayed. This relies on the fact that most setups cannot 89 | view the entirety of a large image (> 2,500,000 pixels) at one time. This comes 90 | at the cost of increased complexity but is probably the most performant 91 | solution. (The complexity lay in splitting conversion up into pieces, and 92 | triggering the appropriate conversion when the image is panned.) 93 | 94 | As for drawing the image to an X pixmap, I was surprised to see that this was 95 | fairly quick by comparison. It uses Go's built in copy function, which I 96 | suspect is the source of its speediness. 97 | 98 | Zooming 99 | 100 | Zooming into an image can be more precisely described as increasing the scale 101 | of an image (zoom in) and decreasing the scale of an image (zoom out). 102 | 103 | Zooming adds some complexity to the design of imgv, as it requires representing 104 | each image as a set of images, and keeping state to determine which image in 105 | the set is currently viewable. (Where each image in the set corresponds to a 106 | different scaling level.) 107 | 108 | While complexity is a reasonable barrier, the bigger barrier is the performance 109 | implications. Namely, zooming in exacerbates the performance problems described 110 | in the section above. It turns a sort-of extreme case (large images) into a 111 | common occurrence. 112 | 113 | It would appear that the only viable means of implementing scaling is to only 114 | scale the part of the image that can be viewed. (Scaling seems to be bounded by 115 | the scale rather than the size of original image. On my machine, an Intel Core 116 | 2 Duo, a 1000x1000 scale takes on the order of a second or two. Which is slow.) 117 | This is probably the only choice not just in the interest in keeping things 118 | speedy, but also for memory usage. (Keeping several different scaled and 119 | complete versions of a large image can use a ton of memory. With only a few 120 | images like this, memory usage adds up quickly.) 121 | 122 | Perhaps another option is write a scaling routine that optimizes the use of 123 | interfaces out of the performance critical sections. Doing this for image 124 | conversion achieved 50-80% speed ups. (I don't think graphics-go does this 125 | currently.) 126 | 127 | Portability 128 | 129 | Obviously, the image viewer will only work with an X server. There are no plans 130 | to change this. 131 | 132 | More interesting is portability among X servers. While imgv is itself portable 133 | across any X server, the underlying library (xgbutil/xgraphics) is not quite 134 | there yet. Namely, xgbutil/xgraphics assumes a BGRx format (24 bit depth with 135 | 32 bytes per pixel and a least significant image byte order). This is wrong and 136 | needs to be more flexible to fit any X server configuration. 137 | 138 | If your X server doesn't have the configuration expected by xgbutil/xgraphics, 139 | you should see some messages emitted to stderr. If you do see this, I'd greatly 140 | appreciate a bug report filed at the xgbutil project page with the messages 141 | that you see: 142 | https://github.com/BurntSushi/xgbutil. 143 | 144 | Author 145 | 146 | I have never developed an image viewer before, and I'm pretty sure I've never 147 | looked at the source code of another image viewer. Therefore, it's quite likely 148 | that I'm stumbling over solved problems. (Are they solved in image viewers or 149 | GUI toolkits?) 150 | 151 | */ 152 | package main 153 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "time" 6 | 7 | "github.com/BurntSushi/xgbutil" 8 | "github.com/BurntSushi/xgbutil/xgraphics" 9 | ) 10 | 11 | // vimage acts as an xgraphics.Image type with a name. 12 | // (The name is the basename of the image's corresponding file name.) 13 | type vimage struct { 14 | *xgraphics.Image 15 | name string 16 | } 17 | 18 | // newImage is meant to be run as a goroutine and loads a decoded image into 19 | // an xgraphics.Image value and draws it to an X pixmap. 20 | // The loading doesn't start until this image's corresponding imgLoadChan 21 | // has been pinged. 22 | // This implies that all images are decoded on start-up and are converted 23 | // and drawn to an X pixmap on-demand. I am still deliberating on whether this 24 | // is a smart decision. 25 | // Note that this process, particularly image conversion, can be quite 26 | // costly for large images. 27 | func newImage(X *xgbutil.XUtil, name string, img image.Image, index int, 28 | imgLoadChan chan struct{}, imgChan chan imageLoaded) { 29 | 30 | // Don't start loading until we're told to do so. 31 | <-imgLoadChan 32 | 33 | // We send this when we're done processing this image, whether its 34 | // an error or not. 35 | loaded := imageLoaded{index: index} 36 | 37 | start := time.Now() 38 | reg := xgraphics.NewConvert(X, img) 39 | lg("Converted '%s' to an xgraphics.Image type (%s).", 40 | name, time.Since(start)) 41 | 42 | // Only blend a checkered background if the image *may* have an alpha 43 | // channel. If we want to be a bit more efficient, we could type switch 44 | // on all image types use Opaque, but this may add undesirable overhead. 45 | // (Where the overhead is scanning the image for opaqueness.) 46 | switch img.(type) { 47 | case *image.Gray: 48 | case *image.Gray16: 49 | case *image.YCbCr: 50 | default: 51 | start = time.Now() 52 | blendCheckered(reg) 53 | lg("Blended '%s' into a checkered background (%s).", 54 | name, time.Since(start)) 55 | } 56 | 57 | if err := reg.CreatePixmap(); err != nil { 58 | // TODO: We should display a "Could not load image" image instead 59 | // of dying. However, creating a pixmap rarely fails, unless we have 60 | // a *ton* of images. (In all likelihood, we'll run out of memory 61 | // before a new pixmap cannot be created.) 62 | errLg.Fatal(err) 63 | } else { 64 | start = time.Now() 65 | reg.XDraw() 66 | lg("Drawn '%s' to an X pixmap (%s).", name, time.Since(start)) 67 | } 68 | 69 | loaded.img = &vimage{ 70 | Image: reg, 71 | name: name, 72 | } 73 | 74 | // Tell the canvas that this image has been loaded. 75 | imgChan <- loaded 76 | } 77 | 78 | // blendCheckered is basically a copy of xgraphics.Blend with no interfaces. 79 | // (It's faster.) Also, it is hardcoded to blend into a checkered background. 80 | func blendCheckered(dest *xgraphics.Image) { 81 | dsrc := dest.Bounds() 82 | dmnx, dmxx, dmny, dmxy := dsrc.Min.X, dsrc.Max.X, dsrc.Min.Y, dsrc.Max.Y 83 | 84 | clr1 := xgraphics.BGRA{B: 0xff, G: 0xff, R: 0xff, A: 0xff} 85 | clr2 := xgraphics.BGRA{B: 0xde, G: 0xdc, R: 0xdf, A: 0xff} 86 | 87 | var dx, dy int 88 | var bgra, clr xgraphics.BGRA 89 | for dx = dmnx; dx < dmxx; dx++ { 90 | for dy = dmny; dy < dmxy; dy++ { 91 | if dx%30 >= 15 { 92 | if dy%30 >= 15 { 93 | clr = clr1 94 | } else { 95 | clr = clr2 96 | } 97 | } else { 98 | if dy%30 >= 15 { 99 | clr = clr2 100 | } else { 101 | clr = clr1 102 | } 103 | } 104 | 105 | bgra = dest.At(dx, dy).(xgraphics.BGRA) 106 | dest.SetBGRA(dx, dy, xgraphics.BlendBGRA(clr, bgra)) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var errLg = log.New(os.Stderr, "[imgv error] ", log.Lshortfile) 9 | 10 | // lg is a convenient alias for printing verbose output. 11 | func lg(format string, v ...interface{}) { 12 | if !flagVerbose { 13 | return 14 | } 15 | log.Printf(format, v...) 16 | } 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | _ "image/gif" 8 | _ "image/jpeg" 9 | _ "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "runtime/pprof" 15 | "time" 16 | 17 | "github.com/BurntSushi/xgbutil" 18 | "github.com/BurntSushi/xgbutil/xevent" 19 | ) 20 | 21 | var ( 22 | // When flagVerbose is true, logging output will be written to stderr. 23 | // Errors will always be written to stderr. 24 | flagVerbose bool 25 | 26 | // The initial width and height of the window. 27 | flagWidth, flagHeight int 28 | 29 | // If set, the image window will automatically resize to the first image 30 | // that it displays. 31 | flagAutoResize bool 32 | 33 | // The amount to increment panning when using h,j,k,l 34 | flagStepIncrement int 35 | 36 | // Whether to run a CPU profile. 37 | flagProfile string 38 | 39 | // When set, imgv will print all keybindings and exit. 40 | flagKeybindings bool 41 | 42 | // A list of keybindings. Each value corresponds to a triple of the key 43 | // sequence to bind to, the action to run when that key sequence is 44 | // pressed and a quick description of what the keybinding does. 45 | keybinds = []keyb{ 46 | { 47 | "left", "Cycle to the previous image.", 48 | func(w *window) { w.chans.prevImg <- struct{}{} }, 49 | }, 50 | { 51 | "right", "Cycle to the next image.", 52 | func(w *window) { w.chans.nextImg <- struct{}{} }, 53 | }, 54 | { 55 | "shift-h", "Cycle to the previous image.", 56 | func(w *window) { w.chans.prevImg <- struct{}{} }, 57 | }, 58 | { 59 | "shift-l", "Cycle to the next image.", 60 | func(w *window) { w.chans.nextImg <- struct{}{} }, 61 | }, 62 | { 63 | "r", "Resize the window to fit the current image.", 64 | func(w *window) { w.chans.resizeToImageChan <- struct{}{} }, 65 | }, 66 | { 67 | "h", "Pan left.", func(w *window) { w.stepLeft() }, 68 | }, 69 | { 70 | "j", "Pan down.", func(w *window) { w.stepDown() }, 71 | }, 72 | { 73 | "k", "Pan up.", func(w *window) { w.stepUp() }, 74 | }, 75 | { 76 | "l", "Pan right.", func(w *window) { w.stepRight() }, 77 | }, 78 | { 79 | "q", "Quit.", func(w *window) { xevent.Quit(w.X) }, 80 | }, 81 | } 82 | ) 83 | 84 | func init() { 85 | // Set GOMAXPROCS, since imgv can benefit greatly from parallelism. 86 | runtime.GOMAXPROCS(runtime.NumCPU()) 87 | 88 | // Set the prefix for verbose output. 89 | log.SetPrefix("[imgv] ") 90 | 91 | // Set all of the flags. 92 | flag.BoolVar(&flagVerbose, "v", false, 93 | "If set, logging output will be printed to stderr.") 94 | flag.IntVar(&flagWidth, "width", 600, 95 | "The initial width of the window.") 96 | flag.IntVar(&flagHeight, "height", 600, 97 | "The initial height of the window.") 98 | flag.BoolVar(&flagAutoResize, "auto-resize", false, 99 | "If set, window will resize to size of first image.") 100 | flag.IntVar(&flagStepIncrement, "increment", 20, 101 | "The increment (in pixels) used to pan the image.") 102 | flag.StringVar(&flagProfile, "profile", "", 103 | "If set, a CPU profile will be saved to the file name provided.") 104 | flag.BoolVar(&flagKeybindings, "keybindings", false, 105 | "If set, imgv will output a list all keybindings.") 106 | flag.Usage = usage 107 | flag.Parse() 108 | 109 | // Do some error checking on the flag values... naughty! 110 | if flagWidth == 0 || flagHeight == 0 { 111 | errLg.Fatal("The width and height must be non-zero values.") 112 | } 113 | } 114 | 115 | func usage() { 116 | fmt.Fprintf(os.Stderr, "Usage: %s [flags] image-file [image-file ...]\n", 117 | basename(os.Args[0])) 118 | flag.PrintDefaults() 119 | os.Exit(1) 120 | } 121 | 122 | func main() { 123 | // If we just need the keybindings, print them and be done. 124 | if flagKeybindings { 125 | for _, keyb := range keybinds { 126 | fmt.Printf("%-10s %s\n", keyb.key, keyb.desc) 127 | } 128 | fmt.Printf("%-10s %s\n", "mouse", 129 | "Left mouse button will pan the image.") 130 | os.Exit(0) 131 | } 132 | 133 | // Run the CPU profile if we're instructed to. 134 | if len(flagProfile) > 0 { 135 | f, err := os.Create(flagProfile) 136 | if err != nil { 137 | errLg.Fatal(err) 138 | } 139 | pprof.StartCPUProfile(f) 140 | defer pprof.StopCPUProfile() 141 | } 142 | 143 | // Whoops! 144 | if flag.NArg() == 0 { 145 | fmt.Fprint(os.Stderr, "\n") 146 | errLg.Print("No images specified.\n\n") 147 | usage() 148 | } 149 | 150 | // Connect to X and quit if we fail. 151 | X, err := xgbutil.NewConn() 152 | if err != nil { 153 | errLg.Fatal(err) 154 | } 155 | 156 | // Create the X window before starting anything so that the user knows 157 | // something is going on. 158 | window := newWindow(X) 159 | 160 | // Decode all images (in parallel). 161 | names, imgs := decodeImages(findFiles(flag.Args())) 162 | 163 | // Die now if we don't have any images! 164 | if len(imgs) == 0 { 165 | errLg.Fatal("No images specified could be shown. Quitting...") 166 | } 167 | 168 | // Auto-size the window if appropriate. 169 | if flagAutoResize { 170 | window.Resize(imgs[0].Bounds().Dx(), imgs[0].Bounds().Dy()) 171 | } 172 | 173 | // Create the canvas and start the image goroutines. 174 | chans := canvas(X, window, names, len(imgs)) 175 | for i, img := range imgs { 176 | go newImage(X, names[i], img, i, chans.imgLoadChans[i], chans.imgChan) 177 | } 178 | 179 | // Start the main X event loop. 180 | xevent.Main(X) 181 | } 182 | 183 | func findFiles(args []string) []string { 184 | files := []string{} 185 | for _, f := range args { 186 | fi, err := os.Stat(f) 187 | if err != nil { 188 | errLg.Print("Can't access", f, err) 189 | } else if fi.IsDir() { 190 | files = append(files, dirImages(f)...) 191 | } else { 192 | files = append(files, f) 193 | } 194 | } 195 | return files 196 | } 197 | 198 | func dirImages(dir string) []string { 199 | 200 | fd, _ := os.Open(dir) 201 | fs, _ := fd.Readdirnames(0) 202 | files := []string{} 203 | for _, f := range fs { 204 | // TODO filter by regexp 205 | if filepath.Ext(f) != "" { 206 | files = append(files, filepath.Join(dir, f)) 207 | } 208 | } 209 | return files 210 | } 211 | 212 | // decodeImages takes a list of image files and decodes them into image.Image 213 | // types. Note that the number of images returned may not be the number of 214 | // image files passed in. Namely, an image file is skipped if it cannot be 215 | // read or deocoded into an image type that Go understands. 216 | func decodeImages(imageFiles []string) ([]string, []image.Image) { 217 | // A temporary type used to transport decoded images over channels. 218 | type tmpImage struct { 219 | img image.Image 220 | name string 221 | } 222 | 223 | // Decoded all images specified in parallel. 224 | imgChans := make([]chan tmpImage, len(imageFiles)) 225 | for i, fName := range imageFiles { 226 | imgChans[i] = make(chan tmpImage, 0) 227 | go func(i int, fName string) { 228 | file, err := os.Open(fName) 229 | if err != nil { 230 | errLg.Println(err) 231 | close(imgChans[i]) 232 | return 233 | } 234 | 235 | start := time.Now() 236 | img, kind, err := image.Decode(file) 237 | if err != nil { 238 | errLg.Printf("Could not decode '%s' into a supported image "+ 239 | "format: %s", fName, err) 240 | close(imgChans[i]) 241 | return 242 | } 243 | lg("Decoded '%s' into image type '%s' (%s).", 244 | fName, kind, time.Since(start)) 245 | 246 | imgChans[i] <- tmpImage{ 247 | img: img, 248 | name: basename(fName), 249 | } 250 | }(i, fName) 251 | } 252 | 253 | // Now collect all the decoded images into a slice of names and a slice 254 | // of images. 255 | names := make([]string, 0, flag.NArg()) 256 | imgs := make([]image.Image, 0, flag.NArg()) 257 | for _, imgChan := range imgChans { 258 | if tmpImg, ok := <-imgChan; ok { 259 | names = append(names, tmpImg.name) 260 | imgs = append(imgs, tmpImg.img) 261 | } 262 | } 263 | 264 | return names, imgs 265 | } 266 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "strings" 6 | 7 | "github.com/BurntSushi/xgbutil/xgraphics" 8 | ) 9 | 10 | // vpCenter inspects the canvas and image geometry, and determines where the 11 | // origin of the image should be painted into the canvas. 12 | // If the image is bigger than the canvas, this is always (0, 0). 13 | // If the image is the same size, then it is also (0, 0). 14 | // If a dimension of the image is smaller than the canvas, then: 15 | // x = (canvas_width - image_width) / 2 and 16 | // y = (canvas_height - image_height) / 2 17 | func vpCenter(ximg *xgraphics.Image, canWidth, canHeight int) image.Point { 18 | xmargin, ymargin := 0, 0 19 | if ximg.Bounds().Dx() < canWidth { 20 | xmargin = (canWidth - ximg.Bounds().Dx()) / 2 21 | } 22 | if ximg.Bounds().Dy() < canHeight { 23 | ymargin = (canHeight - ximg.Bounds().Dy()) / 2 24 | } 25 | return image.Point{xmargin, ymargin} 26 | } 27 | 28 | // basename retrieves the basename of a file path. 29 | func basename(fName string) string { 30 | if lslash := strings.LastIndex(fName, "/"); lslash != -1 { 31 | fName = fName[lslash+1:] 32 | } 33 | return fName 34 | } 35 | 36 | func min(a, b int) int { 37 | if a < b { 38 | return a 39 | } 40 | return b 41 | } 42 | 43 | func max(a, b int) int { 44 | if a > b { 45 | return a 46 | } 47 | return b 48 | } 49 | -------------------------------------------------------------------------------- /win.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | 9 | "github.com/BurntSushi/xgbutil" 10 | "github.com/BurntSushi/xgbutil/ewmh" 11 | "github.com/BurntSushi/xgbutil/icccm" 12 | "github.com/BurntSushi/xgbutil/keybind" 13 | "github.com/BurntSushi/xgbutil/mousebind" 14 | "github.com/BurntSushi/xgbutil/xevent" 15 | "github.com/BurntSushi/xgbutil/xgraphics" 16 | "github.com/BurntSushi/xgbutil/xwindow" 17 | ) 18 | 19 | // keyb represents a value in the keybinding list. Namely, it contains the 20 | // function to run when a particular key sequence has been pressed, the 21 | // key sequence to bind to, and a quick description of what the keybinding 22 | // actually does. 23 | type keyb struct { 24 | key string 25 | desc string 26 | action func(w *window) 27 | } 28 | 29 | // window embeds an xwindow.Window value and all available channels used to 30 | // communicate with the canvas. 31 | // While the canvas and the window are essentialy the same, the canvas 32 | // focuses on the abstraction of drawing some image into a viewport while the 33 | // window focuses on the more X related aspects of setting up the canvas. 34 | type window struct { 35 | *xwindow.Window 36 | chans chans 37 | } 38 | 39 | // newWndow creates a new window and dies on failure. 40 | // This includes mapping the window but not setting up the event handlers. 41 | // (The event handlers require the channels, and we don't create the channels 42 | // until all images have been decoded. But we want to show the window to the 43 | // user before that task is complete.) 44 | func newWindow(X *xgbutil.XUtil) *window { 45 | xwin, err := xwindow.Generate(X) 46 | if err != nil { 47 | errLg.Fatalf("Could not create window: %s", err) 48 | } 49 | 50 | w := &window{ 51 | Window: xwin, 52 | } 53 | w.create() 54 | 55 | return w 56 | } 57 | 58 | // create creates the window, initializes the keybind and mousebind packages 59 | // and sets up the window to act like a real top-level client. 60 | func (w *window) create() { 61 | keybind.Initialize(w.X) 62 | mousebind.Initialize(w.X) 63 | 64 | err := w.CreateChecked(w.X.RootWin(), 0, 0, flagWidth, flagHeight, 65 | xproto.CwBackPixel, 0xffffff) 66 | if err != nil { 67 | errLg.Fatalf("Could not create window: %s", err) 68 | } 69 | 70 | // Make the window close gracefully using the WM_DELETE_WINDOW protocol. 71 | w.WMGracefulClose(func(w *xwindow.Window) { 72 | xevent.Detach(w.X, w.Id) 73 | keybind.Detach(w.X, w.Id) 74 | mousebind.Detach(w.X, w.Id) 75 | w.Destroy() 76 | xevent.Quit(w.X) 77 | }) 78 | 79 | // Set WM_STATE so it is interpreted as top-level and is mapped. 80 | err = icccm.WmStateSet(w.X, w.Id, &icccm.WmState{ 81 | State: icccm.StateNormal, 82 | }) 83 | if err != nil { // not a fatal error 84 | lg("Could not set WM_STATE: %s", err) 85 | } 86 | 87 | // _NET_WM_STATE = _NET_WM_STATE_NORMAL 88 | ewmh.WmStateSet(w.X, w.Id, []string{"_NET_WM_STATE_NORMAL"}) 89 | 90 | // Set the name to something. 91 | w.nameSet("Decoding all images...") 92 | 93 | w.Map() 94 | } 95 | 96 | // stepLeft moves the origin of the image to the left. 97 | func (w *window) stepLeft() { 98 | w.chans.drawChan <- func(origin image.Point) image.Point { 99 | return image.Point{origin.X - flagStepIncrement, origin.Y} 100 | } 101 | } 102 | 103 | // stepRight moves the origin of the image to the right. 104 | func (w *window) stepRight() { 105 | w.chans.drawChan <- func(origin image.Point) image.Point { 106 | return image.Point{origin.X + flagStepIncrement, origin.Y} 107 | } 108 | } 109 | 110 | // stepUp moves the origin of the image down (this would be up, but X origins 111 | // are in the top-left corner). 112 | func (w *window) stepUp() { 113 | w.chans.drawChan <- func(origin image.Point) image.Point { 114 | return image.Point{origin.X, origin.Y - flagStepIncrement} 115 | } 116 | } 117 | 118 | // stepDown moves the origin of the image up (this would be down, but X origins 119 | // are in the top-left corner). 120 | func (w *window) stepDown() { 121 | w.chans.drawChan <- func(origin image.Point) image.Point { 122 | return image.Point{origin.X, origin.Y + flagStepIncrement} 123 | } 124 | } 125 | 126 | // paint uses the xgbutil/xgraphics package to copy the area corresponding 127 | // to ximg in its pixmap to the window. It will also issue a clear request 128 | // before hand to try and avoid artifacts. 129 | func (w *window) paint(ximg *xgraphics.Image) { 130 | dst := vpCenter(ximg, w.Geom.Width(), w.Geom.Height()) 131 | // UUU Commenting this out avoids flickering, and I see no artifacts! 132 | // w.ClearAll() 133 | ximg.XExpPaint(w.Id, dst.X, dst.Y) 134 | } 135 | 136 | // nameSet will set the name of the window and emit a benign message to 137 | // verbose output if it fails. 138 | func (w *window) nameSet(name string) { 139 | // Set _NET_WM_NAME so it looks nice. 140 | err := ewmh.WmNameSet(w.X, w.Id, fmt.Sprintf("imgv :: %s", name)) 141 | if err != nil { // not a fatal error 142 | lg("Could not set _NET_WM_NAME: %s", err) 143 | } 144 | } 145 | 146 | // setupEventHandlers attaches the canvas' channels to the window and 147 | // sets the appropriate callbacks to some events: 148 | // ConfigureNotify events will cause the window to update its state of geometry. 149 | // Expose events will cause the window to repaint the current image. 150 | // Button events to allow panning. 151 | // Key events to perform various tasks when certain keys are pressed. Should 152 | // these be configurable? Meh. 153 | func (w *window) setupEventHandlers(chans chans) { 154 | w.chans = chans 155 | w.Listen(xproto.EventMaskStructureNotify | xproto.EventMaskExposure | 156 | xproto.EventMaskButtonPress | xproto.EventMaskButtonRelease | 157 | xproto.EventMaskKeyPress) 158 | 159 | // Get the current geometry in case we don't get a ConfigureNotify event 160 | // (or have already missed it). 161 | _, err := w.Geometry() 162 | if err != nil { 163 | errLg.Fatal(err) 164 | } 165 | 166 | // And ask the canvas to draw the first image when it gets around to it. 167 | go func() { 168 | w.chans.drawChan <- func(origin image.Point) image.Point { 169 | return image.Point{} 170 | } 171 | }() 172 | 173 | // Keep a state of window geometry. 174 | xevent.ConfigureNotifyFun( 175 | func(X *xgbutil.XUtil, ev xevent.ConfigureNotifyEvent) { 176 | w.Geom.WidthSet(int(ev.Width)) 177 | w.Geom.HeightSet(int(ev.Height)) 178 | }).Connect(w.X, w.Id) 179 | 180 | // Repaint the window on expose events. 181 | xevent.ExposeFun( 182 | func(X *xgbutil.XUtil, ev xevent.ExposeEvent) { 183 | w.chans.drawChan <- func(origin image.Point) image.Point { 184 | return origin 185 | } 186 | }).Connect(w.X, w.Id) 187 | 188 | // Setup a drag handler to allow panning. 189 | mousebind.Drag(w.X, w.Id, w.Id, "1", false, 190 | func(X *xgbutil.XUtil, rx, ry, ex, ey int) (bool, xproto.Cursor) { 191 | w.chans.panStartChan <- image.Point{ex, ey} 192 | return true, 0 193 | }, 194 | func(X *xgbutil.XUtil, rx, ry, ex, ey int) { 195 | w.chans.panStepChan <- image.Point{ex, ey} 196 | }, 197 | func(X *xgbutil.XUtil, rx, ry, ex, ey int) { 198 | w.chans.panEndChan <- image.Point{ex, ey} 199 | }) 200 | 201 | // Set up a map of keybindings to avoid a lot of boiler plate. 202 | // for keystring, fun := range kbs { 203 | for _, keyb := range keybinds { 204 | keyb := keyb 205 | err := keybind.KeyPressFun( 206 | func(X *xgbutil.XUtil, ev xevent.KeyPressEvent) { 207 | keyb.action(w) 208 | }).Connect(w.X, w.Id, keyb.key, false) 209 | if err != nil { 210 | errLg.Println(err) 211 | } 212 | } 213 | } 214 | --------------------------------------------------------------------------------