diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## v0.5.5.2
+
+  * Supports GHC 8.6
+  * Replaced `HPDF` with C library `pdfgen`
+
 ## v0.5.5.1
 
   * Switched to new `conduit` syntax
diff --git a/cbits/pdfgen.c b/cbits/pdfgen.c
new file mode 100644
--- /dev/null
+++ b/cbits/pdfgen.c
@@ -0,0 +1,2242 @@
+/**
+ * Simple engine for creating PDF files.
+ * It supports text, shapes, images etc...
+ * Capable of handling millions of objects without too much performance
+ * penalty.
+ * Public domain license - no warrenty implied; use at your own risk.
+ */
+
+/**
+ * PDF HINTS & TIPS
+ * The following sites have various bits & pieces about PDF document
+ * generation
+ * http://www.mactech.com/articles/mactech/Vol.15/15.09/PDFIntro/index.html
+ * http://gnupdf.org/Introduction_to_PDF
+ * http://www.planetpdf.com/mainpage.asp?WebPageID=63
+ * http://archive.vector.org.uk/art10008970
+ * http://www.adobe.com/devnet/acrobat/pdfs/pdf_reference_1-7.pdf
+ * https://blog.idrsolutions.com/2013/01/understanding-the-pdf-file-format-overview/
+ *
+ * To validate the PDF output, there are several online validators:
+ * http://www.validatepdfa.com/online.htm
+ * http://www.datalogics.com/products/callas/callaspdfA-onlinedemo.asp
+ * http://www.pdf-tools.com/pdf/validate-pdfa-online.aspx
+ *
+ * In addition the 'pdftk' server can be used to analyse the output:
+ * https://www.pdflabs.com/docs/pdftk-cli-examples/
+ *
+ * PDF page markup operators:
+ * b    closepath, fill,and stroke path.
+ * B    fill and stroke path.
+ * b*   closepath, eofill,and stroke path.
+ * B*   eofill and stroke path.
+ * BI   begin image.
+ * BMC  begin marked content.
+ * BT   begin text object.
+ * BX   begin section allowing undefined operators.
+ * c    curveto.
+ * cm   concat. Concatenates the matrix to the current transform.
+ * cs   setcolorspace for fill.
+ * CS   setcolorspace for stroke.
+ * d    setdash.
+ * Do   execute the named XObject.
+ * DP   mark a place in the content stream, with a dictionary.
+ * EI   end image.
+ * EMC  end marked content.
+ * ET   end text object.
+ * EX   end section that allows undefined operators.
+ * f    fill path.
+ * f*   eofill Even/odd fill path.
+ * g    setgray (fill).
+ * G    setgray (stroke).
+ * gs   set parameters in the extended graphics state.
+ * h    closepath.
+ * i    setflat.
+ * ID   begin image data.
+ * j    setlinejoin.
+ * J    setlinecap.
+ * k    setcmykcolor (fill).
+ * K    setcmykcolor (stroke).
+ * l    lineto.
+ * m    moveto.
+ * M    setmiterlimit.
+ * n    end path without fill or stroke.
+ * q    save graphics state.
+ * Q    restore graphics state.
+ * re   rectangle.
+ * rg   setrgbcolor (fill).
+ * RG   setrgbcolor (stroke).
+ * s    closepath and stroke path.
+ * S    stroke path.
+ * sc   setcolor (fill).
+ * SC   setcolor (stroke).
+ * sh   shfill (shaded fill).
+ * Tc   set character spacing.
+ * Td   move text current point.
+ * TD   move text current point and set leading.
+ * Tf   set font name and size.
+ * Tj   show text.
+ * TJ   show text, allowing individual character positioning.
+ * TL   set leading.
+ * Tm   set text matrix.
+ * Tr   set text rendering mode.
+ * Ts   set super/subscripting text rise.
+ * Tw   set word spacing.
+ * Tz   set horizontal scaling.
+ * T*   move to start of next line.
+ * v    curveto.
+ * w    setlinewidth.
+ * W    clip.
+ * y    curveto.
+ */
+
+// MT: commented this out, breaks things for some reason
+// #define _POSIX_SOURCE /* For localtime_r */
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "pdfgen.h"
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+#define PDF_RGB_R(c) ((((c) >> 16) & 0xff) / 255.0)
+#define PDF_RGB_G(c) ((((c) >> 8) & 0xff) / 255.0)
+#define PDF_RGB_B(c) ((((c) >> 0) & 0xff) / 255.0)
+#define PDF_IS_TRANSPARENT(c) (((c) >> 24) == 0xff)
+
+#if defined(_MSC_VER) && (_MSC_VER < 1900)
+/*
+ * As stated here:
+ * http://stackoverflow.com/questions/70013/how-to-detect-if-im-compiling-code-with-visual-studio-2008
+ * Visual Studio 2015 has better support for C99
+ * We need to use __inline for older version.
+ */
+#define inline __inline
+#define snprintf _snprintf
+#define strcasecmp _stricmp
+#define strncasecmp _strnicmp
+#define SKIP_ATTRIBUTE
+#endif // _MSC_VER && _MSC_VER < 1900
+
+#ifndef M_SQRT2
+#define M_SQRT2 1.41421356237309504880f
+#endif
+
+typedef struct pdf_object pdf_object;
+
+enum {
+    OBJ_none, /* skipped */
+    OBJ_info,
+    OBJ_stream,
+    OBJ_font,
+    OBJ_page,
+    OBJ_bookmark,
+    OBJ_outline,
+    OBJ_catalog,
+    OBJ_pages,
+    OBJ_image,
+
+    OBJ_count,
+};
+
+struct flexarray {
+    void ***bins;
+    int item_count;
+    int bin_count;
+};
+
+/**
+ * Simple dynamic string object. Tries to store a reasonable amount on the
+ * stack before falling back to malloc once things get large
+ */
+struct dstr {
+    char static_data[128];
+    char *data;
+    int alloc_len;
+    int used_len;
+};
+
+struct pdf_object {
+    int type;                /* See OBJ_xxxx */
+    int index;               /* PDF output index */
+    int offset;              /* Byte position within the output file */
+    struct pdf_object *prev; /* Previous of this type */
+    struct pdf_object *next; /* Next of this type */
+    union {
+        struct {
+            struct pdf_object *page;
+            char name[64];
+            struct pdf_object *parent;
+            struct flexarray children;
+        } bookmark;
+        struct dstr stream;
+        struct {
+            int width;
+            int height;
+            struct flexarray children;
+        } page;
+        struct pdf_info *info;
+        struct {
+            char name[64];
+            int index;
+        } font;
+    };
+};
+
+struct pdf_doc {
+    char errstr[128];
+    int errval;
+    struct flexarray objects;
+
+    int width;
+    int height;
+
+    struct pdf_object *current_font;
+
+    struct pdf_object *last_objects[OBJ_count];
+    struct pdf_object *first_objects[OBJ_count];
+};
+
+/**
+ * Simple flexible resizing array implementation
+ * The bins get larger in powers of two
+ * bin 0 = 1024 items
+ *     1 = 2048 items
+ *     2 = 4096 items
+ *     etc...
+ */
+/* What is the first index that will be in the given bin? */
+#define MIN_SHIFT 10
+#define MIN_OFFSET ((1 << MIN_SHIFT) - 1)
+static int bin_offset[] = {
+    (1 << (MIN_SHIFT + 0)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 1)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 2)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 3)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 4)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 5)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 6)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 7)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 8)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 9)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 10)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 11)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 12)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 13)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 14)) - 1 - MIN_OFFSET,
+    (1 << (MIN_SHIFT + 15)) - 1 - MIN_OFFSET,
+};
+
+static inline int flexarray_get_bin(struct flexarray *flex, int index)
+{
+    (void)flex;
+    for (int i = 0; i < ARRAY_SIZE(bin_offset); i++)
+        if (index < bin_offset[i])
+            return i - 1;
+    return -1;
+}
+
+static inline int flexarray_get_bin_size(struct flexarray *flex, int bin)
+{
+    (void)flex;
+    if (bin >= ARRAY_SIZE(bin_offset))
+        return -1;
+    int next = bin_offset[bin + 1];
+    return next - bin_offset[bin];
+}
+
+static inline int flexarray_get_bin_offset(struct flexarray *flex, int bin,
+                                           int index)
+{
+    (void)flex;
+    return index - bin_offset[bin];
+}
+
+static void flexarray_clear(struct flexarray *flex)
+{
+    for (int i = 0; i < flex->bin_count; i++)
+        free(flex->bins[i]);
+    free(flex->bins);
+    flex->bin_count = 0;
+    flex->item_count = 0;
+}
+
+static inline int flexarray_size(struct flexarray *flex)
+{
+    return flex->item_count;
+}
+
+static int flexarray_set(struct flexarray *flex, int index, void *data)
+{
+    int bin = flexarray_get_bin(flex, index);
+    if (bin < 0)
+        return -EINVAL;
+    if (bin >= flex->bin_count) {
+        void *bins =
+            realloc(flex->bins, (flex->bin_count + 1) * sizeof(flex->bins));
+        if (!bins)
+            return -ENOMEM;
+        flex->bin_count++;
+        flex->bins = bins;
+        flex->bins[flex->bin_count - 1] =
+            calloc(flexarray_get_bin_size(flex, flex->bin_count - 1),
+                   sizeof(void *));
+        if (!flex->bins[flex->bin_count - 1]) {
+            flex->bin_count--;
+            return -ENOMEM;
+        }
+    }
+    flex->item_count++;
+    flex->bins[bin][flexarray_get_bin_offset(flex, bin, index)] = data;
+    return flex->item_count - 1;
+}
+
+static inline int flexarray_append(struct flexarray *flex, void *data)
+{
+    return flexarray_set(flex, flexarray_size(flex), data);
+}
+
+static inline void *flexarray_get(struct flexarray *flex, int index)
+{
+    int bin;
+
+    if (index >= flex->item_count)
+        return NULL;
+    bin = flexarray_get_bin(flex, index);
+    if (bin < 0 || bin >= flex->bin_count)
+        return NULL;
+    return flex->bins[bin][flexarray_get_bin_offset(flex, bin, index)];
+}
+
+/**
+ * Simple dynamic string object. Tries to store a reasonable amount on the
+ * stack before falling back to malloc once things get large
+ */
+
+#define INIT_DSTR                                                            \
+    (struct dstr)                                                            \
+    {                                                                        \
+        .static_data = {0}, .data = NULL, .alloc_len = 0, .used_len = 0      \
+    }
+
+static char *dstr_data(struct dstr *str)
+{
+    return str->data ? str->data : str->static_data;
+}
+
+static int dstr_len(struct dstr *str)
+{
+    return str->used_len;
+}
+
+static int dstr_ensure(struct dstr *str, int len)
+{
+    if (len <= str->alloc_len)
+        return 0;
+    if (!str->data && len <= sizeof(str->static_data))
+        str->alloc_len = len;
+    else if (str->alloc_len < len) {
+        char *new_data;
+        int new_len;
+
+        new_len = len + 4096;
+        new_data = realloc(str->data, new_len);
+        if (!new_data)
+            return -ENOMEM;
+        // If we move beyond the on-stack buffer, copy the old data out
+        if (!str->data && str->used_len > 0)
+            memcpy(new_data, str->static_data, str->used_len + 1);
+        str->data = new_data;
+        str->alloc_len = new_len;
+    }
+    return 0;
+}
+
+#ifndef SKIP_ATTRIBUTE
+static int dstr_printf(struct dstr *str, const char *fmt, ...)
+    __attribute__((format(gnu_printf, 2, 3)));
+#endif
+static int dstr_printf(struct dstr *str, const char *fmt, ...)
+{
+    va_list ap, aq;
+    int len;
+
+    va_start(ap, fmt);
+    va_copy(aq, ap);
+    len = vsnprintf(NULL, 0, fmt, ap);
+    if (dstr_ensure(str, str->used_len + len + 1) < 0) {
+        va_end(ap);
+        va_end(aq);
+        return -ENOMEM;
+    }
+    vsprintf(dstr_data(str) + str->used_len, fmt, aq);
+    str->used_len += len;
+    va_end(ap);
+    va_end(aq);
+
+    return len;
+}
+
+static int dstr_append_data(struct dstr *str, const void *extend, int len)
+{
+    if (dstr_ensure(str, str->used_len + len) < 0)
+        return -ENOMEM;
+    memcpy(dstr_data(str) + str->used_len, extend, len);
+    str->used_len += len;
+    return len;
+}
+
+static int dstr_append(struct dstr *str, const char *extend)
+{
+    int len = strlen(extend);
+    if (dstr_ensure(str, str->used_len + len + 1) < 0)
+        return -ENOMEM;
+    memcpy(dstr_data(str) + str->used_len, extend, len + 1);
+    str->used_len += len;
+    return len;
+}
+
+static void dstr_free(struct dstr *str)
+{
+    if (str->data)
+        free(str->data);
+    *str = INIT_DSTR;
+}
+
+/**
+ * PDF Implementation
+ */
+
+#ifndef SKIP_ATTRIBUTE
+static int pdf_set_err(struct pdf_doc *doc, int errval, const char *buffer,
+                       ...) __attribute__((format(gnu_printf, 3, 4)));
+#endif
+static int pdf_set_err(struct pdf_doc *doc, int errval, const char *buffer,
+                       ...)
+{
+    va_list ap;
+    int len;
+
+    va_start(ap, buffer);
+    len = vsnprintf(doc->errstr, sizeof(doc->errstr) - 2, buffer, ap);
+    va_end(ap);
+
+    /* Make sure we're properly terminated */
+    if (doc->errstr[len] != '\n')
+        doc->errstr[len] = '\n';
+    doc->errstr[len] = '\0';
+    doc->errval = errval;
+
+    return errval;
+}
+
+const char *pdf_get_err(struct pdf_doc *pdf, int *errval)
+{
+    if (!pdf)
+        return NULL;
+    if (pdf->errstr[0] == '\0')
+        return NULL;
+    if (errval)
+        *errval = pdf->errval;
+    return pdf->errstr;
+}
+
+void pdf_clear_err(struct pdf_doc *pdf)
+{
+    if (!pdf)
+        return;
+    pdf->errstr[0] = '\0';
+    pdf->errval = 0;
+}
+
+static struct pdf_object *pdf_get_object(struct pdf_doc *pdf, int index)
+{
+    return flexarray_get(&pdf->objects, index);
+}
+
+static int pdf_append_object(struct pdf_doc *pdf, struct pdf_object *obj)
+{
+    int index = flexarray_append(&pdf->objects, obj);
+
+    if (index < 0)
+        return index;
+    obj->index = index;
+
+    if (pdf->last_objects[obj->type]) {
+        obj->prev = pdf->last_objects[obj->type];
+        pdf->last_objects[obj->type]->next = obj;
+    }
+    pdf->last_objects[obj->type] = obj;
+
+    if (!pdf->first_objects[obj->type])
+        pdf->first_objects[obj->type] = obj;
+
+    return 0;
+}
+
+static struct pdf_object *pdf_add_object(struct pdf_doc *pdf, int type)
+{
+    struct pdf_object *obj;
+
+    obj = calloc(1, sizeof(*obj));
+    if (!obj) {
+        pdf_set_err(pdf, -errno, "Unable to allocate object %d: %s",
+                    flexarray_size(&pdf->objects) + 1, strerror(errno));
+        return NULL;
+    }
+
+    obj->type = type;
+
+    if (pdf_append_object(pdf, obj) < 0) {
+        free(obj);
+        return NULL;
+    }
+
+    return obj;
+}
+
+struct pdf_doc *pdf_create(int width, int height, struct pdf_info *info)
+{
+    struct pdf_doc *pdf;
+    struct pdf_object *obj;
+
+    pdf = calloc(1, sizeof(*pdf));
+    pdf->width = width;
+    pdf->height = height;
+
+    /* We don't want to use ID 0 */
+    pdf_add_object(pdf, OBJ_none);
+
+    /* Create the 'info' object */
+    obj = pdf_add_object(pdf, OBJ_info);
+    if (!obj) {
+        pdf_destroy(pdf);
+        return NULL;
+    }
+    obj->info = calloc(sizeof(*obj->info), 1);
+    if (!obj->info) {
+        pdf_destroy(pdf);
+        return NULL;
+    }
+    if (info) {
+        *obj->info = *info;
+        obj->info->creator[sizeof(obj->info->creator) - 1] = '\0';
+        obj->info->producer[sizeof(obj->info->producer) - 1] = '\0';
+        obj->info->title[sizeof(obj->info->title) - 1] = '\0';
+        obj->info->author[sizeof(obj->info->author) - 1] = '\0';
+        obj->info->subject[sizeof(obj->info->subject) - 1] = '\0';
+        obj->info->date[sizeof(obj->info->date) - 1] = '\0';
+    }
+    /* FIXME: Should be quoting PDF strings? */
+    if (!obj->info->date[0]) {
+        time_t now = time(NULL);
+        struct tm tm;
+#ifdef _WIN32
+        struct tm *tmp;
+        tmp = localtime(&now);
+        tm = *tmp;
+#else
+        localtime_r(&now, &tm);
+#endif
+        strftime(obj->info->date, sizeof(obj->info->date), "%Y%m%d%H%M%SZ",
+                 &tm);
+    }
+    if (!obj->info->creator[0])
+        snprintf(obj->info->creator, sizeof(obj->info->creator), "pdfgen");
+    if (!obj->info->producer[0])
+        snprintf(obj->info->producer, sizeof(obj->info->producer), "pdfgen");
+    if (!obj->info->title[0])
+        snprintf(obj->info->title, sizeof(obj->info->title), "pdfgen");
+    if (!obj->info->author[0])
+        snprintf(obj->info->author, sizeof(obj->info->author), "pdfgen");
+    if (!obj->info->subject[0])
+        snprintf(obj->info->subject, sizeof(obj->info->subject), "pdfgen");
+
+    pdf_add_object(pdf, OBJ_pages);
+    pdf_add_object(pdf, OBJ_catalog);
+
+    pdf_set_font(pdf, "Times-Roman");
+
+    return pdf;
+}
+
+int pdf_width(struct pdf_doc *pdf)
+{
+    return pdf->width;
+}
+
+int pdf_height(struct pdf_doc *pdf)
+{
+    return pdf->height;
+}
+
+static void pdf_object_destroy(struct pdf_object *object)
+{
+    switch (object->type) {
+    case OBJ_stream:
+    case OBJ_image:
+        dstr_free(&object->stream);
+        break;
+    case OBJ_page:
+        flexarray_clear(&object->page.children);
+        break;
+    case OBJ_info:
+        free(object->info);
+        break;
+    case OBJ_bookmark:
+        flexarray_clear(&object->bookmark.children);
+        break;
+    }
+    free(object);
+}
+
+void pdf_destroy(struct pdf_doc *pdf)
+{
+    if (pdf) {
+        for (int i = 0; i < flexarray_size(&pdf->objects); i++)
+            pdf_object_destroy(pdf_get_object(pdf, i));
+        flexarray_clear(&pdf->objects);
+        free(pdf);
+    }
+}
+
+static struct pdf_object *pdf_find_first_object(struct pdf_doc *pdf, int type)
+{
+    return pdf->first_objects[type];
+}
+
+static struct pdf_object *pdf_find_last_object(struct pdf_doc *pdf, int type)
+{
+    return pdf->last_objects[type];
+}
+
+int pdf_set_font(struct pdf_doc *pdf, const char *font)
+{
+    struct pdf_object *obj;
+    int last_index = 0;
+
+    /* See if we've used this font before */
+    for (obj = pdf_find_first_object(pdf, OBJ_font); obj; obj = obj->next) {
+        if (strcmp(obj->font.name, font) == 0)
+            break;
+        last_index = obj->font.index;
+    }
+
+    /* Create a new font object if we need it */
+    if (!obj) {
+        obj = pdf_add_object(pdf, OBJ_font);
+        if (!obj)
+            return pdf->errval;
+        strncpy(obj->font.name, font, sizeof(obj->font.name));
+        obj->font.name[sizeof(obj->font.name) - 1] = '\0';
+        obj->font.index = last_index + 1;
+    }
+
+    pdf->current_font = obj;
+
+    return 0;
+}
+
+struct pdf_object *pdf_append_page(struct pdf_doc *pdf)
+{
+    struct pdf_object *page;
+
+    page = pdf_add_object(pdf, OBJ_page);
+
+    if (!page)
+        return NULL;
+
+    page->page.width = pdf->width;
+    page->page.height = pdf->height;
+
+    return page;
+}
+
+int pdf_page_set_size(struct pdf_doc *pdf, struct pdf_object *page, int width,
+                      int height)
+{
+    if (!page)
+        page = pdf_find_last_object(pdf, OBJ_page);
+
+    if (!page || page->type != OBJ_page)
+        return pdf_set_err(pdf, -EINVAL, "Invalid PDF page");
+    page->page.width = width;
+    page->page.height = height;
+    return 0;
+}
+
+static int pdf_save_object(struct pdf_doc *pdf, FILE *fp, int index)
+{
+    struct pdf_object *object = pdf_get_object(pdf, index);
+
+    if (object->type == OBJ_none)
+        return -ENOENT;
+
+    object->offset = ftell(fp);
+
+    fprintf(fp, "%d 0 obj\r\n", index);
+
+    switch (object->type) {
+    case OBJ_stream:
+    case OBJ_image: {
+        fwrite(dstr_data(&object->stream), dstr_len(&object->stream), 1, fp);
+        break;
+    }
+    case OBJ_info: {
+        struct pdf_info *info = object->info;
+
+        fprintf(fp,
+                "<<\r\n"
+                "  /Creator (%s)\r\n"
+                "  /Producer (%s)\r\n"
+                "  /Title (%s)\r\n"
+                "  /Author (%s)\r\n"
+                "  /Subject (%s)\r\n"
+                "  /CreationDate (D:%s)\r\n"
+                ">>\r\n",
+                info->creator, info->producer, info->title, info->author,
+                info->subject, info->date);
+        break;
+    }
+
+    case OBJ_page: {
+        struct pdf_object *pages = pdf_find_first_object(pdf, OBJ_pages);
+        struct pdf_object *image = pdf_find_first_object(pdf, OBJ_image);
+
+        fprintf(fp,
+                "<<\r\n"
+                "/Type /Page\r\n"
+                "/Parent %d 0 R\r\n",
+                pages->index);
+        fprintf(fp, "/MediaBox [0 0 %d %d]\r\n", object->page.width,
+                object->page.height);
+        fprintf(fp, "/Resources <<\r\n");
+        fprintf(fp, "  /Font <<\r\n");
+        for (struct pdf_object *font = pdf_find_first_object(pdf, OBJ_font);
+             font; font = font->next)
+            fprintf(fp, "    /F%d %d 0 R\r\n", font->font.index, font->index);
+        fprintf(fp, "  >>\r\n");
+
+        if (image) {
+            fprintf(fp, "  /XObject <<");
+            for (; image; image = image->next)
+                fprintf(fp, "/Image%d %d 0 R ", image->index, image->index);
+            fprintf(fp, ">>\r\n");
+        }
+
+        fprintf(fp, ">>\r\n");
+        fprintf(fp, "/Contents [\r\n");
+        for (int i = 0; i < flexarray_size(&object->page.children); i++) {
+            struct pdf_object *child =
+                flexarray_get(&object->page.children, i);
+            fprintf(fp, "%d 0 R\r\n", child->index);
+        }
+        fprintf(fp, "]\r\n");
+        fprintf(fp, ">>\r\n");
+        break;
+    }
+
+    case OBJ_bookmark: {
+        struct pdf_object *parent, *other;
+
+        parent = object->bookmark.parent;
+        if (!parent)
+            parent = pdf_find_first_object(pdf, OBJ_outline);
+        if (!object->bookmark.page)
+            break;
+        fprintf(fp,
+                "<<\r\n"
+                "/A << /Type /Action\r\n"
+                "      /S /GoTo\r\n"
+                "      /D [%d 0 R /XYZ 0 %d null]\r\n"
+                "   >>\r\n"
+                "/Parent %d 0 R\r\n"
+                "/Title (%s)\r\n",
+                object->bookmark.page->index, pdf->height, parent->index,
+                object->bookmark.name);
+        int nchildren = flexarray_size(&object->bookmark.children);
+        if (nchildren > 0) {
+            struct pdf_object *f, *l;
+            f = flexarray_get(&object->bookmark.children, 0);
+            l = flexarray_get(&object->bookmark.children, nchildren - 1);
+            fprintf(fp, "/First %d 0 R\r\n", f->index);
+            fprintf(fp, "/Last %d 0 R\r\n", l->index);
+        }
+        // Find the previous bookmark with the same parent
+        for (other = object->prev;
+             other && other->bookmark.parent != object->bookmark.parent;
+             other = other->prev)
+            ;
+        if (other)
+            fprintf(fp, "/Prev %d 0 R\r\n", other->index);
+        // Find the next bookmark with the same parent
+        for (other = object->next;
+             other && other->bookmark.parent != object->bookmark.parent;
+             other = other->next)
+            ;
+        if (other)
+            fprintf(fp, "/Next %d 0 R\r\n", other->index);
+        fprintf(fp, ">>\r\n");
+        break;
+    }
+
+    case OBJ_outline: {
+        struct pdf_object *first, *last, *cur;
+        first = pdf_find_first_object(pdf, OBJ_bookmark);
+        last = pdf_find_last_object(pdf, OBJ_bookmark);
+
+        if (first && last) {
+            int count = 0;
+            cur = first;
+            while (cur) {
+                if (!cur->bookmark.parent)
+                    count++;
+                cur = cur->next;
+            }
+
+            /* Bookmark outline */
+            fprintf(fp,
+                    "<<\r\n"
+                    "/Count %d\r\n"
+                    "/Type /Outlines\r\n"
+                    "/First %d 0 R\r\n"
+                    "/Last %d 0 R\r\n"
+                    ">>\r\n",
+                    count, first->index, last->index);
+        }
+        break;
+    }
+
+    case OBJ_font:
+        fprintf(fp,
+                "<<\r\n"
+                "  /Type /Font\r\n"
+                "  /Subtype /Type1\r\n"
+                "  /BaseFont /%s\r\n"
+                "  /Encoding /WinAnsiEncoding\r\n"
+                ">>\r\n",
+                object->font.name);
+        break;
+
+    case OBJ_pages: {
+        int npages = 0;
+
+        fprintf(fp, "<<\r\n"
+                    "/Type /Pages\r\n"
+                    "/Kids [ ");
+        for (struct pdf_object *page = pdf_find_first_object(pdf, OBJ_page);
+             page; page = page->next) {
+            npages++;
+            fprintf(fp, "%d 0 R ", page->index);
+        }
+        fprintf(fp, "]\r\n");
+        fprintf(fp, "/Count %d\r\n", npages);
+        fprintf(fp, ">>\r\n");
+        break;
+    }
+
+    case OBJ_catalog: {
+        struct pdf_object *outline = pdf_find_first_object(pdf, OBJ_outline);
+        struct pdf_object *pages = pdf_find_first_object(pdf, OBJ_pages);
+
+        fprintf(fp, "<<\r\n"
+                    "/Type /Catalog\r\n");
+        if (outline)
+            fprintf(fp,
+                    "/Outlines %d 0 R\r\n"
+                    "/PageMode /UseOutlines\r\n",
+                    outline->index);
+        fprintf(fp,
+                "/Pages %d 0 R\r\n"
+                ">>\r\n",
+                pages->index);
+        break;
+    }
+
+    default:
+        return pdf_set_err(pdf, -EINVAL, "Invalid PDF object type %d",
+                           object->type);
+    }
+
+    fprintf(fp, "endobj\r\n");
+
+    return 0;
+}
+
+int pdf_save_file(struct pdf_doc *pdf, FILE *fp)
+{
+    struct pdf_object *obj;
+    int xref_offset;
+    int xref_count = 0;
+
+    fprintf(fp, "%%PDF-1.2\r\n");
+    /* Hibit bytes */
+    fprintf(fp, "%c%c%c%c%c\r\n", 0x25, 0xc7, 0xec, 0x8f, 0xa2);
+
+    /* Dump all the objects & get their file offsets */
+    for (int i = 0; i < flexarray_size(&pdf->objects); i++)
+        if (pdf_save_object(pdf, fp, i) >= 0)
+            xref_count++;
+
+    /* xref */
+    xref_offset = ftell(fp);
+    fprintf(fp, "xref\r\n");
+    fprintf(fp, "0 %d\r\n", xref_count + 1);
+    fprintf(fp, "0000000000 65535 f\r\n");
+    for (int i = 0; i < flexarray_size(&pdf->objects); i++) {
+        obj = pdf_get_object(pdf, i);
+        if (obj->type != OBJ_none)
+            fprintf(fp, "%10.10d 00000 n\r\n", obj->offset);
+    }
+
+    fprintf(fp,
+            "trailer\r\n"
+            "<<\r\n"
+            "/Size %d\r\n",
+            xref_count + 1);
+    obj = pdf_find_first_object(pdf, OBJ_catalog);
+    fprintf(fp, "/Root %d 0 R\r\n", obj->index);
+    obj = pdf_find_first_object(pdf, OBJ_info);
+    fprintf(fp, "/Info %d 0 R\r\n", obj->index);
+    /* FIXME: Not actually generating a unique ID */
+    fprintf(fp, "/ID [<%16.16x> <%16.16x>]\r\n", 0x123, 0x123);
+    fprintf(fp, ">>\r\n"
+                "startxref\r\n");
+    fprintf(fp, "%d\r\n", xref_offset);
+    fprintf(fp, "%%%%EOF\r\n");
+
+    return 0;
+}
+
+int pdf_save(struct pdf_doc *pdf, const char *filename)
+{
+    FILE *fp;
+    int e;
+
+    if (filename == NULL)
+        fp = stdout;
+    else if ((fp = fopen(filename, "wb")) == NULL)
+        return pdf_set_err(pdf, -errno, "Unable to open '%s': %s", filename,
+                           strerror(errno));
+
+    e = pdf_save_file(pdf, fp);
+
+    if (fp != stdout)
+        if (fclose(fp) != 0 && e >= 0)
+            return pdf_set_err(pdf, -errno, "Unable to close '%s': %s",
+                               filename, strerror(errno));
+
+    return e;
+}
+
+static int pdf_add_stream(struct pdf_doc *pdf, struct pdf_object *page,
+                          const char *buffer)
+{
+    struct pdf_object *obj;
+    int len;
+
+    if (!page)
+        page = pdf_find_last_object(pdf, OBJ_page);
+
+    if (!page)
+        return pdf_set_err(pdf, -EINVAL, "Invalid pdf page");
+
+    len = strlen(buffer);
+    /* We don't want any trailing whitespace in the stream */
+    while (len >= 1 && (buffer[len - 1] == '\r' || buffer[len - 1] == '\n'))
+        len--;
+
+    obj = pdf_add_object(pdf, OBJ_stream);
+    if (!obj)
+        return pdf->errval;
+
+    dstr_printf(&obj->stream, "<< /Length %d >>stream\r\n", len);
+    dstr_append_data(&obj->stream, buffer, len);
+    dstr_append(&obj->stream, "\r\nendstream\r\n");
+
+    return flexarray_append(&page->page.children, obj);
+}
+
+int pdf_add_bookmark(struct pdf_doc *pdf, struct pdf_object *page, int parent,
+                     const char *name)
+{
+    struct pdf_object *obj;
+
+    if (!page)
+        page = pdf_find_last_object(pdf, OBJ_page);
+
+    if (!page)
+        return pdf_set_err(pdf, -EINVAL,
+                           "Unable to add bookmark, no pages available");
+
+    if (!pdf_find_first_object(pdf, OBJ_outline))
+        if (!pdf_add_object(pdf, OBJ_outline))
+            return pdf->errval;
+
+    obj = pdf_add_object(pdf, OBJ_bookmark);
+    if (!obj)
+        return pdf->errval;
+
+    strncpy(obj->bookmark.name, name, sizeof(obj->bookmark.name));
+    obj->bookmark.name[sizeof(obj->bookmark.name) - 1] = '\0';
+    obj->bookmark.page = page;
+    if (parent >= 0) {
+        struct pdf_object *parent_obj = pdf_get_object(pdf, parent);
+        if (!parent_obj)
+            return pdf_set_err(pdf, -EINVAL, "Invalid parent ID %d supplied",
+                               parent);
+        obj->bookmark.parent = parent_obj;
+        flexarray_append(&parent_obj->bookmark.children, obj);
+    }
+
+    return obj->index;
+}
+
+static int utf8_to_utf32(const char *utf8, int len, uint32_t *utf32)
+{
+    uint32_t ch = *utf8;
+    uint8_t mask;
+
+    if ((ch & 0x80) == 0) {
+        len = 1;
+        mask = 0x7f;
+    } else if ((ch & 0xe0) == 0xc0 && len >= 2) {
+        len = 2;
+        mask = 0x1f;
+    } else if ((ch & 0xf0) == 0xe0 && len >= 3) {
+        len = 3;
+        mask = 0xf;
+    } else if ((ch & 0xf8) == 0xf0 && len >= 4) {
+        len = 4;
+        mask = 0x7;
+    } else
+        return -EINVAL;
+
+    ch = 0;
+    for (int i = 0; i < len; i++) {
+        int shift = (len - i - 1) * 6;
+        if (i == 0)
+            ch |= ((uint32_t)(*utf8++) & mask) << shift;
+        else
+            ch |= ((uint32_t)(*utf8++) & 0x3f) << shift;
+    }
+
+    *utf32 = ch;
+
+    return len;
+}
+
+static int pdf_add_text_spacing(struct pdf_doc *pdf, struct pdf_object *page,
+                                const char *text, int size, int xoff,
+                                int yoff, uint32_t colour, double spacing)
+{
+    int ret;
+    int len = text ? strlen(text) : 0;
+    struct dstr str = INIT_DSTR;
+
+    /* Don't bother adding empty/null strings */
+    if (!len)
+        return 0;
+
+    dstr_append(&str, "BT ");
+    dstr_printf(&str, "%d %d TD ", xoff, yoff);
+    dstr_printf(&str, "/F%d %d Tf ", pdf->current_font->font.index, size);
+    dstr_printf(&str, "%f %f %f rg ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%f Tc ", spacing);
+    dstr_append(&str, "(");
+
+    /* Escape magic characters properly */
+    for (int i = 0; i < len;) {
+        uint32_t code;
+        int code_len;
+        code_len = utf8_to_utf32(&text[i], len - i, &code);
+        if (code_len < 0) {
+            dstr_free(&str);
+            return pdf_set_err(pdf, -EINVAL, "Invalid UTF-8 encoding");
+        }
+
+        if (code > 255) {
+            /* We support *some* minimal UTF-8 characters */
+            char buf[5] = {0};
+            switch (code) {
+            case 0x160:
+                buf[0] = (char)0x8a;
+                break;
+            case 0x161:
+                buf[0] = (char)0x9a;
+                break;
+            case 0x17d:
+                buf[0] = (char)0x8e;
+                break;
+            case 0x17e:
+                buf[0] = (char)0x9e;
+                break;
+            case 0x20ac:
+                strcpy(buf, "\\200");
+                break;
+            default:
+                dstr_free(&str);
+                return pdf_set_err(pdf, -EINVAL,
+                                   "Unsupported UTF-8 character: 0x%x 0o%o",
+                                   code, code);
+            }
+            dstr_append(&str, buf);
+        } else if (strchr("()\\", code)) {
+            char buf[3];
+            /* Escape some characters */
+            buf[0] = '\\';
+            buf[1] = code;
+            buf[2] = '\0';
+            dstr_append(&str, buf);
+        } else if (strrchr("\n\r\t\b\f", code)) {
+            /* Skip over these characters */
+            ;
+        } else {
+            char buf[2];
+            buf[0] = code;
+            buf[1] = '\0';
+            dstr_append(&str, buf);
+        }
+
+        i += code_len;
+    }
+    dstr_append(&str, ") Tj ");
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+    return ret;
+}
+
+int pdf_add_text(struct pdf_doc *pdf, struct pdf_object *page,
+                 const char *text, int size, int xoff, int yoff,
+                 uint32_t colour)
+{
+    return pdf_add_text_spacing(pdf, page, text, size, xoff, yoff, colour, 0);
+}
+
+/* How wide is each character, in points, at size 14 */
+static const uint16_t helvetica_widths[256] = {
+    280, 280, 280, 280,  280, 280, 280, 280,  280,  280, 280,  280, 280,
+    280, 280, 280, 280,  280, 280, 280, 280,  280,  280, 280,  280, 280,
+    280, 280, 280, 280,  280, 280, 280, 280,  357,  560, 560,  896, 672,
+    192, 335, 335, 392,  588, 280, 335, 280,  280,  560, 560,  560, 560,
+    560, 560, 560, 560,  560, 560, 280, 280,  588,  588, 588,  560, 1023,
+    672, 672, 727, 727,  672, 615, 784, 727,  280,  504, 672,  560, 839,
+    727, 784, 672, 784,  727, 672, 615, 727,  672,  951, 672,  672, 615,
+    280, 280, 280, 472,  560, 335, 560, 560,  504,  560, 560,  280, 560,
+    560, 223, 223, 504,  223, 839, 560, 560,  560,  560, 335,  504, 280,
+    560, 504, 727, 504,  504, 504, 336, 262,  336,  588, 352,  560, 352,
+    223, 560, 335, 1008, 560, 560, 335, 1008, 672,  335, 1008, 352, 615,
+    352, 352, 223, 223,  335, 335, 352, 560,  1008, 335, 1008, 504, 335,
+    951, 352, 504, 672,  280, 335, 560, 560,  560,  560, 262,  560, 335,
+    742, 372, 560, 588,  335, 742, 335, 403,  588,  335, 335,  335, 560,
+    541, 280, 335, 335,  367, 560, 840, 840,  840,  615, 672,  672, 672,
+    672, 672, 672, 1008, 727, 672, 672, 672,  672,  280, 280,  280, 280,
+    727, 727, 784, 784,  784, 784, 784, 588,  784,  727, 727,  727, 727,
+    672, 672, 615, 560,  560, 560, 560, 560,  560,  896, 504,  560, 560,
+    560, 560, 280, 280,  280, 280, 560, 560,  560,  560, 560,  560, 560,
+    588, 615, 560, 560,  560, 560, 504, 560,  504,
+};
+
+static const uint16_t helvetica_bold_widths[256] = {
+    280,  280, 280,  280, 280, 280, 280, 280,  280, 280, 280, 280,  280, 280,
+    280,  280, 280,  280, 280, 280, 280, 280,  280, 280, 280, 280,  280, 280,
+    280,  280, 280,  280, 280, 335, 477, 560,  560, 896, 727, 239,  335, 335,
+    392,  588, 280,  335, 280, 280, 560, 560,  560, 560, 560, 560,  560, 560,
+    560,  560, 335,  335, 588, 588, 588, 615,  982, 727, 727, 727,  727, 672,
+    615,  784, 727,  280, 560, 727, 615, 839,  727, 784, 672, 784,  727, 672,
+    615,  727, 672,  951, 672, 672, 615, 335,  280, 335, 588, 560,  335, 560,
+    615,  560, 615,  560, 335, 615, 615, 280,  280, 560, 280, 896,  615, 615,
+    615,  615, 392,  560, 335, 615, 560, 784,  560, 560, 504, 392,  282, 392,
+    588,  352, 560,  352, 280, 560, 504, 1008, 560, 560, 335, 1008, 672, 335,
+    1008, 352, 615,  352, 352, 280, 280, 504,  504, 352, 560, 1008, 335, 1008,
+    560,  335, 951,  352, 504, 672, 280, 335,  560, 560, 560, 560,  282, 560,
+    335,  742, 372,  560, 588, 335, 742, 335,  403, 588, 335, 335,  335, 615,
+    560,  280, 335,  335, 367, 560, 840, 840,  840, 615, 727, 727,  727, 727,
+    727,  727, 1008, 727, 672, 672, 672, 672,  280, 280, 280, 280,  727, 727,
+    784,  784, 784,  784, 784, 588, 784, 727,  727, 727, 727, 672,  672, 615,
+    560,  560, 560,  560, 560, 560, 896, 560,  560, 560, 560, 560,  280, 280,
+    280,  280, 615,  615, 615, 615, 615, 615,  615, 588, 615, 615,  615, 615,
+    615,  560, 615,  560,
+};
+
+static const uint16_t helvetica_bold_oblique_widths[256] = {
+    280,  280, 280,  280, 280, 280, 280, 280,  280, 280, 280, 280,  280, 280,
+    280,  280, 280,  280, 280, 280, 280, 280,  280, 280, 280, 280,  280, 280,
+    280,  280, 280,  280, 280, 335, 477, 560,  560, 896, 727, 239,  335, 335,
+    392,  588, 280,  335, 280, 280, 560, 560,  560, 560, 560, 560,  560, 560,
+    560,  560, 335,  335, 588, 588, 588, 615,  982, 727, 727, 727,  727, 672,
+    615,  784, 727,  280, 560, 727, 615, 839,  727, 784, 672, 784,  727, 672,
+    615,  727, 672,  951, 672, 672, 615, 335,  280, 335, 588, 560,  335, 560,
+    615,  560, 615,  560, 335, 615, 615, 280,  280, 560, 280, 896,  615, 615,
+    615,  615, 392,  560, 335, 615, 560, 784,  560, 560, 504, 392,  282, 392,
+    588,  352, 560,  352, 280, 560, 504, 1008, 560, 560, 335, 1008, 672, 335,
+    1008, 352, 615,  352, 352, 280, 280, 504,  504, 352, 560, 1008, 335, 1008,
+    560,  335, 951,  352, 504, 672, 280, 335,  560, 560, 560, 560,  282, 560,
+    335,  742, 372,  560, 588, 335, 742, 335,  403, 588, 335, 335,  335, 615,
+    560,  280, 335,  335, 367, 560, 840, 840,  840, 615, 727, 727,  727, 727,
+    727,  727, 1008, 727, 672, 672, 672, 672,  280, 280, 280, 280,  727, 727,
+    784,  784, 784,  784, 784, 588, 784, 727,  727, 727, 727, 672,  672, 615,
+    560,  560, 560,  560, 560, 560, 896, 560,  560, 560, 560, 560,  280, 280,
+    280,  280, 615,  615, 615, 615, 615, 615,  615, 588, 615, 615,  615, 615,
+    615,  560, 615,  560,
+};
+
+static const uint16_t helvetica_oblique_widths[256] = {
+    280, 280, 280, 280,  280, 280, 280, 280,  280,  280, 280,  280, 280,
+    280, 280, 280, 280,  280, 280, 280, 280,  280,  280, 280,  280, 280,
+    280, 280, 280, 280,  280, 280, 280, 280,  357,  560, 560,  896, 672,
+    192, 335, 335, 392,  588, 280, 335, 280,  280,  560, 560,  560, 560,
+    560, 560, 560, 560,  560, 560, 280, 280,  588,  588, 588,  560, 1023,
+    672, 672, 727, 727,  672, 615, 784, 727,  280,  504, 672,  560, 839,
+    727, 784, 672, 784,  727, 672, 615, 727,  672,  951, 672,  672, 615,
+    280, 280, 280, 472,  560, 335, 560, 560,  504,  560, 560,  280, 560,
+    560, 223, 223, 504,  223, 839, 560, 560,  560,  560, 335,  504, 280,
+    560, 504, 727, 504,  504, 504, 336, 262,  336,  588, 352,  560, 352,
+    223, 560, 335, 1008, 560, 560, 335, 1008, 672,  335, 1008, 352, 615,
+    352, 352, 223, 223,  335, 335, 352, 560,  1008, 335, 1008, 504, 335,
+    951, 352, 504, 672,  280, 335, 560, 560,  560,  560, 262,  560, 335,
+    742, 372, 560, 588,  335, 742, 335, 403,  588,  335, 335,  335, 560,
+    541, 280, 335, 335,  367, 560, 840, 840,  840,  615, 672,  672, 672,
+    672, 672, 672, 1008, 727, 672, 672, 672,  672,  280, 280,  280, 280,
+    727, 727, 784, 784,  784, 784, 784, 588,  784,  727, 727,  727, 727,
+    672, 672, 615, 560,  560, 560, 560, 560,  560,  896, 504,  560, 560,
+    560, 560, 280, 280,  280, 280, 560, 560,  560,  560, 560,  560, 560,
+    588, 615, 560, 560,  560, 560, 504, 560,  504,
+};
+
+static const uint16_t symbol_widths[256] = {
+    252, 252, 252, 252,  252, 252, 252,  252, 252,  252,  252, 252, 252, 252,
+    252, 252, 252, 252,  252, 252, 252,  252, 252,  252,  252, 252, 252, 252,
+    252, 252, 252, 252,  252, 335, 718,  504, 553,  839,  784, 442, 335, 335,
+    504, 553, 252, 553,  252, 280, 504,  504, 504,  504,  504, 504, 504, 504,
+    504, 504, 280, 280,  553, 553, 553,  447, 553,  727,  672, 727, 616, 615,
+    769, 607, 727, 335,  636, 727, 691,  896, 727,  727,  774, 746, 560, 596,
+    615, 695, 442, 774,  650, 801, 615,  335, 869,  335,  663, 504, 504, 636,
+    553, 553, 497, 442,  525, 414, 607,  331, 607,  553,  553, 580, 525, 553,
+    553, 525, 553, 607,  442, 580, 718,  691, 496,  691,  497, 483, 201, 483,
+    553, 0,   0,   0,    0,   0,   0,    0,   0,    0,    0,   0,   0,   0,
+    0,   0,   0,   0,    0,   0,   0,    0,   0,    0,    0,   0,   0,   0,
+    0,   0,   0,   0,    0,   0,   756,  624, 248,  553,  168, 718, 504, 759,
+    759, 759, 759, 1050, 994, 607, 994,  607, 403,  553,  414, 553, 553, 718,
+    497, 463, 553, 553,  553, 553, 1008, 607, 1008, 663,  829, 691, 801, 994,
+    774, 774, 829, 774,  774, 718, 718,  718, 718,  718,  718, 718, 774, 718,
+    796, 796, 897, 829,  553, 252, 718,  607, 607,  1050, 994, 607, 994, 607,
+    497, 331, 796, 796,  792, 718, 387,  387, 387,  387,  387, 387, 497, 497,
+    497, 497, 0,   331,  276, 691, 691,  691, 387,  387,  387, 387, 387, 387,
+    497, 497, 497, 0,
+};
+
+static const uint16_t times_widths[256] = {
+    252, 252, 252, 252, 252, 252, 252, 252,  252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 252, 252, 252,  252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 335, 411, 504,  504, 839, 784, 181,  335, 335,
+    504, 568, 252, 335, 252, 280, 504, 504,  504, 504, 504, 504,  504, 504,
+    504, 504, 280, 280, 568, 568, 568, 447,  928, 727, 672, 672,  727, 615,
+    560, 727, 727, 335, 392, 727, 615, 896,  727, 727, 560, 727,  672, 560,
+    615, 727, 727, 951, 727, 727, 615, 335,  280, 335, 472, 504,  335, 447,
+    504, 447, 504, 447, 335, 504, 504, 280,  280, 504, 280, 784,  504, 504,
+    504, 504, 335, 392, 280, 504, 504, 727,  504, 504, 447, 483,  201, 483,
+    545, 352, 504, 352, 335, 504, 447, 1008, 504, 504, 335, 1008, 560, 335,
+    896, 352, 615, 352, 352, 335, 335, 447,  447, 352, 504, 1008, 335, 987,
+    392, 335, 727, 352, 447, 727, 252, 335,  504, 504, 504, 504,  201, 504,
+    335, 766, 278, 504, 568, 335, 766, 335,  403, 568, 302, 302,  335, 504,
+    456, 252, 335, 302, 312, 504, 756, 756,  756, 447, 727, 727,  727, 727,
+    727, 727, 896, 672, 615, 615, 615, 615,  335, 335, 335, 335,  727, 727,
+    727, 727, 727, 727, 727, 568, 727, 727,  727, 727, 727, 727,  560, 504,
+    447, 447, 447, 447, 447, 447, 672, 447,  447, 447, 447, 447,  280, 280,
+    280, 280, 504, 504, 504, 504, 504, 504,  504, 568, 504, 504,  504, 504,
+    504, 504, 504, 504,
+};
+
+static const uint16_t times_bold_widths[256] = {
+    252, 252, 252, 252,  252, 252, 252, 252,  252,  252,  252,  252,  252,
+    252, 252, 252, 252,  252, 252, 252, 252,  252,  252,  252,  252,  252,
+    252, 252, 252, 252,  252, 252, 252, 335,  559,  504,  504,  1008, 839,
+    280, 335, 335, 504,  574, 252, 335, 252,  280,  504,  504,  504,  504,
+    504, 504, 504, 504,  504, 504, 335, 335,  574,  574,  574,  504,  937,
+    727, 672, 727, 727,  672, 615, 784, 784,  392,  504,  784,  672,  951,
+    727, 784, 615, 784,  727, 560, 672, 727,  727,  1008, 727,  727,  672,
+    335, 280, 335, 585,  504, 335, 504, 560,  447,  560,  447,  335,  504,
+    560, 280, 335, 560,  280, 839, 560, 504,  560,  560,  447,  392,  335,
+    560, 504, 727, 504,  504, 447, 397, 221,  397,  524,  352,  504,  352,
+    335, 504, 504, 1008, 504, 504, 335, 1008, 560,  335,  1008, 352,  672,
+    352, 352, 335, 335,  504, 504, 352, 504,  1008, 335,  1008, 392,  335,
+    727, 352, 447, 727,  252, 335, 504, 504,  504,  504,  221,  504,  335,
+    752, 302, 504, 574,  335, 752, 335, 403,  574,  302,  302,  335,  560,
+    544, 252, 335, 302,  332, 504, 756, 756,  756,  504,  727,  727,  727,
+    727, 727, 727, 1008, 727, 672, 672, 672,  672,  392,  392,  392,  392,
+    727, 727, 784, 784,  784, 784, 784, 574,  784,  727,  727,  727,  727,
+    727, 615, 560, 504,  504, 504, 504, 504,  504,  727,  447,  447,  447,
+    447, 447, 280, 280,  280, 280, 504, 560,  504,  504,  504,  504,  504,
+    574, 504, 560, 560,  560, 560, 504, 560,  504,
+};
+
+static const uint16_t times_bold_italic_widths[256] = {
+    252, 252, 252, 252, 252, 252, 252, 252,  252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 252, 252, 252,  252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 392, 559, 504,  504, 839, 784, 280,  335, 335,
+    504, 574, 252, 335, 252, 280, 504, 504,  504, 504, 504, 504,  504, 504,
+    504, 504, 335, 335, 574, 574, 574, 504,  838, 672, 672, 672,  727, 672,
+    672, 727, 784, 392, 504, 672, 615, 896,  727, 727, 615, 727,  672, 560,
+    615, 727, 672, 896, 672, 615, 615, 335,  280, 335, 574, 504,  335, 504,
+    504, 447, 504, 447, 335, 504, 560, 280,  280, 504, 280, 784,  560, 504,
+    504, 504, 392, 392, 280, 560, 447, 672,  504, 447, 392, 350,  221, 350,
+    574, 352, 504, 352, 335, 504, 504, 1008, 504, 504, 335, 1008, 560, 335,
+    951, 352, 615, 352, 352, 335, 335, 504,  504, 352, 504, 1008, 335, 1008,
+    392, 335, 727, 352, 392, 615, 252, 392,  504, 504, 504, 504,  221, 504,
+    335, 752, 268, 504, 610, 335, 752, 335,  403, 574, 302, 302,  335, 580,
+    504, 252, 335, 302, 302, 504, 756, 756,  756, 504, 672, 672,  672, 672,
+    672, 672, 951, 672, 672, 672, 672, 672,  392, 392, 392, 392,  727, 727,
+    727, 727, 727, 727, 727, 574, 727, 727,  727, 727, 727, 615,  615, 504,
+    504, 504, 504, 504, 504, 504, 727, 447,  447, 447, 447, 447,  280, 280,
+    280, 280, 504, 560, 504, 504, 504, 504,  504, 574, 504, 560,  560, 560,
+    560, 447, 504, 447,
+};
+
+static const uint16_t times_italic_widths[256] = {
+    252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252,  252, 252,
+    252, 252, 252, 252, 252, 335, 423, 504, 504, 839, 784, 215,  335, 335,
+    504, 680, 252, 335, 252, 280, 504, 504, 504, 504, 504, 504,  504, 504,
+    504, 504, 335, 335, 680, 680, 680, 504, 927, 615, 615, 672,  727, 615,
+    615, 727, 727, 335, 447, 672, 560, 839, 672, 727, 615, 727,  615, 504,
+    560, 727, 615, 839, 615, 560, 560, 392, 280, 392, 425, 504,  335, 504,
+    504, 447, 504, 447, 280, 504, 504, 280, 280, 447, 280, 727,  504, 504,
+    504, 504, 392, 392, 280, 504, 447, 672, 447, 447, 392, 403,  277, 403,
+    545, 352, 504, 352, 335, 504, 560, 896, 504, 504, 335, 1008, 504, 335,
+    951, 352, 560, 352, 352, 335, 335, 560, 560, 352, 504, 896,  335, 987,
+    392, 335, 672, 352, 392, 560, 252, 392, 504, 504, 504, 504,  277, 504,
+    335, 766, 278, 504, 680, 335, 766, 335, 403, 680, 302, 302,  335, 504,
+    527, 252, 335, 302, 312, 504, 756, 756, 756, 504, 615, 615,  615, 615,
+    615, 615, 896, 672, 615, 615, 615, 615, 335, 335, 335, 335,  727, 672,
+    727, 727, 727, 727, 727, 680, 727, 727, 727, 727, 727, 560,  615, 504,
+    504, 504, 504, 504, 504, 504, 672, 447, 447, 447, 447, 447,  280, 280,
+    280, 280, 504, 504, 504, 504, 504, 504, 504, 680, 504, 504,  504, 504,
+    504, 447, 504, 447,
+};
+
+static const uint16_t zapfdingbats_widths[256] = {
+    0,   0,   0,   0,   0,    0,   0,   0,   0,   0,   0,   0,   0,   0,
+    0,   0,   0,   0,   0,    0,   0,   0,   0,   0,   0,   0,   0,   0,
+    0,   0,   0,   0,   280,  981, 968, 981, 987, 724, 795, 796, 797, 695,
+    967, 946, 553, 861, 918,  940, 918, 952, 981, 761, 852, 768, 767, 575,
+    682, 769, 766, 765, 760,  497, 556, 541, 581, 697, 792, 794, 794, 796,
+    799, 800, 822, 829, 795,  847, 829, 839, 822, 837, 930, 749, 728, 754,
+    796, 798, 700, 782, 774,  798, 765, 712, 713, 687, 706, 832, 821, 795,
+    795, 712, 692, 701, 694,  792, 793, 718, 797, 791, 797, 879, 767, 768,
+    768, 765, 765, 899, 899,  794, 790, 441, 139, 279, 418, 395, 395, 673,
+    673, 0,   393, 393, 319,  319, 278, 278, 513, 513, 413, 413, 235, 235,
+    336, 336, 0,   0,   0,    0,   0,   0,   0,   0,   0,   0,   0,   0,
+    0,   0,   0,   0,   0,    0,   0,   737, 548, 548, 917, 672, 766, 766,
+    782, 599, 699, 631, 794,  794, 794, 794, 794, 794, 794, 794, 794, 794,
+    794, 794, 794, 794, 794,  794, 794, 794, 794, 794, 794, 794, 794, 794,
+    794, 794, 794, 794, 794,  794, 794, 794, 794, 794, 794, 794, 794, 794,
+    794, 794, 901, 844, 1024, 461, 753, 931, 753, 925, 934, 935, 935, 840,
+    879, 834, 931, 931, 924,  937, 938, 466, 890, 842, 842, 873, 873, 701,
+    701, 880, 0,   880, 766,  953, 777, 871, 777, 895, 974, 895, 837, 879,
+    934, 977, 925, 0,
+};
+
+static const uint16_t courier_widths[256] = {
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604,
+    604,
+};
+
+static int pdf_text_pixel_width(struct pdf_doc *pdf, const char *text,
+                                int text_len, int size,
+                                const uint16_t *widths)
+{
+    int len = 0;
+    if (text_len < 0)
+        text_len = strlen(text);
+
+    for (int i = 0; i < text_len;) {
+        uint32_t code;
+        int code_len;
+        code_len = utf8_to_utf32(&text[i], text_len - i, &code);
+        if (code_len < 0)
+            return pdf_set_err(pdf, code_len,
+                               "Invalid unicode string at position %d in %s",
+                               i, text);
+        if (code >= 255)
+            return pdf_set_err(
+                pdf, code_len,
+                "Unable to determine width of character code %d", code);
+        i += code_len;
+
+        if (code != '\n' && code != '\r')
+            len += widths[(uint8_t)code];
+    }
+
+    /* Our widths arrays are for 14pt fonts */
+    return len * size / (14 * 72);
+}
+
+static const uint16_t *find_font_widths(const char *font_name)
+{
+    if (strcasecmp(font_name, "Helvetica") == 0)
+        return helvetica_widths;
+    if (strcasecmp(font_name, "Helvetica-Bold") == 0)
+        return helvetica_bold_widths;
+    if (strcasecmp(font_name, "Helvetica-BoldOblique") == 0)
+        return helvetica_bold_oblique_widths;
+    if (strcasecmp(font_name, "Helvetica-Oblique") == 0)
+        return helvetica_oblique_widths;
+    if (strcasecmp(font_name, "Courier") == 0 ||
+        strcasecmp(font_name, "Courier-Bold") == 0 ||
+        strcasecmp(font_name, "Courier-BoldOblique") == 0 ||
+        strcasecmp(font_name, "Courier-Oblique") == 0)
+        return courier_widths;
+    if (strcasecmp(font_name, "Times-Roman") == 0)
+        return times_widths;
+    if (strcasecmp(font_name, "Times-Bold") == 0)
+        return times_bold_widths;
+    if (strcasecmp(font_name, "Times-Italic") == 0)
+        return times_italic_widths;
+    if (strcasecmp(font_name, "Times-BoldItalic") == 0)
+        return times_bold_italic_widths;
+    if (strcasecmp(font_name, "Symbol") == 0)
+        return symbol_widths;
+    if (strcasecmp(font_name, "ZapfDingbats") == 0)
+        return zapfdingbats_widths;
+
+    return NULL;
+}
+
+int pdf_get_font_text_width(struct pdf_doc *pdf, const char *font_name,
+                            const char *text, int size)
+{
+    const uint16_t *widths = find_font_widths(font_name);
+
+    if (!widths)
+        return pdf_set_err(pdf, -EINVAL,
+                           "Unable to determine width for font '%s'",
+                           pdf->current_font->font.name);
+    return pdf_text_pixel_width(pdf, text, -1, size, widths);
+}
+
+static const char *find_word_break(const char *string)
+{
+    /* Skip over the actual word */
+    while (string && *string && !isspace(*string))
+        string++;
+
+    return string;
+}
+
+int pdf_add_text_wrap(struct pdf_doc *pdf, struct pdf_object *page,
+                      const char *text, int size, int xoff, int yoff,
+                      uint32_t colour, int wrap_width, int align)
+{
+    /* Move through the text string, stopping at word boundaries,
+     * trying to find the longest text string we can fit in the given width
+     */
+    const char *start = text;
+    const char *last_best = text;
+    const char *end = text;
+    char line[512];
+    const uint16_t *widths;
+    int orig_yoff = yoff;
+
+    widths = find_font_widths(pdf->current_font->font.name);
+    if (!widths)
+        return pdf_set_err(pdf, -EINVAL,
+                           "Unable to determine width for font '%s'",
+                           pdf->current_font->font.name);
+
+    while (start && *start) {
+        const char *new_end = find_word_break(end + 1);
+        int line_width;
+        int output = 0;
+        int xoff_align = xoff;
+
+        end = new_end;
+
+        line_width =
+            pdf_text_pixel_width(pdf, start, end - start, size, widths);
+        if (line_width < 0)
+            return line_width;
+
+        if (line_width >= wrap_width) {
+            if (last_best == start) {
+                /* There is a single word that is too long for the line */
+                int i;
+                /* Find the best character to chop it at */
+                for (i = end - start - 1; i > 0; i--) {
+                    int e = pdf_text_pixel_width(pdf, start, i, size, widths);
+                    if (e < 0)
+                        return e;
+                    if (e < wrap_width)
+                        break;
+                }
+
+                end = start + i;
+            } else
+                end = last_best;
+            output = 1;
+        }
+        if (*end == '\0')
+            output = 1;
+
+        if (*end == '\n' || *end == '\r')
+            output = 1;
+
+        if (output) {
+            int len = end - start;
+            double char_spacing = 0;
+            if (len >= sizeof(line))
+                len = sizeof(line) - 1;
+            strncpy(line, start, len);
+            line[len] = '\0';
+
+            line_width = pdf_text_pixel_width(pdf, start, len, size, widths);
+            if (line_width < 0)
+                return line_width;
+
+            switch (align) {
+            case PDF_ALIGN_RIGHT:
+                xoff_align += wrap_width - line_width;
+                break;
+            case PDF_ALIGN_CENTER:
+                xoff_align += (wrap_width - line_width) / 2;
+                break;
+            case PDF_ALIGN_JUSTIFY:
+                if ((len - 1) > 0 && *end != '\r' && *end != '\n' &&
+                    *end != '\0')
+                    char_spacing =
+                        ((double)(wrap_width - line_width)) / (len - 2);
+                break;
+            case PDF_ALIGN_JUSTIFY_ALL:
+                if ((len - 1) > 0)
+                    char_spacing =
+                        ((double)(wrap_width - line_width)) / (len - 2);
+                break;
+            }
+
+            pdf_add_text_spacing(pdf, page, line, size, xoff_align, yoff,
+                                 colour, char_spacing);
+
+            if (*end == ' ')
+                end++;
+
+            start = last_best = end;
+            yoff -= size;
+        } else
+            last_best = end;
+    }
+
+    return orig_yoff - yoff;
+}
+
+int pdf_add_line(struct pdf_doc *pdf, struct pdf_object *page, int x1, int y1,
+                 int x2, int y2, int width, uint32_t colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "BT\r\n");
+    dstr_printf(&str, "%d w\r\n", width);
+    dstr_printf(&str, "%d %d m\r\n", x1, y1);
+    dstr_printf(&str, "/DeviceRGB CS\r\n");
+    dstr_printf(&str, "%f %f %f RG\r\n", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%d %d l S\r\n", x2, y2);
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+int pdf_add_ellipse(struct pdf_doc *pdf, struct pdf_object *page, int xr,
+                    int yr, int xradius, int yradius, int width,
+                    uint32_t colour, uint32_t fill_colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    float rx = xradius * 1.0f;
+    float ry = yradius * 1.0f;
+    float lx, ly;
+    float x = xr * 1.0f;
+    float y = yr * 1.0f;
+
+    lx = (4.0f / 3.0f) * (M_SQRT2 - 1) * rx;
+    ly = (4.0f / 3.0f) * (M_SQRT2 - 1) * ry;
+
+    if (!PDF_IS_TRANSPARENT(fill_colour)) {
+        dstr_append(&str, "BT ");
+        dstr_printf(&str, "/DeviceRGB CS\r\n");
+        dstr_printf(&str, "%f %f %f rg\r\n", PDF_RGB_R(fill_colour),
+                    PDF_RGB_G(fill_colour), PDF_RGB_B(fill_colour));
+        dstr_append(&str, "ET\r\n");
+    }
+
+    dstr_append(&str, "BT ");
+
+    /* stroke color */
+    dstr_printf(&str, "/DeviceRGB CS\r\n");
+    dstr_printf(&str, "%f %f %f RG\r\n", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+
+    dstr_printf(&str, "%d w ", width);
+
+    dstr_printf(&str, "%.2f %.2f m ", (x + rx), (y));
+
+    dstr_printf(&str, "%.2f %.2f %.2f %.2f %.2f %.2f c ", (x + rx), (y - ly),
+                (x + lx), (y - ry), x, (y - ry));
+
+    dstr_printf(&str, "%.2f %.2f %.2f %.2f %.2f %.2f c ", (x - lx), (y - ry),
+                (x - rx), (y - ly), (x - rx), y);
+
+    dstr_printf(&str, "%.2f %.2f %.2f %.2f %.2f %.2f c ", (x - rx), (y + ly),
+                (x - lx), (y + ry), x, (y + ry));
+
+    dstr_printf(&str, "%.2f %.2f %.2f %.2f %.2f %.2f c ", (x + lx), (y + ry),
+                (x + rx), (y + ly), (x + rx), y);
+
+    if (PDF_IS_TRANSPARENT(fill_colour))
+        dstr_printf(&str, "%s", "S ");
+    else
+        dstr_printf(&str, "%s", "B ");
+
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+int pdf_add_circle(struct pdf_doc *pdf, struct pdf_object *page, int xr,
+                   int yr, int radius, int width, uint32_t colour,
+                   uint32_t fill_colour)
+{
+    return pdf_add_ellipse(pdf, page, xr, yr, radius, radius, width, colour,
+                           fill_colour);
+}
+
+int pdf_add_rectangle(struct pdf_doc *pdf, struct pdf_object *page, int x,
+                      int y, int width, int height, int border_width,
+                      uint32_t colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "BT ");
+    dstr_printf(&str, "%f %f %f RG ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%d w ", border_width);
+    dstr_printf(&str, "%d %d %d %d re S ", x, y, width, height);
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+int pdf_add_filled_rectangle(struct pdf_doc *pdf, struct pdf_object *page,
+                             int x, int y, int width, int height,
+                             int border_width, uint32_t colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "BT ");
+    dstr_printf(&str, "%f %f %f rg ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%d w ", border_width);
+    dstr_printf(&str, "%d %d %d %d re f ", x, y, width, height);
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+int pdf_add_polygon(struct pdf_doc *pdf, struct pdf_object *page, int x[],
+                    int y[], int count, int border_width, uint32_t colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "BT ");
+    dstr_printf(&str, "%f %f %f RG ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%d w ", border_width);
+    dstr_printf(&str, "%d %d m ", x[0], y[0]);
+    for (int i = 1; i < count; i++) {
+        dstr_printf(&str, "%d %d l ", x[i], y[i]);
+    }
+    dstr_printf(&str, "h S ");
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+int pdf_add_filled_polygon(struct pdf_doc *pdf, struct pdf_object *page,
+                           int x[], int y[], int count, int border_width,
+                           uint32_t colour)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "BT ");
+    dstr_printf(&str, "%f %f %f RG ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%f %f %f rg ", PDF_RGB_R(colour), PDF_RGB_G(colour),
+                PDF_RGB_B(colour));
+    dstr_printf(&str, "%d w ", border_width);
+    dstr_printf(&str, "%d %d m ", x[0], y[0]);
+    for (int i = 1; i < count; i++) {
+        dstr_printf(&str, "%d %d l ", x[i], y[i]);
+    }
+    dstr_printf(&str, "h f ");
+    dstr_append(&str, "ET");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+
+    return ret;
+}
+
+static const struct {
+    uint32_t code;
+    char ch;
+} code_128a_encoding[] = {
+    {0x212222, ' '},  {0x222122, '!'},  {0x222221, '"'},   {0x121223, '#'},
+    {0x121322, '$'},  {0x131222, '%'},  {0x122213, '&'},   {0x122312, '\''},
+    {0x132212, '('},  {0x221213, ')'},  {0x221312, '*'},   {0x231212, '+'},
+    {0x112232, ','},  {0x122132, '-'},  {0x122231, '.'},   {0x113222, '/'},
+    {0x123122, '0'},  {0x123221, '1'},  {0x223211, '2'},   {0x221132, '3'},
+    {0x221231, '4'},  {0x213212, '5'},  {0x223112, '6'},   {0x312131, '7'},
+    {0x311222, '8'},  {0x321122, '9'},  {0x321221, ':'},   {0x312212, ';'},
+    {0x322112, '<'},  {0x322211, '='},  {0x212123, '>'},   {0x212321, '?'},
+    {0x232121, '@'},  {0x111323, 'A'},  {0x131123, 'B'},   {0x131321, 'C'},
+    {0x112313, 'D'},  {0x132113, 'E'},  {0x132311, 'F'},   {0x211313, 'G'},
+    {0x231113, 'H'},  {0x231311, 'I'},  {0x112133, 'J'},   {0x112331, 'K'},
+    {0x132131, 'L'},  {0x113123, 'M'},  {0x113321, 'N'},   {0x133121, 'O'},
+    {0x313121, 'P'},  {0x211331, 'Q'},  {0x231131, 'R'},   {0x213113, 'S'},
+    {0x213311, 'T'},  {0x213131, 'U'},  {0x311123, 'V'},   {0x311321, 'W'},
+    {0x331121, 'X'},  {0x312113, 'Y'},  {0x312311, 'Z'},   {0x332111, '['},
+    {0x314111, '\\'}, {0x221411, ']'},  {0x431111, '^'},   {0x111224, '_'},
+    {0x111422, '`'},  {0x121124, 'a'},  {0x121421, 'b'},   {0x141122, 'c'},
+    {0x141221, 'd'},  {0x112214, 'e'},  {0x112412, 'f'},   {0x122114, 'g'},
+    {0x122411, 'h'},  {0x142112, 'i'},  {0x142211, 'j'},   {0x241211, 'k'},
+    {0x221114, 'l'},  {0x413111, 'm'},  {0x241112, 'n'},   {0x134111, 'o'},
+    {0x111242, 'p'},  {0x121142, 'q'},  {0x121241, 'r'},   {0x114212, 's'},
+    {0x124112, 't'},  {0x124211, 'u'},  {0x411212, 'v'},   {0x421112, 'w'},
+    {0x421211, 'x'},  {0x212141, 'y'},  {0x214121, 'z'},   {0x412121, '{'},
+    {0x111143, '|'},  {0x111341, '}'},  {0x131141, '~'},   {0x114113, '\0'},
+    {0x114311, '\0'}, {0x411113, '\0'}, {0x411311, '\0'},  {0x113141, '\0'},
+    {0x114131, '\0'}, {0x311141, '\0'}, {0x411131, '\0'},  {0x211412, '\0'},
+    {0x211214, '\0'}, {0x211232, '\0'}, {0x2331112, '\0'},
+};
+
+static int find_128_encoding(char ch)
+{
+    for (int i = 0; i < ARRAY_SIZE(code_128a_encoding); i++) {
+        if (code_128a_encoding[i].ch == ch)
+            return i;
+    }
+    return -1;
+}
+
+static int pdf_barcode_128a_ch(struct pdf_doc *pdf, struct pdf_object *page,
+                               int x, int y, int width, int height,
+                               uint32_t colour, int index, int code_len)
+{
+    uint32_t code = code_128a_encoding[index].code;
+    int line_width = width / 11;
+
+    for (int i = 0; i < code_len; i++) {
+        uint8_t shift = (code_len - 1 - i) * 4;
+        uint8_t mask = (code >> shift) & 0xf;
+
+        if (!(i % 2))
+            pdf_add_filled_rectangle(pdf, page, x, y, line_width * mask,
+                                     height, 0, colour);
+        x += line_width * mask;
+    }
+    return x;
+}
+
+static int pdf_add_barcode_128a(struct pdf_doc *pdf, struct pdf_object *page,
+                                int x, int y, int width, int height,
+                                const char *string, uint32_t colour)
+{
+    const char *s;
+    int len = strlen(string) + 3;
+    int char_width = width / len;
+    int checksum, i;
+
+    if (char_width / 11 <= 0)
+        return pdf_set_err(pdf, -EINVAL,
+                           "Insufficient width to draw barcode");
+
+    for (s = string; *s; s++)
+        if (find_128_encoding(*s) < 0)
+            return pdf_set_err(pdf, -EINVAL, "Invalid barcode character 0x%x",
+                               *s);
+
+    x = pdf_barcode_128a_ch(pdf, page, x, y, char_width, height, colour, 104,
+                            6);
+    checksum = 104;
+
+    for (i = 1, s = string; *s; s++, i++) {
+        int index = find_128_encoding(*s);
+        x = pdf_barcode_128a_ch(pdf, page, x, y, char_width, height, colour,
+                                index, 6);
+        checksum += index * i;
+    }
+    x = pdf_barcode_128a_ch(pdf, page, x, y, char_width, height, colour,
+                            checksum % 103, 6);
+    pdf_barcode_128a_ch(pdf, page, x, y, char_width, height, colour, 106, 7);
+    return 0;
+}
+
+/* Code 39 character encoding. Each 4-bit value indicates:
+ * 0 => wide bar
+ * 1 => narrow bar
+ * 2 => wide space
+ */
+static const struct {
+    uint32_t code;
+    char ch;
+} code_39_encoding[] = {
+    {0x012110, '1'}, {0x102110, '2'}, {0x002111, '3'},
+    {0x112010, '4'}, {0x012011, '5'}, {0x102011, '6'},
+    {0x112100, '7'}, {0x012101, '8'}, {0x102101, '9'},
+    {0x112001, '0'}, {0x011210, 'A'}, {0x101210, 'B'},
+    {0x001211, 'C'}, {0x110210, 'D'}, {0x010211, 'E'},
+    {0x100211, 'F'}, {0x111200, 'G'}, {0x011201, 'H'},
+    {0x101201, 'I'}, {0x110201, 'J'}, {0x011120, 'K'},
+    {0x101120, 'L'}, {0x001121, 'M'}, {0x110120, 'N'},
+    {0x010121, 'O'}, {0x100121, 'P'}, {0x111020, 'Q'},
+    {0x011021, 'R'}, {0x101021, 'S'}, {0x110021, 'T'},
+    {0x021110, 'U'}, {0x120110, 'V'}, {0x020111, 'W'},
+    {0x121010, 'X'}, {0x021011, 'Y'}, {0x120011, 'Z'},
+    {0x121100, '-'}, {0x021101, '.'}, {0x120101, ' '},
+    {0x121001, '*'}, // 'stop' character
+};
+
+static int pdf_barcode_39_ch(struct pdf_doc *pdf, struct pdf_object *page,
+                             int x, int y, int char_width, int height,
+                             uint32_t colour, char ch)
+{
+    int nw = char_width / 12;
+    int ww = char_width / 4;
+    int i;
+    uint32_t code;
+
+    if (nw <= 1 || ww <= 1)
+        return pdf_set_err(pdf, -EINVAL,
+                           "Insufficient width for each character");
+
+    for (i = 0; i < ARRAY_SIZE(code_39_encoding); i++) {
+        if (code_39_encoding[i].ch == ch) {
+            code = code_39_encoding[i].code;
+            break;
+        }
+    }
+    if (i == ARRAY_SIZE(code_39_encoding))
+        return pdf_set_err(pdf, -EINVAL, "Invalid Code 39 character %c 0x%x",
+                           ch, ch);
+
+    for (i = 5; i >= 0; i--) {
+        int pattern = (code >> i * 4) & 0xf;
+        if (pattern == 0) { // wide
+            if (pdf_add_filled_rectangle(pdf, page, x, y, ww - 1, height, 0,
+                                         colour) < 0)
+                return pdf->errval;
+            x += ww;
+        }
+        if (pattern == 1) { // narrow
+            if (pdf_add_filled_rectangle(pdf, page, x, y, nw - 1, height, 0,
+                                         colour) < 0)
+                return pdf->errval;
+            x += nw;
+        }
+        if (pattern == 2) { // space
+            x += nw;
+        }
+    }
+    return x;
+}
+
+static int pdf_add_barcode_39(struct pdf_doc *pdf, struct pdf_object *page,
+                              int x, int y, int width, int height,
+                              const char *string, uint32_t colour)
+{
+    int len = strlen(string);
+    int char_width = width / (len + 2);
+
+    x = pdf_barcode_39_ch(pdf, page, x, y, char_width, height, colour, '*');
+    if (x < 0)
+        return x;
+
+    while (string && *string) {
+        x = pdf_barcode_39_ch(pdf, page, x, y, char_width, height, colour,
+                              *string);
+        if (x < 0)
+            return x;
+        string++;
+    };
+
+    x = pdf_barcode_39_ch(pdf, page, x, y, char_width, height, colour, '*');
+    if (x < 0)
+        return x;
+
+    return 0;
+}
+
+int pdf_add_barcode(struct pdf_doc *pdf, struct pdf_object *page, int code,
+                    int x, int y, int width, int height, const char *string,
+                    uint32_t colour)
+{
+    if (!string || !*string)
+        return 0;
+    switch (code) {
+    case PDF_BARCODE_128A:
+        return pdf_add_barcode_128a(pdf, page, x, y, width, height, string,
+                                    colour);
+    case PDF_BARCODE_39:
+        return pdf_add_barcode_39(pdf, page, x, y, width, height, string,
+                                  colour);
+    default:
+        return pdf_set_err(pdf, -EINVAL, "Invalid barcode code %d", code);
+    }
+}
+
+static pdf_object *pdf_add_raw_rgb24(struct pdf_doc *pdf, uint8_t *data,
+                                     int width, int height)
+{
+    struct pdf_object *obj;
+    int len;
+    const char *endstream = ">\r\nendstream\r\n";
+
+    obj = pdf_add_object(pdf, OBJ_image);
+    if (!obj)
+        return NULL;
+
+    dstr_printf(
+        &obj->stream,
+        "<<\r\n/Type /XObject\r\n/Name /Image%d\r\n/Subtype /Image\r\n"
+        "/ColorSpace /DeviceRGB\r\n/Height %d\r\n/Width %d\r\n"
+        "/BitsPerComponent 8\r\n/Filter /ASCIIHexDecode\r\n"
+        "/Length %d\r\n>>stream\r\n",
+        flexarray_size(&pdf->objects), height, width,
+        width * height * 3 * 2 + 1);
+
+    len = dstr_len(&obj->stream) + width * height * 3 * 2 +
+          strlen(endstream) + 1;
+    if (dstr_ensure(&obj->stream, len) < 0) {
+        pdf_set_err(pdf, -ENOMEM,
+                    "Unable to allocate %d bytes memory for image", len);
+        return NULL;
+    }
+    for (int i = 0; i < width * height * 3; i++) {
+        char buf[2] = {"0123456789ABCDEF"[(data[i] >> 4) & 0xf],
+                       "0123456789ABCDEF"[data[i] & 0xf]};
+        dstr_append_data(&obj->stream, buf, 2);
+    }
+    dstr_append(&obj->stream, endstream);
+
+    return obj;
+}
+
+/* See http://www.64lines.com/jpeg-width-height for details */
+static int jpeg_size(unsigned char *data, unsigned int data_size, int *width,
+                     int *height)
+{
+    int i = 0;
+    if (i + 3 < data_size && data[i] == 0xFF && data[i + 1] == 0xD8 &&
+        data[i + 2] == 0xFF && data[i + 3] == 0xE0) {
+        i += 4;
+        if (i + 6 < data_size && data[i + 2] == 'J' && data[i + 3] == 'F' &&
+            data[i + 4] == 'I' && data[i + 5] == 'F' && data[i + 6] == 0x00) {
+            unsigned short block_length = data[i] * 256 + data[i + 1];
+            while (i < data_size) {
+                i += block_length;
+                if ((i + 8) >= data_size)
+                    return -1;
+                if (data[i] != 0xFF)
+                    return -1;
+                if (data[i + 1] == 0xC0) {
+                    *height = data[i + 5] * 256 + data[i + 6];
+                    *width = data[i + 7] * 256 + data[i + 8];
+                    return 0;
+                }
+                i += 2;
+                block_length = data[i] * 256 + data[i + 1];
+            }
+        }
+    }
+
+    return -1;
+}
+
+// MT: added this function
+static pdf_object *pdf_add_raw_jpeg_direct
+    ( struct pdf_doc *pdf
+    , uint8_t *jpeg_data
+    , size_t len
+    , int width
+    , int height
+    )
+{
+    struct pdf_object *obj;
+
+    obj = pdf_add_object(pdf, OBJ_image);
+    if (!obj)
+        return NULL;
+    dstr_printf(&obj->stream,
+                "<<\r\n/Type /XObject\r\n/Name /Image%d\r\n"
+                "/Subtype /Image\r\n/ColorSpace /DeviceRGB\r\n"
+                "/Width %d\r\n/Height %d\r\n"
+                "/BitsPerComponent 8\r\n/Filter /DCTDecode\r\n"
+                "/Length %d\r\n>>stream\r\n",
+                flexarray_size(&pdf->objects), width, height, (int)len);
+    dstr_append_data(&obj->stream, jpeg_data, len);
+
+    dstr_printf(&obj->stream, "\r\nendstream\r\n");
+
+    return obj;
+}
+
+static pdf_object *pdf_add_raw_jpeg(struct pdf_doc *pdf,
+                                    const char *jpeg_file)
+{
+    struct stat buf;
+    size_t len;
+    uint8_t *jpeg_data;
+    FILE *fp;
+    struct pdf_object *obj;
+    int width, height;
+
+    if (stat(jpeg_file, &buf) < 0) {
+        pdf_set_err(pdf, -errno, "Unable to access %s: %s", jpeg_file,
+                    strerror(errno));
+        return NULL;
+    }
+
+    len = buf.st_size;
+
+    if ((fp = fopen(jpeg_file, "rb")) == NULL) {
+        pdf_set_err(pdf, -errno, "Unable to open %s: %s", jpeg_file,
+                    strerror(errno));
+        return NULL;
+    }
+
+    jpeg_data = malloc(len);
+    if (!jpeg_data) {
+        pdf_set_err(pdf, -errno, "Unable to allocate: %zd", len);
+        fclose(fp);
+        return NULL;
+    }
+
+    if (fread(jpeg_data, len, 1, fp) != 1) {
+        pdf_set_err(pdf, -errno, "Unable to read full jpeg data");
+        free(jpeg_data);
+        fclose(fp);
+        return NULL;
+    }
+    fclose(fp);
+
+    if (jpeg_size(jpeg_data, len, &width, &height) < 0) {
+        free(jpeg_data);
+        pdf_set_err(pdf, -EINVAL,
+                    "Unable to determine jpeg width/height from %s",
+                    jpeg_file);
+        return NULL;
+    }
+
+    obj = pdf_add_object(pdf, OBJ_image);
+    if (!obj)
+        return NULL;
+    dstr_printf(&obj->stream,
+                "<<\r\n/Type /XObject\r\n/Name /Image%d\r\n"
+                "/Subtype /Image\r\n/ColorSpace /DeviceRGB\r\n"
+                "/Width %d\r\n/Height %d\r\n"
+                "/BitsPerComponent 8\r\n/Filter /DCTDecode\r\n"
+                "/Length %d\r\n>>stream\r\n",
+                flexarray_size(&pdf->objects), width, height, (int)len);
+    dstr_append_data(&obj->stream, jpeg_data, len);
+
+    dstr_printf(&obj->stream, "\r\nendstream\r\n");
+
+    free(jpeg_data);
+
+    return obj;
+}
+
+static int pdf_add_image(struct pdf_doc *pdf, struct pdf_object *page,
+                         struct pdf_object *image, int x, int y, int width,
+                         int height)
+{
+    int ret;
+    struct dstr str = INIT_DSTR;
+
+    dstr_append(&str, "q ");
+    dstr_printf(&str, "%d 0 0 %d %d %d cm ", width, height, x, y);
+    dstr_printf(&str, "/Image%d Do ", image->index);
+    dstr_append(&str, "Q");
+
+    ret = pdf_add_stream(pdf, page, dstr_data(&str));
+    dstr_free(&str);
+    return ret;
+}
+
+int pdf_add_ppm(struct pdf_doc *pdf, struct pdf_object *page, int x, int y,
+                int display_width, int display_height, const char *ppm_file)
+{
+    struct pdf_object *obj;
+    uint8_t *data;
+    FILE *fp;
+    char line[1024];
+    unsigned width, height, size;
+
+    /* Load the PPM file */
+    fp = fopen(ppm_file, "rb");
+    if (!fp)
+        return pdf_set_err(pdf, -errno, "Unable to open '%s'", ppm_file);
+    if (!fgets(line, sizeof(line) - 1, fp)) {
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL, "Invalid PPM file");
+    }
+
+    /* We only support binary ppms */
+    if (strncmp(line, "P6", 2) != 0) {
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL, "Only binary PPM files supported");
+    }
+
+    /* Skip the header comments until we get to the dimensions info */
+    do {
+        if (!fgets(line, sizeof(line) - 1, fp)) {
+            fclose(fp);
+            return pdf_set_err(pdf, -EINVAL, "Unable to find PPM size");
+        }
+        if (line[0] != '#')
+            break;
+    } while (1);
+
+    if (sscanf(line, "%u %u\n", &width, &height) != 2) {
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL, "Unable to find PPM size");
+    }
+
+    /* Skip over the byte-size line */
+    if (!fgets(line, sizeof(line) - 1, fp)) {
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL, "No byte-size line in PPM file");
+    }
+
+    /* Try and limit the memory usage to sane images */
+    if (width > 2 << 14 || height > 2 << 14) {
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL,
+                           "Invalid width/height in PPM file: %ux%u", width,
+                           height);
+    }
+
+    size = width * height * 3;
+    data = malloc(size);
+    if (!data) {
+        fclose(fp);
+        return pdf_set_err(pdf, -ENOMEM,
+                           "Unable to allocate memory for RGB data");
+    }
+    if (fread(data, 1, size, fp) != size) {
+        free(data);
+        fclose(fp);
+        return pdf_set_err(pdf, -EINVAL, "Insufficient RGB data available");
+    }
+    fclose(fp);
+    obj = pdf_add_raw_rgb24(pdf, data, width, height);
+    free(data);
+    if (!obj)
+        return pdf->errval;
+
+    return pdf_add_image(pdf, page, obj, x, y, display_width, display_height);
+}
+
+int pdf_add_jpeg(struct pdf_doc *pdf, struct pdf_object *page, int x, int y,
+                 int display_width, int display_height, const char *jpeg_file)
+{
+    struct pdf_object *obj;
+
+    obj = pdf_add_raw_jpeg(pdf, jpeg_file);
+    if (!obj)
+        return pdf->errval;
+
+    return pdf_add_image(pdf, page, obj, x, y, display_width, display_height);
+}
+
+// MT: added this function
+int pdf_add_jpeg_direct(struct pdf_doc *pdf, struct pdf_object *page, int w, int h,
+                 int x, int y, int display_width, int display_height, uint8_t *jpeg_data, size_t len)
+{
+    struct pdf_object *obj;
+
+    obj = pdf_add_raw_jpeg_direct(pdf, jpeg_data, len, w, h);
+    if (!obj)
+        return pdf->errval;
+
+    return pdf_add_image(pdf, page, obj, x, y, display_width, display_height);
+}
+
+// MT: added this function
+struct pdf_doc *pdf_create_nostruct(int width, int height,
+    char *creator,
+    char *producer,
+    char *title,
+    char *author,
+    char *subject,
+    char *date)
+{
+    struct pdf_info info = {0};
+    strncpy(info.creator, creator, 63);
+    strncpy(info.producer, producer, 63);
+    strncpy(info.title, title, 63);
+    strncpy(info.author, author, 63);
+    strncpy(info.subject, subject, 63);
+    strncpy(info.date, date, 63);
+    return pdf_create(width, height, &info);
+}
diff --git a/cbits/pdfgen.h b/cbits/pdfgen.h
new file mode 100644
--- /dev/null
+++ b/cbits/pdfgen.h
@@ -0,0 +1,456 @@
+/**
+ * Simple engine for creating PDF files.
+ * It supports text, shapes, images etc...
+ * Capable of handling millions of objects without too much performance
+ * penalty.
+ * Public domain license - no warrenty implied; use at your own risk.
+ */
+#ifndef PDFGEN_H
+#define PDFGEN_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * @defgroup subsystem Simple PDF Generation
+ * Allows for quick generation of simple PDF documents.
+ * This is useful for producing easily printed output from C code, where
+ * advanced formatting is not required
+ *
+ * Note: All coordinates/sizes are in points (1/72 of an inch).
+ * All coordinates are based on 0,0 being the bottom left of the page.
+ * All colours are specified as a packed 32-bit value - see @ref PDF_RGB.
+ * Text strings are interpreted as UTF-8 encoded, but only a small subset of
+ * characters beyond 7-bit ascii are supported (see @ref pdf_add_text for
+ * details).
+ *
+ * @par PDF library example:
+ * @code
+#include "pdfgen.h"
+ ...
+struct pdf_info info = {
+         .creator = "My software",
+         .producer = "My software",
+         .title = "My document",
+         .author = "My name",
+         .subject = "My subject",
+         .date = "Today"
+         };
+struct pdf_doc *pdf = pdf_create(PDF_A4_WIDTH, PDF_A4_HEIGHT, &info);
+pdf_set_font(pdf, "Times-Roman");
+pdf_append_page(pdf);
+pdf_add_text(pdf, NULL, "This is text", 12, 50, 20);
+pdf_add_line(pdf, NULL, 50, 24, 150, 24);
+pdf_save(pdf, "output.pdf");
+pdf_destroy(pdf);
+ * @endcode
+ */
+
+struct pdf_doc;
+struct pdf_object;
+
+struct pdf_info {
+    char creator[64];
+    char producer[64];
+    char title[64];
+    char author[64];
+    char subject[64];
+    char date[64];
+};
+
+/**
+ * Convert a value in inches into a number of points.
+ * Always returns an integer value
+ */
+#define PDF_INCH_TO_POINT(inch) ((int)((inch)*72 + 0.5))
+
+/**
+ * Convert a value in milli-meters into a number of points.
+ * Always returns an integer value
+ */
+#define PDF_MM_TO_POINT(mm) ((int)((mm)*72 / 25.4 + 0.5))
+
+/**
+ * Point width of a standard US-Letter page
+ */
+#define PDF_LETTER_WIDTH PDF_INCH_TO_POINT(8.5)
+
+/**
+ * Point height of a standard US-Letter page
+ */
+#define PDF_LETTER_HEIGHT PDF_INCH_TO_POINT(11)
+
+/**
+ * Point width of a standard A4 page
+ */
+#define PDF_A4_WIDTH PDF_MM_TO_POINT(210)
+
+/**
+ * Point height of a standard A4 page
+ */
+#define PDF_A4_HEIGHT PDF_MM_TO_POINT(297)
+
+/**
+ * Point width of a standard A3 page
+ */
+#define PDF_A3_WIDTH PDF_MM_TO_POINT(297)
+
+/**
+ * Point height of a standard A3 page
+ */
+#define PDF_A3_HEIGHT PDF_MM_TO_POINT(420)
+
+/**
+ * Convert three 8-bit RGB values into a single packed 32-bit
+ * colour. These 32-bit colours are used by various functions
+ * in PDFGen
+ */
+#define PDF_RGB(r, g, b)                                                     \
+    ((((r)&0xff) << 16) | (((g)&0xff) << 8) | (((b)&0xff)))
+
+/**
+ * Utility macro to provide bright red
+ */
+#define PDF_RED PDF_RGB(0xff, 0, 0)
+
+/**
+ * Utility macro to provide bright green
+ */
+#define PDF_GREEN PDF_RGB(0, 0xff, 0)
+
+/**
+ * Utility macro to provide bright blue
+ */
+#define PDF_BLUE PDF_RGB(0, 0, 0xff)
+
+/**
+ * Utility macro to provide black
+ */
+#define PDF_BLACK PDF_RGB(0, 0, 0)
+
+/**
+ * Utility macro to provide white
+ */
+#define PDF_WHITE PDF_RGB(0xff, 0xff, 0xff)
+
+/**
+ * Utility macro to provide a transparent colour
+ * This is used in some places for 'fill' colours, where no fill is required
+ */
+#define PDF_TRANSPARENT (0xff << 24)
+
+enum {
+    PDF_ALIGN_LEFT,
+    PDF_ALIGN_RIGHT,
+    PDF_ALIGN_CENTER,
+    PDF_ALIGN_JUSTIFY,
+    PDF_ALIGN_JUSTIFY_ALL, // Like PDF_ALIGN_JUSTIFY, except even short lines
+                           // will be fully justified
+};
+
+/**
+ * Create a new PDF object, with the given page
+ * width/height
+ * @param width Width of the page
+ * @param height Height of the page
+ * @param info Optional information to be put into the PDF header
+ * @return PDF document object, or NULL on failure
+ */
+struct pdf_doc *pdf_create(int width, int height, struct pdf_info *info);
+
+/**
+ * Destroy the pdf object, and all of its associated memory
+ */
+void pdf_destroy(struct pdf_doc *pdf);
+
+/**
+ * Retrieve the error message if any operation fails
+ * @param pdf pdf document to retrieve error message from
+ * @param errval optional pointer to an integer to be set to the error code
+ * @return NULL if no error message, string description of error otherwise
+ */
+const char *pdf_get_err(struct pdf_doc *pdf, int *errval);
+
+/**
+ * Acknowledge an outstanding pdf error
+ * @param pdf pdf document to clear the error message from
+ */
+void pdf_clear_err(struct pdf_doc *pdf);
+
+/**
+ * Sets the font to use for text objects. Default value is Times-Roman if
+ * this function is not called
+ * Note: The font selection should be done before text is output,
+ * and will remain until pdf_set_font is called again
+ * @param pdf PDF document to update font on
+ * @param font New font to use. This must be one of the standard PDF fonts:
+ *  Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
+ *  Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
+ *  Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic,
+ *  Symbol or ZapfDingbats
+ * @return < 0 on failure, 0 on success
+ */
+int pdf_set_font(struct pdf_doc *pdf, const char *font);
+
+/**
+ * Returns the width of a given string in the current font
+ * @param pdf PDF document
+ * @param font_name Name of the font to get the width of.
+ *  This must be one of the standard PDF fonts:
+ *  Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
+ *  Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
+ *  Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic,
+ *  Symbol or ZapfDingbats
+ * @param text Text to determine width of
+ * @param size Size of the text, in points
+ * @return < 0 on failure, 0 on success
+ */
+int pdf_get_font_text_width(struct pdf_doc *pdf, const char *font_name,
+                            const char *text, int size);
+
+/**
+ * Retrieves a PDF document height
+ */
+int pdf_height(struct pdf_doc *pdf);
+
+/**
+ * Retrieves a PDF document width
+ */
+int pdf_width(struct pdf_doc *pdf);
+
+/**
+ * Add a new page to the given pdf
+ * @return new page object
+ */
+struct pdf_object *pdf_append_page(struct pdf_doc *pdf);
+
+/**
+ * Adjust the width/height of a specific page
+ * @param pdf PDF document that the page belongs to
+ * @param page object returned from @ref pdf_append_page
+ * @param width Width of the page in points
+ * @param height Height of the page in points
+ * @return < 0 on failure, 0 on success
+ */
+int pdf_page_set_size(struct pdf_doc *pdf, struct pdf_object *page, int width,
+                      int height);
+
+/**
+ * Save the given pdf document to the supplied filename
+ * If the filename is NULL, defaults to stdout
+ */
+int pdf_save(struct pdf_doc *pdf, const char *filename);
+
+/**
+ * Save the given pdf document to the given FILE output
+ */
+int pdf_save_file(struct pdf_doc *pdf, FILE *fp);
+
+/**
+ * Add a text string to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param text String to display
+ * @param size Point size of the font
+ * @param xoff X location to put it in
+ * @param yoff Y location to put it in
+ * @param colour Colour to draw the text
+ * @return 0 on success, < 0 on failure
+ */
+int pdf_add_text(struct pdf_doc *pdf, struct pdf_object *page,
+                 const char *text, int size, int xoff, int yoff,
+                 uint32_t colour);
+
+/**
+ * Add a text string to the document, making it wrap if it is too
+ * long
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param text String to display
+ * @param size Point size of the font
+ * @param xoff X location to put it in
+ * @param yoff Y location to put it in
+ * @param colour Colour to draw the text
+ * @param wrap_width Width at which to wrap the text
+ * @param align Text alignment (see PDF_ALIGN_xxx)
+ * @return height of drawn text on success, < 0 on failure
+ */
+int pdf_add_text_wrap(struct pdf_doc *pdf, struct pdf_object *page,
+                      const char *text, int size, int xoff, int yoff,
+                      uint32_t colour, int wrap_width, int align);
+
+/**
+ * Add a line to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x1 X offset of start of line
+ * @param y1 Y offset of start of line
+ * @param x2 X offset of end of line
+ * @param y2 Y offset of end of line
+ * @param width Width of the line
+ * @param colour Colour to draw the line
+ * @return 0 on success, < 0 on failure
+ */
+int pdf_add_line(struct pdf_doc *pdf, struct pdf_object *page, int x1, int y1,
+                 int x2, int y2, int width, uint32_t colour);
+
+/**
+ * Add an ellipse to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x X offset of the center of the ellipse
+ * @param y Y offset of the center of the ellipse
+ * @param xradius Radius of the ellipse in the X axis
+ * @param yradius Radius of the ellipse in the Y axis
+ * @param colour Colour to draw the ellipse outline stroke
+ * @param width Width of the ellipse outline stroke
+ * @param fill_colour Colour to fill the ellipse
+ * @return 0 on success, < 0 on failure
+ */
+int pdf_add_ellipse(struct pdf_doc *pdf, struct pdf_object *page, int x,
+                    int y, int xradius, int yradius, int width,
+                    uint32_t colour, uint32_t fill_colour);
+
+/**
+ * Add a circle to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x X offset of the center of the circle
+ * @param y Y offset of the center of the circle
+ * @param radius Radius of the circle
+ * @param width Width of the circle outline stroke
+ * @param colour Colour to draw the circle outline stroke
+ * @param fill_colour Colour to fill the circle
+ * @return 0 on success, < 0 on failure
+ */
+int pdf_add_circle(struct pdf_doc *pdf, struct pdf_object *page, int x, int y,
+                   int radius, int width, uint32_t colour,
+                   uint32_t fill_colour);
+
+/**
+ * Add an outline rectangle to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x X offset to start rectangle at
+ * @param y Y offset to start rectangle at
+ * @param width Width of rectangle
+ * @param height Height of rectangle
+ * @param border_width Width of rectangle border
+ * @param colour Colour to draw the rectangle
+ * @return 0 on succss, < 0 on failure
+ */
+int pdf_add_rectangle(struct pdf_doc *pdf, struct pdf_object *page, int x,
+                      int y, int width, int height, int border_width,
+                      uint32_t colour);
+
+/**
+ * Add a filled rectangle to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x X offset to start rectangle at
+ * @param y Y offset to start rectangle at
+ * @param width Width of rectangle
+ * @param height Height of rectangle
+ * @param border_width Width of rectangle border
+ * @param colour Colour to draw the rectangle
+ * @return 0 on succss, < 0 on failure
+ */
+int pdf_add_filled_rectangle(struct pdf_doc *pdf, struct pdf_object *page,
+                             int x, int y, int width, int height,
+                             int border_width, uint32_t colour);
+
+/**
+ * Add an outline polygon to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x array of X offsets for points comprising the polygon
+ * @param y array of Y offsets for points comprising the polygon
+ * @param count Number of points comprising the polygon
+ * @param border_width Width of polygon border
+ * @param colour Colour to draw the polygon
+ * @return 0 on succss, < 0 on failure
+ */
+int pdf_add_polygon(struct pdf_doc *pdf, struct pdf_object *page, int x[],
+                    int y[], int count, int border_width, uint32_t colour);
+
+/**
+ * Add a filled polygon to the document
+ * @param pdf PDF document to add to
+ * @param page Page to add object to (NULL => most recently added page)
+ * @param x array of X offsets of points comprising the polygon
+ * @param y array of Y offsets of points comprising the polygon
+ * @param count Number of points comprising the polygon
+ * @param border_width Width of polygon border
+ * @param colour Colour to draw the polygon
+ * @return 0 on succss, < 0 on failure
+ */
+int pdf_add_filled_polygon(struct pdf_doc *pdf, struct pdf_object *page,
+                           int x[], int y[], int count, int border_width,
+                           uint32_t colour);
+
+/**
+ * Add a bookmark to the document
+ *
+ * @param pdf PDF document to add bookmark to
+ * @param page Page to jump to for bookmark
+               (or NULL for the most recently added page)
+ * @param parent ID of a previosly created bookmark that is the parent
+               of this one. -1 if this should be a top-level bookmark.
+ * @param name String to associate with the bookmark
+ * @return < 0 on failure, new bookmark id on success
+ */
+int pdf_add_bookmark(struct pdf_doc *pdf, struct pdf_object *page, int parent,
+                     const char *name);
+
+/**
+ * List of different barcode encodings that are supported
+ */
+enum {
+    PDF_BARCODE_128A,
+    PDF_BARCODE_39,
+};
+
+/**
+ * Add a barcode to the document
+ * @param pdf PDF document to add bookmark to
+ * @param page Page to add barcode to (NULL => most recently added page)
+ * @param code Type of barcode to add (PDF_BARCODE_xxx)
+ * @param x X offset to put barcode at
+ * @param y Y offset to put barcode at
+ * @param width Width of barcode
+ * @param height Height of barcode
+ * @param string Barcode contents
+ * @param colour Colour to draw barcode
+ */
+int pdf_add_barcode(struct pdf_doc *pdf, struct pdf_object *page, int code,
+                    int x, int y, int width, int height, const char *string,
+                    uint32_t colour);
+
+/**
+ * Add a PPM file as an image to the document
+ * @param pdf PDF document to add bookmark to
+ * @param page Page to add PPM to (NULL => most recently added page)
+ * @param x X offset to put PPM at
+ * @param y Y offset to put PPM at
+ * @param display_width Displayed width of image
+ * @param display_height Displayed height of image
+ * @param ppm_file Filename of P6 (binary) ppm file to display
+ */
+int pdf_add_ppm(struct pdf_doc *pdf, struct pdf_object *page, int x, int y,
+                int display_width, int display_height, const char *ppm_file);
+
+/**
+ * Add a JPEG file as an image to the document
+ * @param pdf PDF document to add bookmark to
+ * @param page Page to add PPM to (NULL => most recently added page)
+ * @param x X offset to put JPEG at
+ * @param y Y offset to put JPEG at
+ * @param display_width Displayed width of image
+ * @param display_height Displayed height of image
+ * @param jpeg_file Filename of JPEG file to display
+ */
+int pdf_add_jpeg(struct pdf_doc *pdf, struct pdf_object *page, int x, int y,
+                 int display_width, int display_height,
+                 const char *jpeg_file);
+
+#endif // PDFGEN_H
diff --git a/jammittools.cabal b/jammittools.cabal
--- a/jammittools.cabal
+++ b/jammittools.cabal
@@ -1,5 +1,5 @@
 Name:               jammittools
