/* A lightweight PNG viewer using libpng (and * xshmu/yeso). It runs in about 16 ms user+system time on my machine, * on small images, without zooming. * * The display(1) program from ImageMagick on the same machine takes * 430 ms on the same laptop to reach a quiescent state, according to * strace -tt. This program takes 107 ms by the same measure. * LinuxMint’s XViewer takes several seconds. This is probably a * somewhat useful *relative* indication of how long it takes each of * the three programs to spin up and do some useful work, but it isn’t * a reasonable absolute measurement or even a very precise relative * one because of the overhead introduced by strace. * * I don’t have a very good way of measuring these times, but this * program sure does feel faster. * * For very large images, like this 9.7 megapixel digital camera image * I have here, this program slows down substantially, with a * bottleneck of only about 13 megapixels per second. This is about * twice as fast as XViewer or eog and three times as fast as * ImageMagick. */ #include #include #include #include "xshmu.h" xshmu_pic read_png(char *filename) { xshmu_pic canvas = {0}; FILE *fp = fopen(filename, "rb"); if (!fp) { perror(filename); /* XXX not a good thing to do in a library function */ return canvas; } char *err = "png_create_read_struct failed"; png_infop info_ptr = 0; png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if (!png_ptr) goto fail; err = "png_create_info_struct failed"; info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) goto fail; if (setjmp(png_jmpbuf(png_ptr))) { err = "failure in libpng"; goto fail; } png_init_io(png_ptr, fp); png_set_palette_to_rgb(png_ptr); png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER); // Ensure there is an alpha channel int transforms = PNG_TRANSFORM_BGR | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_GRAY_TO_RGB; png_read_png(png_ptr, info_ptr, transforms, 0); /* XXX probably the ideal thing would be to allocate the canvas first, then pass in row pointers to a libpng function to avoid the extra copy below */ png_bytep *row_pointers = png_get_rows(png_ptr, info_ptr); int width = png_get_image_width(png_ptr, info_ptr), height = png_get_image_height(png_ptr, info_ptr), rowbytes = png_get_rowbytes(png_ptr, info_ptr); /* XXX how do we free row_pointers and the data they point to? Does * `png_destroy_read_struct` take care of that? */ canvas = xshmu_canvas(width, height); for (int y = 0; y < height; y++) { memcpy(xshmu_pix(canvas, 0, y), row_pointers[y], rowbytes); } err = 0; fail: if (err) fprintf(stderr, "%s\n", err); if (png_ptr) png_destroy_read_struct(&png_ptr, &info_ptr, png_infopp_NULL); fclose(fp); return canvas; } // XXX C&P from tetris.c static inline void fill(xshmu_pic pic, uint32_t color) { for (int i = 0; i < pic.h; i++) { uint32_t *p = xshmu_pix(pic, 0, i); for (int j = 0; j < pic.w; j++) p[j] = color; } } xshmu_pic reduce2x(xshmu_pic big) { xshmu_pic small = xshmu_canvas(big.w / 2, big.h / 2); for (int y = 1; y < big.h; y += 2) { for (int x = 1; x < big.w; x += 2) { *xshmu_pix(small, x/2, y/2) = *xshmu_pix(big, x, y); } } return small; } typedef struct { xshmu_pic i[16]; } mipmap; void delete_mipmap(mipmap *m) { for (int i = 0; i < 16; i++) { free(m->i[i].p); m->i[i].p = 0; } } void init_mipmap(mipmap *m, xshmu_pic img) { delete_mipmap(m); m->i[0] = img; } void ensure_level_present(mipmap *m, int zoom) { if (!m->i[zoom].p) m->i[zoom] = reduce2x(m->i[zoom-1]); } static inline int int_max(int a, int b) { return (a > b) ? a : b; } #ifndef __GNUC__ #define __attribute__(x) #endif int __attribute__((weak)) main(int argc, char **argv) { int keep_running = 1; if (0 == strcmp(argv[1], "-q")) { argv++; argc--; keep_running = 0; } if (argc == 1) { fprintf(stderr, "usage: %s foo.png\n", argv[0]); return 1; } int img_idx = 1; mipmap img = {{{0}}}; init_mipmap(&img, read_png(argv[img_idx])); if (!img.i[0].p) return 1; /* XXX this is not a good way to report errors */ xshmu w = xshmu_open(argv[1], img.i[0].w, img.i[0].h, ""); int xoff = 0, yoff = 0, zoom = 0, image_is_valid = 1; for (;;) { for (xshmu_event *ev; (ev = xshmu_get_event(w));) { if (xshmu_as_die_event(ev)) goto done; xshmu_key_event *kev = xshmu_as_key_event(ev); if (kev && kev->down) { switch (kev->keysym) { case xks_left: if (xoff > 0) xoff -= 128; break; case xks_right: if (xoff < img.i[zoom].w) xoff += 128; break; case xks_up: if (yoff > 0) yoff -= 128; break; case xks_down: if (yoff < img.i[zoom].h) yoff += 128; break; case 'q': case 'Q': goto done; break; case ' ': if (img_idx >= argc-1) break; img_idx++; image_is_valid = 0; break; case xks_backspace: case 8: case 0x7f: if (img_idx <= 1) break; img_idx--; image_is_valid = 0; break; case '+': if (zoom) { zoom--; xoff *= 2; yoff *= 2; } break; case '-': if (zoom < 16 && img.i[zoom].w > 1 && img.i[zoom].h > 1) { zoom++; xoff /= 2; yoff /= 2; } ensure_level_present(&img, zoom); break; } } } if (!image_is_valid) { fill(xshmu_framebuffer(w), 0x007fff); xshmu_flush(w); init_mipmap(&img, read_png(argv[img_idx])); xoff = yoff = zoom = 0; image_is_valid = 1; } xshmu_pic fb = xshmu_framebuffer(w); fill(fb, 0x7f7f7f); // ensure background is erased for vpng_fb int cx = int_max(0, (fb.w - img.i[zoom].w)/2), cy = int_max(0, (fb.h - img.i[zoom].h)/2); xshmu_copy(xshmu_subpic(fb, cx, cy, fb.w, fb.h), xshmu_subpic(img.i[zoom], xoff, yoff, fb.w, fb.h)); xshmu_flush(w); if (!keep_running) break; xshmu_wait(w); } done: delete_mipmap(&img); xshmu_close(w); return 0; }