From ea3f9b67e5e4e4b3453df55af9758a94c023d6f7 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 14:50:05 +0200 Subject: pdf: minimal PDF writer for generated documents --- THIRD_PARTY_NOTICES.md | 6 + src/pdf.c | 517 +++++++++++++++++++++++++++++++++++++++++++++++++ src/pdf.h | 27 +++ tests/pdf_check.c | 160 +++++++++++++++ 4 files changed, 710 insertions(+) create mode 100644 src/pdf.c create mode 100644 src/pdf.h create mode 100644 tests/pdf_check.c diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7e10b64..6b0897f 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -14,6 +14,12 @@ BAS-kontoplanen and other Swedish accounting material shipped under `data/` are the property of FAR and are distributed under their own terms; they are not covered by the GPL of this repository. +The advance-width tables in `src/pdf.c` are the standard Adobe Core 14 +Helvetica and Helvetica-Bold font metrics (the values published in the +freely redistributable AFM files, cross-checked against the metrically +compatible URW Nimbus Sans AFMs). No font outlines or font software are +included. + The chart files `data/bas_k2.csv` and `data/bas_k3.csv` were derived from the Swedish localisation chart data of Odoo 18.0 (https://github.com/odoo/odoo, `addons/l10n_se/data/template/`), which is diff --git a/src/pdf.c b/src/pdf.c new file mode 100644 index 0000000..c6ae876 --- /dev/null +++ b/src/pdf.c @@ -0,0 +1,517 @@ +#include "pdf.h" + +#include +#include +#include +#include +#include + +#include "util.h" + +#define PDF_PAGE_H 842.0 +#define PDF_ASCENT 718.0 + +/* Adobe Core 14 AFM advance widths, indexed by WinAnsi code point. */ +static const unsigned short pdf_width_h[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, + 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, + 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, + 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, + 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, + 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, + 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0, + 556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, + 0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667, + 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, + 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, + 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, + 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, + 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278, + 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500, +}; + +static const unsigned short pdf_width_hb[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, + 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, + 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, + 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, + 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, + 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, + 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0, + 556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0, + 0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667, + 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, + 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, + 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, + 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, + 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278, + 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556, +}; + +struct pdf { + struct buf content; + int page; +}; + +static const unsigned short *widths_for(const char *font) +{ + return font && strcmp(font, "HB") == 0 ? pdf_width_hb : pdf_width_h; +} + +static uint32_t utf8_next(const unsigned char **pp) +{ + const unsigned char *p = *pp; + uint32_t cp; + int extra, min; + unsigned char c = *p++; + + if (c < 0x80) { + cp = c; + extra = 0; + min = 0; + } else if ((c & 0xE0) == 0xC0) { + cp = c & 0x1F; + extra = 1; + min = 0x80; + } else if ((c & 0xF0) == 0xE0) { + cp = c & 0x0F; + extra = 2; + min = 0x800; + } else if ((c & 0xF8) == 0xF0) { + cp = c & 0x07; + extra = 3; + min = 0x10000; + } else { + *pp = p; + return 0xFFFD; + } + for (int i = 0; i < extra; i++) { + if ((*p & 0xC0) != 0x80) { + *pp = p; + return 0xFFFD; + } + cp = (cp << 6) | (*p++ & 0x3F); + } + *pp = p; + if (cp < (uint32_t)min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) + return 0xFFFD; + return cp; +} + +static int winansi_byte(uint32_t cp) +{ + if ((cp >= 0x20 && cp < 0x7F) || (cp >= 0xA0 && cp <= 0xFF)) + return (int)cp; + switch (cp) { + case 0x20AC: return 0x80; + case 0x201A: return 0x82; + case 0x0192: return 0x83; + case 0x201E: return 0x84; + case 0x2026: return 0x85; + case 0x2020: return 0x86; + case 0x2021: return 0x87; + case 0x02C6: return 0x88; + case 0x2030: return 0x89; + case 0x0160: return 0x8A; + case 0x2039: return 0x8B; + case 0x0152: return 0x8C; + case 0x017D: return 0x8E; + case 0x2018: return 0x91; + case 0x2019: return 0x92; + case 0x201C: return 0x93; + case 0x201D: return 0x94; + case 0x2022: return 0x95; + case 0x2013: return 0x96; + case 0x2014: return 0x97; + case 0x02DC: return 0x98; + case 0x2122: return 0x99; + case 0x0161: return 0x9A; + case 0x203A: return 0x9B; + case 0x0153: return 0x9C; + case 0x017E: return 0x9E; + case 0x0178: return 0x9F; + default: return -1; + } +} + +static void bprintf(struct buf *b, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +static void bprintf(struct buf *b, const char *fmt, ...) +{ + char tmp[512]; + va_list ap; + int n; + + va_start(ap, fmt); + n = vsnprintf(tmp, sizeof tmp, fmt, ap); + va_end(ap); + if (n < 0) + return; + if ((size_t)n >= sizeof tmp) + n = (int)sizeof tmp - 1; + buf_append(b, tmp, (size_t)n); +} + +static void numfmt(char *out, size_t n, double v) +{ + long long t; + unsigned long long a, ip, fp; + const char *sign; + + if (!(v > -1e12 && v < 1e12)) + v = 0.0; + t = (long long)(v * 1000.0 + (v < 0 ? -0.5 : 0.5)); + a = t < 0 ? (unsigned long long)(-t) : (unsigned long long)t; + ip = a / 1000; + fp = a % 1000; + sign = t < 0 ? "-" : ""; + if (!fp) + snprintf(out, n, "%s%llu", sign, ip); + else if (!(fp % 100)) + snprintf(out, n, "%s%llu.%llu", sign, ip, fp / 100); + else if (!(fp % 10)) + snprintf(out, n, "%s%llu.%02llu", sign, ip, fp / 10); + else + snprintf(out, n, "%s%llu.%03llu", sign, ip, fp); +} + +static void badd(struct buf *b, const char *s) +{ + buf_append(b, s, strlen(s)); +} + +static void c_add(struct pdf *p, const char *s) +{ + badd(&p->content, s); +} + +static void c_addn(struct pdf *p, double v) +{ + char tmp[40]; + numfmt(tmp, sizeof tmp, v); + c_add(p, tmp); +} + +static void parse_rgb(const char *rgb, double out[3]) +{ + unsigned v[3] = { 0, 0, 0 }; + + out[0] = out[1] = out[2] = 0.0; + if (!rgb || rgb[0] != '#' || strlen(rgb) < 7) + return; + for (int i = 0; i < 6; i++) { + int c = (unsigned char)rgb[1 + i]; + int d; + if (c >= '0' && c <= '9') + d = c - '0'; + else if (c >= 'a' && c <= 'f') + d = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + d = c - 'A' + 10; + else + return; + v[i / 2] = v[i / 2] * 16 + (unsigned)d; + } + out[0] = v[0] / 255.0; + out[1] = v[1] / 255.0; + out[2] = v[2] / 255.0; +} + +static void c_rgb(struct pdf *p, const char *rgb, int stroke) +{ + double c[3]; + parse_rgb(rgb, c); + c_addn(p, c[0]); + c_add(p, " "); + c_addn(p, c[1]); + c_add(p, " "); + c_addn(p, c[2]); + c_add(p, stroke ? " RG\n" : " rg\n"); +} + +static double flip(double y) +{ + return PDF_PAGE_H - y; +} + +static unsigned char *utf8_to_winansi(const char *s, size_t *len) +{ + size_t cap = strlen(s) + 1; + unsigned char *out = xmalloc(cap); + size_t n = 0; + + for (const unsigned char *p = (const unsigned char *)s; *p;) { + uint32_t cp = utf8_next(&p); + int c = winansi_byte(cp); + out[n++] = c < 0 ? '?' : (unsigned char)c; + } + *len = n; + return out; +} + +struct pdf *pdf_new(void) +{ + struct pdf *p = xcalloc(1, sizeof *p); + buf_init(&p->content); + return p; +} + +int pdf_page(struct pdf *p) +{ + if (!p || p->page) + return -1; + p->page = 1; + return 0; +} + +void pdf_fill_rect(struct pdf *p, double x, double y, double w, double h, + const char *rgb) +{ + if (!p || w <= 0 || h <= 0) + return; + c_rgb(p, rgb, 0); + c_addn(p, x); + c_add(p, " "); + c_addn(p, flip(y + h)); + c_add(p, " "); + c_addn(p, w); + c_add(p, " "); + c_addn(p, h); + c_add(p, " re f\n"); +} + +void pdf_line(struct pdf *p, double x1, double y1, double x2, double y2, + double width, const char *rgb) +{ + if (!p || width <= 0) + return; + c_rgb(p, rgb, 1); + c_addn(p, x1); + c_add(p, " "); + c_addn(p, flip(y1)); + c_add(p, " m "); + c_addn(p, x2); + c_add(p, " "); + c_addn(p, flip(y2)); + c_add(p, " l "); + c_addn(p, width); + c_add(p, " w S\n"); +} + +void pdf_text(struct pdf *p, double x, double baseline, const char *font, + double size, const char *rgb, const char *utf8) +{ + unsigned char *s; + size_t n; + struct buf esc; + + if (!p || !utf8 || !*utf8 || size <= 0) + return; + s = utf8_to_winansi(utf8, &n); + buf_init(&esc); + for (size_t i = 0; i < n; i++) { + if (s[i] == '(' || s[i] == ')' || s[i] == '\\') + buf_append(&esc, "\\", 1); + buf_append(&esc, &s[i], 1); + } + c_rgb(p, rgb, 0); + c_add(p, "BT /"); + c_add(p, font && strcmp(font, "HB") == 0 ? "F2" : "F1"); + c_add(p, " "); + c_addn(p, size); + c_add(p, " Tf "); + c_addn(p, x); + c_add(p, " "); + c_addn(p, flip(baseline)); + c_add(p, " Td ("); + buf_append(&p->content, esc.p, esc.len); + c_add(p, ") Tj ET\n"); + buf_free(&esc); + free(s); +} + +double pdf_text_width(const char *font, double size, const char *utf8) +{ + const unsigned short *w = widths_for(font); + double total = 0.0; + + if (!utf8 || size <= 0) + return 0.0; + for (const unsigned char *p = (const unsigned char *)utf8; *p;) { + int c = winansi_byte(utf8_next(&p)); + total += w[c < 0 ? (int)'?' : c]; + } + return total * size / 1000.0; +} + +double pdf_font_ascent(const char *font, double size) +{ + (void)font; + return size > 0 ? size * PDF_ASCENT / 1000.0 : 0.0; +} + +static int parse_num(const char **sp, double *out) +{ + const char *s = *sp; + double v = 0.0, scale = 0.1; + int neg = 0, digits = 0, e = 0, esign = 1; + + if (*s == '+' || *s == '-') + neg = *s++ == '-'; + while (*s >= '0' && *s <= '9') { + v = v * 10.0 + (*s++ - '0'); + digits = 1; + } + if (*s == '.') { + s++; + while (*s >= '0' && *s <= '9') { + v += (*s++ - '0') * scale; + scale *= 0.1; + digits = 1; + } + } + if (!digits) + return 0; + if (*s == 'e' || *s == 'E') { + const char *q = s + 1; + if (*q == '+' || *q == '-') + esign = *q++ == '-' ? -1 : 1; + if (*q < '0' || *q > '9') + return 0; + while (*q >= '0' && *q <= '9') + e = e * 10 + (*q++ - '0'); + if (e > 99) + return 0; + while (e-- > 0) + v = esign > 0 ? v * 10.0 : v / 10.0; + s = q; + } + *out = neg ? -v : v; + *sp = s; + return 1; +} + +static void path_emit(struct pdf *p, const char *op, const double *v, + int coords, double x, double y) +{ + for (int i = 0; i < coords; i += 2) { + if (i) + c_add(p, " "); + c_addn(p, v[i] + x); + c_add(p, " "); + c_addn(p, flip(v[i + 1] + y)); + } + c_add(p, " "); + c_add(p, op); + c_add(p, "\n"); +} + +/* SVG-style absolute path: "M 10 10 L 20 10 C ... Z", coordinates after the + operator. Extra pairs of M continue as L; repeated C sets continue as C. */ +void pdf_path(struct pdf *p, const char *path, double x, double y, + const char *rgb) +{ + const char *s = path; + double v[6]; + char op = 0; + int n = 0; + + if (!p || !path) + return; + c_rgb(p, rgb, 0); + while (*s) { + if ((unsigned char)*s <= ' ' || *s == ',') { + s++; + continue; + } + if ((*s >= 'A' && *s <= 'Z') || (*s >= 'a' && *s <= 'z')) { + if (*s == 'M' || *s == 'L' || *s == 'C') { + op = *s; + n = 0; + } else if (*s == 'Z') { + c_add(p, "h\n"); + op = 0; + n = 0; + } else { + op = 0; + n = 0; + } + s++; + continue; + } + { + double d; + if (!parse_num(&s, &d)) { + s++; + continue; + } + if (!op) + continue; + if (n < (int)(sizeof v / sizeof v[0])) + v[n++] = d; + if ((op == 'M' || op == 'L') && n == 2) { + path_emit(p, op == 'M' ? "m" : "l", v, 2, x, y); + if (op == 'M') + op = 'L'; + n = 0; + } else if (op == 'C' && n == 6) { + path_emit(p, "c", v, 6, x, y); + n = 0; + } + } + } + c_add(p, "f\n"); +} + +unsigned char *pdf_finish(struct pdf *p, size_t *len) +{ + struct buf out; + size_t off[7] = { 0 }; + size_t xref; + unsigned char *data; + + if (len) + *len = 0; + if (!p) + return NULL; + buf_init(&out); + badd(&out, "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"); + off[1] = out.len; + badd(&out, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"); + off[2] = out.len; + badd(&out, + "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"); + off[3] = out.len; + badd(&out, + "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842]" + " /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >>" + " /Contents 4 0 R >>\nendobj\n"); + off[4] = out.len; + bprintf(&out, "4 0 obj\n<< /Length %zu >>\nstream\n", p->content.len); + buf_append(&out, p->content.p, p->content.len); + badd(&out, "\nendstream\nendobj\n"); + off[5] = out.len; + badd(&out, + "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica" + " /Encoding /WinAnsiEncoding >>\nendobj\n"); + off[6] = out.len; + badd(&out, + "6 0 obj\n<< /Type /Font /Subtype /Type1" + " /BaseFont /Helvetica-Bold" + " /Encoding /WinAnsiEncoding >>\nendobj\n"); + xref = out.len; + bprintf(&out, "xref\n0 7\n0000000000 65535 f \n"); + for (int i = 1; i <= 6; i++) + bprintf(&out, "%010zu 00000 n \n", off[i]); + bprintf(&out, "trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n%zu\n%%%%EOF\n", + xref); + buf_free(&p->content); + data = out.p; + if (len) + *len = out.len; + free(p); + return data; +} diff --git a/src/pdf.h b/src/pdf.h new file mode 100644 index 0000000..c6f95ac --- /dev/null +++ b/src/pdf.h @@ -0,0 +1,27 @@ +#ifndef BOKF_PDF_H +#define BOKF_PDF_H + +#include + +struct pdf; + +/* One-page A4 (595x842 pt) PDF 1.4 writer. Coordinates are in points with a + top-left origin; pdf_text's baseline is measured from the top. Colours are + "#rrggbb". Fonts are the base-14 Helvetica ("H") and Helvetica-Bold ("HB") + with WinAnsiEncoding; UTF-8 input is converted to WinAnsi. */ + +struct pdf *pdf_new(void); +int pdf_page(struct pdf *p); +void pdf_fill_rect(struct pdf *p, double x, double y, double w, double h, + const char *rgb); +void pdf_line(struct pdf *p, double x1, double y1, double x2, double y2, + double width, const char *rgb); +void pdf_text(struct pdf *p, double x, double baseline, const char *font, + double size, const char *rgb, const char *utf8); +double pdf_text_width(const char *font, double size, const char *utf8); +double pdf_font_ascent(const char *font, double size); +void pdf_path(struct pdf *p, const char *path, double x, double y, + const char *rgb); +unsigned char *pdf_finish(struct pdf *p, size_t *len); + +#endif diff --git a/tests/pdf_check.c b/tests/pdf_check.c new file mode 100644 index 0000000..4d56cab --- /dev/null +++ b/tests/pdf_check.c @@ -0,0 +1,160 @@ +#include +#include +#include + +#include "pdf.h" + +static int failures; + +static void check(int cond, const char *what) +{ + if (!cond) { + printf("FAIL %s\n", what); + failures++; + } else { + printf("ok %s\n", what); + } +} + +static void near(const char *what, double got, double want) +{ + double d = got - want; + if (d < 0) + d = -d; + if (d > 0.001) { + printf("FAIL %s: got %.4f want %.4f\n", what, got, want); + failures++; + } else { + printf("ok %s: %.4f\n", what, got); + } +} + +static int contains(const unsigned char *data, size_t len, const char *needle) +{ + size_t n = strlen(needle); + if (n > len) + return 0; + for (size_t i = 0; i + n <= len; i++) + if (memcmp(data + i, needle, n) == 0) + return 1; + return 0; +} + +static void put_text(struct pdf *p, const char *text, double x, double base, + const char *font, double size, const char *rgb) +{ + double w = pdf_text_width(font, size, text); + pdf_text(p, x, base, font, size, rgb, text); + printf("@@%s x=%.3f y=%.3f w=%.3f\n", text, x, base, w); +} + +int main(int argc, char **argv) +{ + struct pdf *p = pdf_new(); + unsigned char *data; + size_t len = 0; + + near("width H 10 Hello", pdf_text_width("H", 10, "Hello"), 22.78); + near("width HB 12 Hello", pdf_text_width("HB", 12, "Hello"), 29.34); + near("width H 12 aao", pdf_text_width("H", 12, "\xc3\xa5\xc3\xa4\xc3\xb6"), + 20.016); + near("width H 12 latin", + pdf_text_width("H", 12, "\xc3\x85\xc3\x84\xc3\x96\xc3\xa5\xc3\xa4" + "\xc3\xb6\xc3\xa9\xc3\xa1\xe2\x80\x93\xe2\x80" + "\x99\xe2\x82\xac"), + 74.712); + near("width H 10 unmappable", pdf_text_width("H", 10, "Unmappable \xe2\x98\x83"), + 65.03); + near("ascent H 10", pdf_font_ascent("H", 10), 7.18); + near("ascent HB 14", pdf_font_ascent("HB", 14), 10.052); + near("width default font", pdf_text_width(NULL, 10, "Hello"), 22.78); + near("width invalid utf8", pdf_text_width("H", 10, "\xff"), 5.56); + near("width 4-byte codepoint", pdf_text_width("H", 10, "\xf0\x9f\x98\x80"), + 5.56); + check(pdf_text_width("H", 0, "Hello") == 0.0, "width size 0"); + check(pdf_text_width("H", 10, NULL) == 0.0, "width NULL text"); + + check(pdf_page(p) == 0, "pdf_page"); + check(pdf_page(p) == -1, "pdf_page twice rejected"); + + pdf_fill_rect(p, 0, 48, 595, 32, "#314c59"); + pdf_fill_rect(p, 300, 300, 60, 40, "#ffffff"); + pdf_line(p, 0, 80, 595, 80, 0.5, "#314c59"); + pdf_line(p, 20, 100, 575, 100, 0.25, "#314c59"); + + put_text(p, "MAKANDRA", 20, 70, "HB", 16, "#ffffff"); + put_text(p, "FAKTURA", 575 - pdf_text_width("H", 16, "FAKTURA"), 70, + "H", 16, "#ffffff"); + put_text(p, "Left", 20, 120, "H", 10, "#314c59"); + put_text(p, "Right", 575 - pdf_text_width("H", 10, "Right"), 120, + "H", 10, "#314c59"); + put_text(p, "V\xc3\xa4nster", 20, 140, "H", 10, "#314c59"); + put_text(p, "H\xc3\xb6ger", 575 - pdf_text_width("HB", 10, "H\xc3\xb6ger"), + 140, "HB", 10, "#314c59"); + put_text(p, "\xc3\x85\xc3\x84\xc3\x96\xc3\xa5\xc3\xa4\xc3\xb6\xc3\xa9" + "\xc3\xa1\xe2\x80\x93\xe2\x80\x99\xe2\x82\xac", + 20, 160, "H", 12, "#314c59"); + put_text(p, "Col", 300, 160, "H", 10, "#314c59"); + put_text(p, "Col2", 300, 180, "H", 10, "#314c59"); + put_text(p, "A(B)C\\D", 20, 180, "H", 10, "#314c59"); + put_text(p, "((((((((((((((((((((((((((((((((", 20, 200, "H", 8, "#314c59"); + near("width 32 parens", pdf_text_width("H", 8, + "(((((((((((((((((((((((((((((((("), 85.248); + + pdf_path(p, "M 10 10 L 90 10 C 100 10 100 60 90 60 L 10 60 Z", + 450, 300, "#314c59"); + pdf_path(p, "M.5.5L20 20Z", 100, 400, "#314c59"); + + data = pdf_finish(p, &len); + check(data != NULL && len > 500, "pdf_finish bytes"); + if (!data) { + printf("FAIL: no data\n"); + return 1; + } + check(memcmp(data, "%PDF-1.4", 8) == 0, "header"); + check(memcmp(data + len - 6, "%%EOF\n", 6) == 0, "trailer eof"); + check(memchr(data, '\0', len) == NULL, "no NUL bytes"); + check(contains(data, len, "/MediaBox [0 0 595 842]"), "MediaBox"); + check(contains(data, len, "/Count 1"), "page count"); + check(contains(data, len, "/BaseFont /Helvetica "), "Helvetica font"); + check(contains(data, len, "/BaseFont /Helvetica-Bold"), "Helvetica-Bold"); + check(contains(data, len, "/Encoding /WinAnsiEncoding"), "WinAnsi"); + check(contains(data, len, "/Length "), "stream length"); + check(contains(data, len, "1 0 obj"), "object 1"); + check(contains(data, len, "6 0 obj"), "object 6"); + check(!contains(data, len, "\r"), "no CR"); + + if (argc > 1) { + FILE *f = fopen(argv[1], "wb"); + if (!f) { + printf("FAIL: cannot write %s\n", argv[1]); + failures++; + } else { + fwrite(data, 1, len, f); + fclose(f); + printf("wrote %s (%zu bytes)\n", argv[1], len); + } + } + free(data); + + { + struct pdf *e = pdf_new(); + unsigned char *d; + size_t n = 1; + + put_text(e, "no explicit pdf_page", 20, 100, "H", 10, "#000000"); + d = pdf_finish(e, &n); + check(d != NULL && n > 0 && memcmp(d, "%PDF-1.4", 8) == 0, + "implicit page document"); + check(d != NULL && memcmp(d + n - 6, "%%EOF\n", 6) == 0, + "implicit page eof"); + free(d); + } + check(pdf_finish(NULL, &len) == NULL && len == 0, "finish NULL"); + + if (failures) + printf("%d failures\n", failures); + else + puts("all ok"); + return failures ? 1 : 0; +} -- cgit v1.3