-Version:            0.5.5.1
+Version:            0.5.5.2
 Synopsis:           Export sheet music and audio from Windows/Mac app Jammit
 Description:
 
@@ -16,13 +16,14 @@
 Author:             Michael Tolly
 Maintainer:         miketolly@gmail.com
 Build-Type:         Simple
-Cabal-Version:      >= 1.8
+Cabal-Version:      >= 1.10
 Category:           Sound
 Homepage:           https://github.com/mtolly/jammittools
 Bug-Reports:        https://github.com/mtolly/jammittools/issues
 Extra-Source-Files:
   README.md
   CHANGELOG.md
+  cbits/pdfgen.h
 
 library
   exposed-modules:
@@ -31,7 +32,6 @@
   other-modules:
     Sound.Jammit.Internal.Audio
     Sound.Jammit.Internal.Image
-    Sound.Jammit.Internal.TempIO
     Sound.Jammit.Internal.PropertyList
   hs-source-dirs:   src
   build-depends:
@@ -40,19 +40,24 @@
     , text          >= 1.2.1.1   && < 1.3
     , directory     >= 1.2.0.1   && < 1.4
     , filepath      >= 1.3.0.1   && < 1.5
-    , containers    >= 0.5.0.0   && < 0.6
+    , containers    >= 0.5.0.0   && < 0.7
     , process       >= 1.1.0.2   && < 1.7
