aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 14:50:05 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 14:50:05 +0200
commitea3f9b67e5e4e4b3453df55af9758a94c023d6f7 (patch)
tree3b248af093a62e5b2c9bd21a3111dcf8c22fee87
parent412dfcca675114b582e4dc9d5739a889066e6277 (diff)
pdf: minimal PDF writer for generated documents
-rw-r--r--THIRD_PARTY_NOTICES.md6
-rw-r--r--src/pdf.c517
-rw-r--r--src/pdf.h27
-rw-r--r--tests/pdf_check.c160
4 files changed, 710 insertions, 0 deletions
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 <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <stddef.h>
+
+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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+}