summary refs log tree commit diff
path: root/bin/gfx
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--bin/gfx/cocoa.m162
-rw-r--r--bin/gfx/fb.c87
-rw-r--r--bin/gfx/gfx.h24
-rw-r--r--bin/gfx/none.c24
-rw-r--r--bin/gfx/x11.c135
-rw-r--r--bin/gfxx.c492
6 files changed, 924 insertions, 0 deletions
diff --git a/bin/gfx/cocoa.m b/bin/gfx/cocoa.m
new file mode 100644
index 00000000..4837a386
--- /dev/null
+++ b/bin/gfx/cocoa.m
@@ -0,0 +1,162 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#import <err.h>
+#import <stdbool.h>
+#import <stdint.h>
+#import <stdlib.h>
+#import <sysexits.h>
+
+#import "gfx.h"
+
+#define UNUSED __attribute__((unused))
+
+@interface BufferView : NSView {
+    size_t bufSize;
+    uint32_t *buf;
+    CGColorSpaceRef colorSpace;
+    CGDataProviderRef dataProvider;
+}
+@end
+
+@implementation BufferView
+- (instancetype) initWithFrame: (NSRect) frameRect {
+    colorSpace = CGColorSpaceCreateDeviceRGB();
+    return [super initWithFrame: frameRect];
+}
+
+- (void) setWindowTitle {
+    [[self window] setTitle: [NSString stringWithUTF8String: status()]];
+}
+
+- (void) draw {
+    draw(buf, [self frame].size.width, [self frame].size.height);
+    [self setNeedsDisplay: YES];
+}
+
+- (void) setFrameSize: (NSSize) newSize {
+    [super setFrameSize: newSize];
+    size_t newBufSize = 4 * newSize.width * newSize.height;
+    if (newBufSize > bufSize) {
+        bufSize = newBufSize;
+        buf = malloc(bufSize);
+        if (!buf) err(EX_OSERR, "malloc(%zu)", bufSize);
+        CGDataProviderRelease(dataProvider);
+        dataProvider = CGDataProviderCreateWithData(NULL, buf, bufSize, NULL);
+    }
+    [self draw];
+}
+
+- (void) drawRect: (NSRect) UNUSED dirtyRect {
+    NSSize size = [self frame].size;
+    CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext];
+    CGImageRef image = CGImageCreate(
+        size.width, size.height,
+        8, 32, 4 * size.width,
+        colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst,
+        dataProvider,
+        NULL, false, kCGRenderingIntentDefault
+    );
+    CGContextDrawImage(ctx, [self frame], image);
+    CGImageRelease(image);
+}
+
+- (BOOL) acceptsFirstResponder {
+    return YES;
+}
+
+- (void) keyDown: (NSEvent *) event {
+    char in;
+    BOOL converted = [
+        [event characters]
+        getBytes: &in
+        maxLength: 1
+        usedLength: NULL
+        encoding: NSASCIIStringEncoding
+        options: 0
+        range: NSMakeRange(0, 1)
+        remainingRange: NULL
+    ];
+    if (converted) {
+        if (!input(in)) {
+            [NSApp terminate: self];
+        }
+        [self setWindowTitle];
+        [self draw];
+    }
+}
+@end
+
+@interface Delegate : NSObject <NSApplicationDelegate>
+@end
+
+@implementation Delegate
+- (BOOL) applicationShouldTerminateAfterLastWindowClosed:
+    (NSApplication *) UNUSED sender {
+    return YES;
+}
+@end
+
+int main(int argc, char *argv[]) {
+    int error = init(argc, argv);
+    if (error) return error;
+
+    [NSApplication sharedApplication];
+    [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
+    [NSApp setDelegate: [Delegate new]];
+
+    NSString *name = [[NSProcessInfo processInfo] processName];
+    NSMenu *menu = [NSMenu new];
+    [
+        menu
+        addItemWithTitle: @"Close Window"
+        action: @selector(performClose:)
+        keyEquivalent: @"w"
+    ];
+    [menu addItem: [NSMenuItem separatorItem]];
+    [
+        menu
+        addItemWithTitle: [@"Quit " stringByAppendingString: name]
+        action: @selector(terminate:)
+        keyEquivalent: @"q"
+    ];
+    NSMenuItem *menuItem = [NSMenuItem new];
+    [menuItem setSubmenu: menu];
+    [NSApp setMainMenu: [NSMenu new]];
+    [[NSApp mainMenu] addItem: menuItem];
+
+    NSUInteger style = NSTitledWindowMask
+        | NSClosableWindowMask
+        | NSMiniaturizableWindowMask
+        | NSResizableWindowMask;
+    NSWindow *window = [
+        [NSWindow alloc]
+        initWithContentRect: NSMakeRect(0, 0, 800, 600)
+        styleMask: style
+        backing: NSBackingStoreBuffered
+        defer: YES
+    ];
+    [window center];
+
+    BufferView *view = [[BufferView alloc] initWithFrame: [window frame]];
+    [window setContentView: view];
+    [view setWindowTitle];
+
+    [window makeKeyAndOrderFront: nil];
+    [NSApp activateIgnoringOtherApps: YES];
+    [NSApp run];
+}
diff --git a/bin/gfx/fb.c b/bin/gfx/fb.c
new file mode 100644
index 00000000..94a3245e
--- /dev/null
+++ b/bin/gfx/fb.c
@@ -0,0 +1,87 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <linux/fb.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sysexits.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "gfx.h"
+
+static struct termios saveTerm;
+static void restoreTerm(void) {
+    tcsetattr(STDERR_FILENO, TCSADRAIN, &saveTerm);
+}
+
+int main(int argc, char *argv[]) {
+    int error;
+
+    error = init(argc, argv);
+    if (error) return error;
+
+    const char *path = getenv("FRAMEBUFFER");
+    if (!path) path = "/dev/fb0";
+
+    int fb = open(path, O_RDWR);
+    if (fb < 0) err(EX_OSFILE, "%s", path);
+
+    struct fb_var_screeninfo info;
+    error = ioctl(fb, FBIOGET_VSCREENINFO, &info);
+    if (error) err(EX_IOERR, "%s", path);
+
+    size_t size = 4 * info.xres * info.yres;
+    uint32_t *buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
+    if (buf == MAP_FAILED) err(EX_IOERR, "%s", path);
+
+    error = tcgetattr(STDERR_FILENO, &saveTerm);
+    if (error) err(EX_IOERR, "tcgetattr");
+    atexit(restoreTerm);
+
+    struct termios term = saveTerm;
+    term.c_lflag &= ~(ICANON | ECHO);
+    error = tcsetattr(STDERR_FILENO, TCSADRAIN, &term);
+    if (error) err(EX_IOERR, "tcsetattr");
+
+    uint32_t saveBg = buf[0];
+
+    uint32_t back[info.xres * info.yres];
+    for (;;) {
+        draw(back, info.xres, info.yres);
+        memcpy(buf, back, size);
+
+        char in;
+        ssize_t len = read(STDERR_FILENO, &in, 1);
+        if (len < 0) err(EX_IOERR, "read");
+        if (!len) return EX_DATAERR;
+
+        if (!input(in)) {
+            for (uint32_t i = 0; i < info.xres * info.yres; ++i) {
+                buf[i] = saveBg;
+            }
+            fprintf(stderr, "%s\n", status());
+            return EX_OK;
+        }
+    }
+}
diff --git a/bin/gfx/gfx.h b/bin/gfx/gfx.h
new file mode 100644
index 00000000..cd59ea3d
--- /dev/null
+++ b/bin/gfx/gfx.h
@@ -0,0 +1,24 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+extern int init(int argc, char *argv[]);
+extern const char *status(void);
+extern void draw(uint32_t *buf, size_t width, size_t height);
+extern bool input(char in);
diff --git a/bin/gfx/none.c b/bin/gfx/none.c
new file mode 100644
index 00000000..6decb24b
--- /dev/null
+++ b/bin/gfx/none.c
@@ -0,0 +1,24 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <sysexits.h>
+
+#include "gfx.h"
+
+int main() {
+    errx(EX_CONFIG, "no gfx frontend");
+}
diff --git a/bin/gfx/x11.c b/bin/gfx/x11.c
new file mode 100644
index 00000000..108d6459
--- /dev/null
+++ b/bin/gfx/x11.c
@@ -0,0 +1,135 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <X11/Xlib.h>
+#include <err.h>
+#include <sysexits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#include "gfx.h"
+
+static size_t width = 800;
+static size_t height = 600;
+
+static Display *display;
+static Window window;
+static Atom WM_DELETE_WINDOW;
+static GC windowGc;
+static XImage *image;
+
+static size_t bufSize;
+static uint32_t *buf;
+
+static size_t pixmapWidth;
+static size_t pixmapHeight;
+static Pixmap pixmap;
+
+static void createWindow(void) {
+    display = XOpenDisplay(NULL);
+    if (!display) errx(EX_UNAVAILABLE, "XOpenDisplay: %s", XDisplayName(NULL));
+
+    Window root = DefaultRootWindow(display);
+    window = XCreateSimpleWindow(display, root, 0, 0, width, height, 0, 0, 0);
+
+    WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", false);
+    XSetWMProtocols(display, window, &WM_DELETE_WINDOW, 1);
+
+    windowGc = XCreateGC(display, window, 0, NULL);
+
+    image = XCreateImage(display, NULL, 24, ZPixmap, 0, NULL, width, height, 32, 0);
+}
+
+static void resizePixmap(void) {
+    size_t newSize = 4 * width * height;
+    if (newSize > bufSize) {
+        bufSize = newSize;
+        free(buf);
+        buf = malloc(bufSize);
+        if (!buf) err(EX_OSERR, "malloc(%zu)", bufSize);
+    }
+
+    image->data = (char *)buf;
+    image->width = width;
+    image->height = height;
+    image->bytes_per_line = 4 * width;
+
+    if (width > pixmapWidth || height > pixmapHeight) {
+        pixmapWidth = width;
+        pixmapHeight = height;
+        if (pixmap) XFreePixmap(display, pixmap);
+        pixmap = XCreatePixmap(display, window, pixmapWidth, pixmapHeight, 24);
+    }
+}
+
+static void drawWindow(void) {
+    draw(buf, width, height);
+    XPutImage(display, pixmap, windowGc, image, 0, 0, 0, 0, width, height);
+    XCopyArea(display, pixmap, window, windowGc, 0, 0, width, height, 0, 0);
+}
+
+int main(int argc, char *argv[]) {
+    int error = init(argc, argv);
+    if (error) return error;
+
+    createWindow();
+    resizePixmap();
+    drawWindow();
+
+    XStoreName(display, window, status());
+    XMapWindow(display, window);
+
+    XEvent event;
+    XSelectInput(display, window, ExposureMask | StructureNotifyMask | KeyPressMask);
+    for (;;) {
+        XNextEvent(display, &event);
+
+        switch (event.type) {
+            case KeyPress: {
+                XKeyEvent key = event.xkey;
+                KeySym sym = XLookupKeysym(&key, key.state & ShiftMask);
+                if (sym > 0x80) break;
+                if (!input(sym)) return EX_OK;
+                XStoreName(display, window, status());
+                drawWindow();
+            } break;
+
+            case ConfigureNotify: {
+                XConfigureEvent configure = event.xconfigure;
+                width = configure.width;
+                height = configure.height;
+                resizePixmap();
+                drawWindow();
+            } break;
+
+            case Expose: {
+                XExposeEvent expose = event.xexpose;
+                XCopyArea(
+                    display, pixmap, window, windowGc,
+                    expose.x, expose.y,
+                    expose.width, expose.height,
+                    expose.x, expose.y
+                );
+            } break;
+
+            case ClientMessage: {
+                XClientMessageEvent client = event.xclient;
+                if ((Atom)client.data.l[0] == WM_DELETE_WINDOW) return EX_OK;
+            } break;
+        }
+    }
+}
diff --git a/bin/gfxx.c b/bin/gfxx.c
new file mode 100644
index 00000000..8a43fd2f
--- /dev/null
+++ b/bin/gfxx.c
@@ -0,0 +1,492 @@
+/* Copyright (c) 2018, Curtis McEnroe <programble@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <err.h>
+#include <fcntl.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <zlib.h>
+
+#include "gfx/gfx.h"
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define MASK(b) ((1 << (b)) - 1)
+
+#define RGB(r, g, b) ((uint32_t)(r) << 16 | (uint32_t)(g) << 8 | (uint32_t)(b))
+#define GRAY(n) RGB(n, n, n)
+
+static enum {
+    COLOR_INDEXED,
+    COLOR_GRAYSCALE,
+    COLOR_RGB,
+    COLOR__COUNT,
+} space = COLOR_RGB;
+static const char *COLOR__STR[COLOR__COUNT] = { "indexed", "grayscale", "rgb" };
+static uint32_t palette[256];
+
+static enum {
+    ENDIAN_LITTLE,
+    ENDIAN_BIG,
+} byteOrder, bitOrder;
+
+enum { PAD, R, G, B };
+static uint8_t bits[4] = { 8, 8, 8, 8 };
+#define BITS_COLOR (bits[R] + bits[G] + bits[B])
+#define BITS_TOTAL (bits[PAD] + BITS_COLOR)
+
+static size_t offset;
+static size_t width = 16;
+static bool flip;
+static bool mirror;
+static size_t scale = 1;
+
+static const char *prefix = "gfxx";
+
+static size_t size;
+static uint8_t *data;
+
+int init(int argc, char *argv[]) {
+    const char *pal = NULL;
+    const char *path = NULL;
+
+    int opt;
+    while (0 < (opt = getopt(argc, argv, "c:p:b:e:E:n:fmw:z:o:"))) {
+        switch (opt) {
+            case 'c': switch (optarg[0]) {
+                case 'i': space = COLOR_INDEXED; break;
+                case 'g': space = COLOR_GRAYSCALE; break;
+                case 'r': space = COLOR_RGB; break;
+                default: return EX_USAGE;
+            } break;
+            case 'p': pal = optarg; break;
+            case 'e': switch (optarg[0]) {
+                case 'l': byteOrder = ENDIAN_LITTLE; break;
+                case 'b': byteOrder = ENDIAN_BIG; break;
+                default: return EX_USAGE;
+            } break;
+            case 'E': switch (optarg[0]) {
+                case 'l': bitOrder = ENDIAN_LITTLE; break;
+                case 'b': bitOrder = ENDIAN_BIG; break;
+                default: return EX_USAGE;
+            } break;
+            case 'b': {
+                if (strlen(optarg) < 4) return EX_USAGE;
+                for (int i = 0; i < 4; ++i) {
+                    bits[i] = optarg[i] - '0';
+                }
+            } break;
+            case 'n': offset  = strtoul(optarg, NULL, 0); break;
+            case 'f': flip   ^= true; break;
+            case 'm': mirror ^= true; break;
+            case 'w': width   = strtoul(optarg, NULL, 0); break;
+            case 'z': scale   = strtoul(optarg, NULL, 0); break;
+            case 'o': prefix  = optarg; break;
+            default: return EX_USAGE;
+        }
+    }
+    if (argc > optind) path = argv[optind];
+    if (!width || !scale) return EX_USAGE;
+
+    if (pal) {
+        FILE *file = fopen(pal, "r");
+        if (!file) err(EX_NOINPUT, "%s", pal);
+        fread(palette, 4, 256, file);
+        if (ferror(file)) err(EX_IOERR, "%s", pal);
+        fclose(file);
+    } else {
+        for (int i = 0; i < 256; ++i) {
+            double h = i / 256.0 * 6.0;
+            double x = 1.0 - fabs(fmod(h, 2.0) - 1.0);
+            double r = 255.0, g = 255.0, b = 255.0;
+            if      (h <= 1.0) { g *= x; b = 0.0; }
+            else if (h <= 2.0) { r *= x; b = 0.0; }
+            else if (h <= 3.0) { r = 0.0; b *= x; }
+            else if (h <= 4.0) { r = 0.0; g *= x; }
+            else if (h <= 5.0) { r *= x; g = 0.0; }
+            else if (h <= 6.0) { g = 0.0; b *= x; }
+            palette[i] = (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b;
+        }
+    }
+
+    if (path) {
+        int fd = open(path, O_RDONLY);
+        if (fd < 0) err(EX_NOINPUT, "%s", path);
+
+        struct stat stat;
+        int error = fstat(fd, &stat);
+        if (error) err(EX_IOERR, "%s", path);
+        size = stat.st_size;
+
+        data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+        if (data == MAP_FAILED) err(EX_IOERR, "%s", path);
+
+    } else {
+        size = 1024 * 1024;
+        data = malloc(size);
+        if (!data) err(EX_OSERR, "malloc(%zu)", size);
+
+        size = fread(data, 1, size, stdin);
+        if (ferror(stdin)) err(EX_IOERR, "(stdin)");
+    }
+
+    return EX_OK;
+}
+
+static char options[128];
+static void formatOptions(void) {
+    snprintf(
+        options, sizeof(options),
+        "gfxx -c %s -e%c -E%c -b %hhu%hhu%hhu%hhu -n %#zx %s%s-w %zu -z %zu",
+        COLOR__STR[space],
+        "lb"[byteOrder],
+        "lb"[bitOrder],
+        bits[PAD], bits[R], bits[G], bits[B],
+        offset,
+        flip ? "-f " : "",
+        mirror ? "-m " : "",
+        width,
+        scale
+    );
+}
+
+const char *status(void) {
+    formatOptions();
+    return options;
+}
+
+struct Iter {
+    uint32_t *buf;
+    size_t bufWidth;
+    size_t bufHeight;
+    size_t left;
+    size_t x;
+    size_t y;
+};
+
+static struct Iter iter(uint32_t *buf, size_t bufWidth, size_t bufHeight) {
+    struct Iter it = { .buf = buf, .bufWidth = bufWidth, .bufHeight = bufHeight };
+    if (mirror) it.x = width - 1;
+    if (flip) it.y = bufHeight / scale - 1;
+    return it;
+}
+
+static bool nextX(struct Iter *it) {
+    if (mirror) {
+        if (it->x == it->left) return false;
+        it->x--;
+    } else {
+        it->x++;
+        if (it->x == it->left + width) return false;
+    }
+    return true;
+}
+
+static bool nextY(struct Iter *it) {
+    if (flip) {
+        if (it->y == 0) {
+            it->left += width;
+            it->y = it->bufHeight / scale;
+        }
+        it->y--;
+    } else {
+        it->y++;
+        if (it->y == it->bufHeight / scale) {
+            it->left += width;
+            it->y = 0;
+        }
+    }
+    it->x = it->left;
+    if (mirror) it->x += width - 1;
+    return (it->left < it->bufWidth / scale);
+}
+
+static bool next(struct Iter *it) {
+    return nextX(it) || nextY(it);
+}
+
+static void put(const struct Iter *it, uint32_t pixel) {
+    size_t scaledX = it->x * scale;
+    size_t scaledY = it->y * scale;
+    for (size_t fillY = scaledY; fillY < scaledY + scale; ++fillY) {
+        if (fillY >= it->bufHeight) break;
+        for (size_t fillX = scaledX; fillX < scaledX + scale; ++fillX) {
+            if (fillX >= it->bufWidth) break;
+            it->buf[fillY * it->bufWidth + fillX] = pixel;
+        }
+    }
+}
+
+static uint8_t interp(uint8_t b, uint32_t n) {
+    if (b == 8) return n;
+    if (b == 0) return 0;
+    return n * MASK(8) / MASK(b);
+}
+
+static uint32_t interpolate(uint32_t rgb) {
+    uint32_t r, g, b;
+    if (bitOrder == ENDIAN_LITTLE) {
+        b = rgb & MASK(bits[B]);
+        g = (rgb >>= bits[B]) & MASK(bits[G]);
+        r = (rgb >>= bits[G]) & MASK(bits[R]);
+    } else {
+        r = rgb & MASK(bits[R]);
+        g = (rgb >>= bits[R]) & MASK(bits[G]);
+        b = (rgb >>= bits[G]) & MASK(bits[B]);
+    }
+    return RGB(interp(bits[R], r), interp(bits[G], g), interp(bits[B], b));
+}
+
+static void drawBits(struct Iter *it) {
+    for (size_t i = offset; i < size; ++i) {
+        for (uint8_t b = 0; b < 8; b += BITS_TOTAL) {
+            uint8_t n;
+            if (byteOrder == ENDIAN_BIG) {
+                n = data[i] >> (8 - BITS_TOTAL - b) & MASK(BITS_TOTAL);
+            } else {
+                n = data[i] >> b & MASK(BITS_TOTAL);
+            }
+
+            if (space == COLOR_INDEXED) {
+                put(it, palette[n]);
+            } else if (space == COLOR_GRAYSCALE) {
+                put(it, GRAY(interp(BITS_COLOR, n & MASK(BITS_COLOR))));
+            } else if (space == COLOR_RGB) {
+                put(it, interpolate(n));
+            }
+
+            if (!next(it)) return;
+        }
+    }
+}
+
+static void drawBytes(struct Iter *it) {
+    uint8_t bytes = (BITS_TOTAL + 7) / 8;
+    for (size_t i = offset; i + bytes <= size; i += bytes) {
+        uint32_t n = 0;
+        for (size_t b = 0; b < bytes; ++b) {
+            n <<= 8;
+            n |= (byteOrder == ENDIAN_BIG) ? data[i + b] : data[i + bytes - b - 1];
+        }
+
+        if (space == COLOR_INDEXED) {
+            put(it, palette[n & 0xFF]);
+        } else if (space == COLOR_GRAYSCALE) {
+            put(it, GRAY(interp(BITS_COLOR, n & MASK(BITS_COLOR))));
+        } else if (space == COLOR_RGB) {
+            put(it, interpolate(n));
+        }
+
+        if (!next(it)) return;
+    }
+}
+
+static struct {
+    unsigned counter;
+    char path[FILENAME_MAX];
+    FILE *file;
+} out;
+static void outOpen(const char *ext) {
+    snprintf(out.path, sizeof(out.path), "%s%04u.%s", prefix, ++out.counter, ext);
+    out.file = fopen(out.path, "wx");
+    if (out.file) {
+        printf("%s\n", out.path);
+    } else {
+        warn("%s", out.path);
+    }
+}
+
+static uint32_t crc;
+static void pngWrite(const void *ptr, size_t size) {
+    fwrite(ptr, size, 1, out.file);
+    if (ferror(out.file)) err(EX_IOERR, "%s", out.path);
+    crc = crc32(crc, ptr, size);
+}
+static void pngUint(uint32_t host) {
+    uint32_t net = htonl(host);
+    pngWrite(&net, 4);
+}
+static void pngChunk(const char *type, uint32_t size) {
+    pngUint(size);
+    crc = crc32(0, Z_NULL, 0);
+    pngWrite(type, 4);
+}
+
+static void pngDump(uint32_t *src, size_t srcWidth, size_t srcHeight) {
+    int error;
+
+    size_t stride = 1 + 3 * srcWidth;
+    uint8_t data[stride * srcHeight];
+    for (size_t y = 0; y < srcHeight; ++y) {
+        data[y * stride] = 0;
+        for (size_t x = 0; x < srcWidth; ++x) {
+            uint8_t *p = &data[y * stride + 1 + 3 * x];
+            p[0] = src[y * srcWidth + x] >> 16;
+            p[1] = src[y * srcWidth + x] >> 8;
+            p[2] = src[y * srcWidth + x];
+        }
+    }
+
+    uLong deflateSize = compressBound(sizeof(data));
+    uint8_t deflate[deflateSize];
+    error = compress(deflate, &deflateSize, data, sizeof(data));
+    if (error != Z_OK) errx(EX_SOFTWARE, "compress: %d", error);
+
+    outOpen("png");
+    if (!out.file) return;
+
+    const uint8_t SIGNATURE[] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n' };
+    const uint8_t HEADER[] = { 8, 2, 0, 0, 0 }; // 8-bit truecolor
+    const char SOFTWARE[] = "Software";
+    formatOptions();
+    uint8_t sbit[3] = { MAX(bits[R], 1), MAX(bits[G], 1), MAX(bits[B], 1) };
+
+    pngWrite(SIGNATURE, sizeof(SIGNATURE));
+
+    pngChunk("IHDR", 4 + 4 + sizeof(HEADER));
+    pngUint(srcWidth);
+    pngUint(srcHeight);
+    pngWrite(HEADER, sizeof(HEADER));
+    pngUint(crc);
+
+    pngChunk("tEXt", sizeof(SOFTWARE) + strlen(options));
+    pngWrite(SOFTWARE, sizeof(SOFTWARE));
+    pngWrite(options, strlen(options));
+    pngUint(crc);
+
+    pngChunk("sBIT", sizeof(sbit));
+    pngWrite(sbit, sizeof(sbit));
+    pngUint(crc);
+
+    pngChunk("IDAT", deflateSize);
+    pngWrite(deflate, deflateSize);
+    pngUint(crc);
+
+    pngChunk("IEND", 0);
+    pngUint(crc);
+
+    error = fclose(out.file);
+    if (error) err(EX_IOERR, "%s", out.path);
+}
+
+static enum {
+    DUMP_NONE,
+    DUMP_ONE,
+    DUMP_ALL,
+} dump;
+
+void draw(uint32_t *buf, size_t bufWidth, size_t bufHeight) {
+    memset(buf, 0, 4 * bufWidth * bufHeight);
+    struct Iter it = iter(buf, bufWidth, bufHeight);
+    if (BITS_TOTAL >= 8) {
+        drawBytes(&it);
+    } else {
+        drawBits(&it);
+    }
+    if (dump) pngDump(buf, bufWidth, bufHeight);
+    if (dump == DUMP_ONE) dump = DUMP_NONE;
+}
+
+static void palSample(void) {
+    size_t temp = scale;
+    scale = 1;
+    draw(palette, 256, 1);
+    scale = temp;
+}
+
+static void palDump(void) {
+    outOpen("dat");
+    if (!out.file) return;
+
+    fwrite(palette, 4, 256, out.file);
+    if (ferror(out.file)) err(EX_IOERR, "%s", out.path);
+
+    int error = fclose(out.file);
+    if (error) err(EX_IOERR, "%s", out.path);
+}
+
+static const uint8_t PRESETS[][4] = {
+    { 0, 0, 1, 0 },
+    { 0, 1, 1, 0 },
+    { 1, 1, 1, 1 },
+    { 2, 2, 2, 2 },
+    { 0, 3, 3, 2 },
+    { 4, 4, 4, 4 },
+    { 1, 5, 5, 5 },
+    { 0, 5, 6, 5 },
+    { 0, 8, 8, 8 },
+    { 8, 8, 8, 8 },
+};
+#define PRESETS_LEN (sizeof(PRESETS) / sizeof(PRESETS[0]))
+
+static uint8_t preset = PRESETS_LEN - 1;
+static void setPreset(void) {
+    bits[PAD] = PRESETS[preset][PAD];
+    bits[R] = PRESETS[preset][R];
+    bits[G] = PRESETS[preset][G];
+    bits[B] = PRESETS[preset][B];
+}
+
+static void setBit(char in) {
+    static uint8_t bit = 0;
+    bits[bit++] = in - '0';
+    bit &= 3;
+}
+
+bool input(char in) {
+    size_t pixel = (BITS_TOTAL + 7) / 8;
+    size_t row = width * BITS_TOTAL / 8;
+    switch (in) {
+        case 'q': return false;
+        break; case 'x': dump = DUMP_ONE;
+        break; case 'X': dump ^= DUMP_ALL;
+        break; case 'o': formatOptions(); printf("%s\n", options);
+        break; case '[': if (!space--) space = COLOR__COUNT - 1;
+        break; case ']': if (++space == COLOR__COUNT) space = 0;
+        break; case 'p': palSample();
+        break; case 'P': palDump();
+        break; case '{': if (!preset--) preset = PRESETS_LEN - 1; setPreset();
+        break; case '}': if (++preset == PRESETS_LEN) preset = 0; setPreset();
+        break; case 'e': byteOrder ^= ENDIAN_BIG;
+        break; case 'E': bitOrder ^= ENDIAN_BIG;
+        break; case 'h': if (offset) offset--;
+        break; case 'j': offset += pixel;
+        break; case 'k': if (offset >= pixel) offset -= pixel;
+        break; case 'l': offset++;
+        break; case 'H': if (offset >= row) offset -= row;
+        break; case 'J': offset += width * row;
+        break; case 'K': if (offset >= width * row) offset -= width * row;
+        break; case 'L': offset += row;
+        break; case '.': width++;
+        break; case ',': if (width > 1) width--;
+        break; case '>': width *= 2;
+        break; case '<': if (width > 1) width /= 2;
+        break; case 'f': flip ^= true;
+        break; case 'm': mirror ^= true;
+        break; case '+': scale++;
+        break; case '-': if (scale > 1) scale--;
+        break; default: if (in >= '0' && in <= '9') setBit(in);
+    }
+    return true;
+}