-    , temporary     >= 1.1.2.5   && < 1.4
     , transformers  >= 0.3.0.0   && < 0.6
-    , JuicyPixels   >= 3.2.7     && < 3.3
-    , HPDF          >= 1.4.9     && < 1.5
+    , JuicyPixels   >= 3.2.7     && < 3.4
     , bytestring    >= 0.10.4.0  && < 0.11
     , conduit       >= 1.2.3.1   && < 1.4
     , vector        >= 0.10.12.2 && < 0.13
     , conduit-audio >= 0.1       && < 0.3
     , resourcet     >= 1.1.7.2   && < 1.3
+  if os(windows)
+    build-depends: Win32
+    cpp-options: -DWINDOWS
   ghc-options:      -Wall -O2
-  c-sources:        cbits/ima4.c
+  c-sources:
+    cbits/ima4.c
+    cbits/pdfgen.c
+  cc-options: -DSKIP_ATTRIBUTE
+  default-language: Haskell2010
 
 executable jammittools
   main-is:          Main.hs
@@ -65,6 +70,7 @@
     , boxes         >= 0.1.3   && < 0.2
     , jammittools
   ghc-options:      -Wall -O2
+  default-language: Haskell2010
 
 source-repository head
   type:             git
diff --git a/src/Sound/Jammit/Export.hs b/src/Sound/Jammit/Export.hs
--- a/src/Sound/Jammit/Export.hs
+++ b/src/Sound/Jammit/Export.hs
@@ -26,7 +26,6 @@
 import           Sound.Jammit.Base
 import           Sound.Jammit.Internal.Audio
 import           Sound.Jammit.Internal.Image
-import           Sound.Jammit.Internal.TempIO
 import           System.Directory             (getDirectoryContents)
 import           System.FilePath              (splitFileName, takeFileName,
                                                (</>))
@@ -124,15 +123,13 @@
   -> Int                   -- ^ how many sheet music systems per page
   -> FilePath              -- ^ the resulting PDF
   -> IO ()
-runSheet trks lns fout = runTempIO fout $ do
-  trkLns <- liftIO $ forM trks $ \(fp, ht) -> do
+runSheet trks lns fout = do
+  trkLns <- forM trks $ \(fp, ht) -> do
     let (dir, file) = splitFileName fp
     ls <- getDirectoryContents dir
     return (map (dir </>) $ sort $ filter (file `isPrefixOf`) ls, ht)
   jpegs <- partsToPages trkLns lns
-  pdf <- newTempFile "pages.pdf"
-  liftIO $ jpegsToPDF jpegs pdf
-  return pdf
+  jpegsToPDF jpegs fout
 
 writeMetronomeTrack :: FilePath -> [Beat] -> IO ()
 writeMetronomeTrack fp beats = runResourceT $ writeWAV fp $ metronomeTrack beats
diff --git a/src/Sound/Jammit/Internal/Audio.hs b/src/Sound/Jammit/Internal/Audio.hs
--- a/src/Sound/Jammit/Internal/Audio.hs
+++ b/src/Sound/Jammit/Internal/Audio.hs
@@ -63,21 +63,24 @@
             x <- f end
             liftIO $ IO.hSetPosn here
             return x
+      expect x act = act >>= \y -> if x == y
+        then return ()
+        else error $ "Expected " <> show x <> " but found " <> show y
   frames <- IO.withBinaryFile fp IO.ReadMode $ \h -> do
     let chunkBefore = insideChunk h
     "FORM" `chunkBefore` Nothing $ \formEnd -> do
-      "AIFC" <- liftIO $ B.hGet h 4
+      expect "AIFC" $ liftIO $ B.hGet h 4
       "COMM" `chunkBefore` Just formEnd $ \_ -> do
-        2 <- liftIO (readBE h :: IO Word16) -- channels
+        expect 2 $ liftIO (readBE h :: IO Word16) -- channels
         frames <- liftIO (readBE h :: IO Word32) -- number of chunk pairs
         bits <- liftIO (readBE h :: IO Word16) -- bits per sample, 0 means 16?
         unless (bits `elem` [0, 16]) $ error "readIMA: bits per sample not 16 or 0"
         -- next 10 bytes are sample rate as long float
         -- for now we just compare to the known 44100
-        0x400eac44 <- liftIO (readBE h :: IO Word32)
-        0 <- liftIO (readBE h :: IO Word32)
-        0 <- liftIO (readBE h :: IO Word16)
-        "ima4" <- liftIO $ B.hGet h 4
+        expect 0x400eac44 $ liftIO (readBE h :: IO Word32)
+        expect 0 $ liftIO (readBE h :: IO Word32)
+        expect 0 $ liftIO (readBE h :: IO Word16)
+        expect "ima4" $ liftIO $ B.hGet h 4
         return frames
   let src = C.bracketP
         (IO.openBinaryFile fp IO.ReadMode)
@@ -85,10 +88,10 @@
         $ \h -> do
           let chunkBefore = insideChunk h
           "FORM" `chunkBefore` Nothing $ \formEnd -> do
-            "AIFC" <- liftIO $ B.hGet h 4
+            expect "AIFC" $ liftIO $ B.hGet h 4
             "SSND" `chunkBefore` Just formEnd $ \_ -> do
-              0 <- liftIO (readBE h :: IO Word32) -- offset
-              0 <- liftIO (readBE h :: IO Word32) -- blocksize
+              expect 0 $ liftIO (readBE h :: IO Word32) -- offset
+              expect 0 $ liftIO (readBE h :: IO Word32) -- blocksize
               let go _     _     0         = return ()
                   go predL predR remFrames = do
                     chunkL <- liftIO $ B.hGet h 34
diff --git a/src/Sound/Jammit/Internal/Image.hs b/src/Sound/Jammit/Internal/Image.hs
--- a/src/Sound/Jammit/Internal/Image.hs
+++ b/src/Sound/Jammit/Internal/Image.hs
@@ -1,21 +1,69 @@
+{-# LANGUAGE CPP #-}
 module Sound.Jammit.Internal.Image
 ( partsToPages
 , jpegsToPDF
 ) where
 
-import qualified Codec.Picture                as P
-import           Codec.Picture.Types          (convertImage)
-import           Control.Monad                (forM_, replicateM)
-import           Control.Monad.IO.Class       (MonadIO)
-import           Control.Monad.Trans.Class    (lift)
-import qualified Data.ByteString.Lazy         as BL
-import           Data.Conduit                 ((.|))
-import qualified Data.Conduit                 as C
-import           Data.Maybe                   (catMaybes)
-import qualified Data.Vector.Storable         as V
-import qualified Graphics.PDF                 as PDF
-import           Sound.Jammit.Internal.TempIO
+import qualified Codec.Picture          as P
+import           Codec.Picture.Jpg      (encodeJpegAtQuality)
+import           Codec.Picture.Types    (convertImage)
+import           Control.Exception      (bracket)
+import           Control.Monad          (forM_, replicateM)
+import           Control.Monad.IO.Class (MonadIO (..))
+import qualified Data.ByteString        as B
+import qualified Data.ByteString.Lazy   as BL
+import           Data.Conduit           ((.|))
+import qualified Data.Conduit           as C
+import           Data.Conduit.List      (consume)
+import           Data.Maybe             (catMaybes)
+import qualified Data.Vector.Storable   as V
+import           Foreign
+import           Foreign.C
 
+#ifdef WINDOWS
+import           System.Win32.Info      (getShortPathName)
+#endif
+
+-- data PDFInfo
+data PDFDoc
+data PDFObject
+
+-- foreign import ccall unsafe "pdf_create"
+--   pdf_create :: CInt -> CInt -> Ptr PDFInfo -> IO (Ptr PDFDoc)
+
+foreign import ccall unsafe "pdf_create_nostruct"
+  pdf_create_nostruct :: CInt -> CInt
+    -> CString -> CString -> CString -> CString -> CString -> CString
+    -> IO (Ptr PDFDoc)
+
+foreign import ccall unsafe "pdf_append_page"
+  pdf_append_page :: Ptr PDFDoc -> IO (Ptr PDFObject)
+
+foreign import ccall unsafe "pdf_page_set_size"
+  pdf_page_set_size :: Ptr PDFDoc -> Ptr PDFObject -> CInt -> CInt -> IO CInt
+
+-- foreign import ccall unsafe "pdf_add_jpeg"
+--   pdf_add_jpeg :: Ptr PDFDoc -> Ptr PDFObject -> CInt -> CInt -> CInt -> CInt -> CString -> IO CInt
+
+foreign import ccall unsafe "pdf_add_jpeg_direct"
+  pdf_add_jpeg_direct
+    :: Ptr PDFDoc
+    -> Ptr PDFObject
+    -> CInt -> CInt
+    -> CInt -> CInt
+    -> CInt -> CInt
+    -> Ptr Word8 -> CSize
+    -> IO CInt
+
+foreign import ccall unsafe "pdf_save"
+  pdf_save :: Ptr PDFDoc -> CString -> IO CInt
+
+foreign import ccall unsafe "pdf_destroy"
+  pdf_destroy :: Ptr PDFDoc -> IO ()
+
+foreign import ccall unsafe "pdf_get_err"
+  pdf_get_err :: Ptr PDFDoc -> Ptr CInt -> IO CString
+
 loadPNG :: FilePath -> IO (P.Image P.PixelRGB8)
 loadPNG fp = do
   Right dyn <- P.readImage fp
@@ -43,25 +91,14 @@
   [] -> return ()
   _  -> C.yield (vertConcat $ concat systems) >> chunksToPages n
 
-sinkJPEG :: C.ConduitT (P.Image P.PixelRGB8) C.Void TempIO [FilePath]
-sinkJPEG = go [] where
-  go jpegs = C.await >>= \x -> case x of
-    Nothing -> return jpegs
-    Just img -> do
-      jpeg <- lift $ newTempFile "page.jpg"
-      liftIO $ saveJPEG jpeg img
-      go $ jpegs ++ [jpeg]
-
 partsToPages
   :: [([FilePath], Integer)] -- ^ [(images, system height)]
   -> Int -- ^ systems per page
-  -> TempIO [FilePath]
+  -> IO [P.Image P.PixelRGB8]
+partsToPages []    _ = return []
 partsToPages parts n = let
   sources = map (\(imgs, h) -> pngChunks (fromIntegral h) imgs) parts
-  in C.runConduit $ C.sequenceSources sources .| chunksToPages n .| sinkJPEG
-
-saveJPEG :: FilePath -> P.Image P.PixelRGB8 -> IO ()
-saveJPEG fp img = BL.writeFile fp $ P.encodeJpegAtQuality 100 $ convertImage img
+  in C.runConduit $ C.sequenceSources sources .| chunksToPages n .| consume
 
 vertConcat :: [P.Image P.PixelRGB8] -> P.Image P.PixelRGB8
 vertConcat [] = P.Image 0 0 V.empty
@@ -103,16 +140,34 @@
       }
     in first : vertSplit h rest
 
-imagePage :: PDF.JpegFile -> PDF.PDF ()
-imagePage jpeg = do
-  let (w, h) = PDF.jpegBounds jpeg
-  page <- PDF.addPage $ Just $ PDF.PDFRect 0 0 w h
-  ref <- PDF.createPDFJpeg jpeg
-  PDF.drawWithPage page $ PDF.drawXObject ref
-
-jpegsToPDF :: [FilePath] -> FilePath -> IO ()
+jpegsToPDF :: [P.Image P.PixelRGB8] -> FilePath -> IO ()
 jpegsToPDF [] _ = return () -- Buddy Rich "Love for Sale" Kick channel
-jpegsToPDF jpegs pdf = do
-  Right js <- fmap sequence $ mapM PDF.readJpegFile jpegs
-  PDF.runPdf pdf PDF.standardDocInfo (PDF.PDFRect 0 0 600 400) $
-    forM_ js imagePage
+jpegsToPDF jpegs pdf = let
+  inch i = round $ (i :: Rational) * 72
+  pageWidth = inch 8.5
+  pageHeight = inch 11
+  in withCString "" $ \mt -> do
+    bracket (pdf_create_nostruct pageWidth pageHeight mt mt mt mt mt mt) pdf_destroy $ \doc -> do
+      let check fn = fn >>= \ret -> case ret of
+            0 -> return ()
+            e -> do
+              str <- pdf_get_err doc nullPtr >>= peekCString
+              error $ "PDF generation error (" <> show e <> "): " <> str
+      forM_ jpegs $ \jpeg@(P.Image w h _) -> do
+        let thisHeight = round $ (toRational h / toRational w) * toRational pageWidth
+        page <- pdf_append_page doc
+        check $ pdf_page_set_size doc page pageWidth thisHeight
+        let bs = encodeJpegAtQuality 100 $ convertImage jpeg
+        check $ B.useAsCStringLen (BL.toStrict bs) $ \(p, len) -> pdf_add_jpeg_direct
+          doc page
+          (fromIntegral w) (fromIntegral h)
+          0 0
+          pageWidth thisHeight
+          (castPtr p) (fromIntegral len)
+#ifdef WINDOWS
+      B.writeFile pdf B.empty -- file has to exist before you get short name
+      pdf' <- getShortPathName pdf
+#else
+      let pdf' = pdf
+#endif
+      check $ withCString pdf' $ pdf_save doc
diff --git a/src/Sound/Jammit/Internal/PropertyList.hs b/src/Sound/Jammit/Internal/PropertyList.hs
--- a/src/Sound/Jammit/Internal/PropertyList.hs
+++ b/src/Sound/Jammit/Internal/PropertyList.hs
@@ -58,10 +58,11 @@
 value e = case asParent e of
   ("array", elts) -> Array <$> mapM value elts
   ("dict" , elts) -> Dict . Map.fromList <$> go elts where
-    go (x : y : xs) = do
-      ("key", k) <- asChild x
-      v <- value y
-      ((k, v) :) <$> go xs
+    go (x : y : xs) = asChild x >>= \case
+      ("key", k) -> do
+        v <- value y
+        ((k, v) :) <$> go xs
+      _ -> Left "(value) expected <key> at start of <dict>"
     go [] = Right []
     go _  = Left "(value) odd number of children when parsing a dict"
   ("true" , []) -> Right $ Bool True
diff --git a/src/Sound/Jammit/Internal/TempIO.hs b/src/Sound/Jammit/Internal/TempIO.hs
deleted file mode 100644
--- a/src/Sound/Jammit/Internal/TempIO.hs
+++ /dev/null
@@ -1,41 +0,0 @@
--- | A wrapper around IO that allows you to treat temporary files like
--- garbage-collected values.
-module Sound.Jammit.Internal.TempIO
-( TempIO
-, runTempIO
-, newTempFile
-, ask
-, liftIO
-) where
-
-import           Control.Monad.IO.Class     (liftIO)
-import           Control.Monad.Trans.Reader (ReaderT (..), ask)
-import           Data.List                  (stripPrefix)
-import           System.Directory           (copyFile, renameFile)
-import           System.FilePath            (splitPath)
-import           System.IO                  (hClose)
-import           System.IO.Error            (catchIOError)
-import           System.IO.Temp             (openTempFile,
-                                             withSystemTempDirectory)
-
--- | A wrapper around IO with a designated directory for temporary files.
-type TempIO = ReaderT FilePath IO
-
--- | Creates a new temporary directory to run a computation in. When finished,
--- the final file will be moved/copied to the given path, and the temporary
--- directory will be deleted.
-runTempIO :: FilePath -> TempIO FilePath -> IO ()
-runTempIO fout act = withSystemTempDirectory "tempfile" $ \tmp -> do
-  res <- runReaderT act tmp
-  case stripPrefix (splitPath tmp) (splitPath res) of
-    Just f | ".." `notElem` f -> -- try rename if we know the file is in tmp
-      catchIOError (renameFile res fout) $ \_ -> copyFile res fout
-    _ -> copyFile res fout
-
--- | Creates a new file in the temporary directory, given a template.
-newTempFile :: String -> TempIO FilePath
-newTempFile pat = do
-  tmp <- ask
-  (f, h) <- liftIO $ openTempFile tmp pat
-  liftIO $ hClose h
-  return f
