#include "cst_pdf.h" #include "cst_util.h" #include #include #include /* Text layer of an ordinary PDF (papers, reports, contracts, invoices), in * one file and with no external dependency. Deliberately not a renderer, a * repair tool or OCR: scanned pages, encrypted files and exotic filters come * back as a clear message instead of garbage. * * Pipeline: inflate (RFC 1951) -> stream filters (Flate/ASCIIHex/ASCII85 + * predictors) -> object scan (classic objects and object streams) -> page * tree -> content operators with per-font ToUnicode CMaps. */ enum { PDF_MAX_OBJECTS = 20000, PDF_MAX_PAGES = 2000, PDF_MAX_CMAP = 32768, /* mapping entries per font */ PDF_MAX_RUN = 4096, /* bytes of one string operand */ PDF_MAX_KIDS = 4096, /* page-tree nodes we are willing to visit */ PDF_MAX_ARR = 256 /* items of one TJ array */ }; #define PDF_MAX_STREAM (32u * 1024u * 1024u) static void set_err(char *err, size_t err_sz, const char *msg) { if (err && err_sz) snprintf(err, err_sz, "%s", msg); } static int is_ws(int c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\0'; } static const char *skip_ws(const char *p, const char *end) { while (p < end && is_ws((unsigned char)*p)) p++; return p; } /* --- inflate (RFC 1951), puff-style canonical Huffman --------------------- */ typedef struct { const unsigned char *p; size_t n, i; unsigned bit; int nbits, err; } bits_t; static int bits_get(bits_t *b, int need) { while (b->nbits < need) { if (b->i >= b->n) { b->err = 1; return 0; } b->bit |= (unsigned)b->p[b->i++] << b->nbits; b->nbits += 8; } int v = (int)(b->bit & ((1u << need) - 1)); b->bit >>= need; b->nbits -= need; return v; } typedef struct { short count[16]; short symbol[288]; } huff_t; static void huff_build(huff_t *h, const unsigned char *lengths, int n) { short offs[16]; int len; for (len = 0; len < 16; len++) h->count[len] = 0; for (int i = 0; i < n; i++) h->count[lengths[i]]++; offs[1] = 0; for (len = 1; len < 15; len++) offs[len + 1] = (short)(offs[len] + h->count[len]); for (int i = 0; i < n; i++) if (lengths[i]) h->symbol[offs[lengths[i]]++] = (short)i; } static int huff_decode(bits_t *b, const huff_t *h) { int code = 0, first = 0, index = 0; for (int len = 1; len < 16; len++) { code |= bits_get(b, 1); if (b->err) return -1; int count = h->count[len]; if (code - count < first) return h->symbol[index + (code - first)]; index += count; first += count; first <<= 1; code <<= 1; } return -1; } static const short LENS[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; static const short LEXT[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; static const short DISTS[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; static const short DEXT[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; static const short ORD[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; /* Raw deflate, appended to out. 0 on success. */ static int inflate_raw(const unsigned char *in, size_t n, cst_buf_t *out, size_t max) { bits_t b = {in, n, 0, 0, 0, 0}; huff_t lencode, distcode; for (;;) { int last = bits_get(&b, 1); int type = bits_get(&b, 2); if (b.err) return -1; if (type == 0) { unsigned len, nlen; b.bit = 0; b.nbits = 0; if (b.i + 4 > b.n) return -1; len = (unsigned)in[b.i] | (unsigned)in[b.i + 1] << 8; nlen = (unsigned)in[b.i + 2] | (unsigned)in[b.i + 3] << 8; b.i += 4; if ((len ^ 0xffffu) != nlen || b.i + len > b.n || out->len + len > max) return -1; if (cst_buf_append(out, (const char *)in + b.i, len) != 0) return -1; b.i += len; } else if (type == 1 || type == 2) { if (type == 1) { unsigned char lengths[288]; for (int i = 0; i < 144; i++) lengths[i] = 8; for (int i = 144; i < 256; i++) lengths[i] = 9; for (int i = 256; i < 280; i++) lengths[i] = 7; for (int i = 280; i < 288; i++) lengths[i] = 8; huff_build(&lencode, lengths, 288); for (int i = 0; i < 30; i++) lengths[i] = 5; huff_build(&distcode, lengths, 30); } else { unsigned char lengths[320], clen[19]; huff_t clcode; int hlit = bits_get(&b, 5) + 257; int hdist = bits_get(&b, 5) + 1; int hclen = bits_get(&b, 4) + 4; if (b.err || hlit > 286 || hdist > 30) return -1; memset(lengths, 0, sizeof(lengths)); memset(clen, 0, sizeof(clen)); for (int i = 0; i < hclen; i++) clen[ORD[i]] = (unsigned char)bits_get(&b, 3); huff_build(&clcode, clen, 19); int i = 0; while (i < hlit + hdist) { int sym = huff_decode(&b, &clcode); if (sym < 0) return -1; if (sym < 16) { lengths[i++] = (unsigned char)sym; continue; } int rep, val = 0; if (sym == 16) { if (i == 0) return -1; val = lengths[i - 1]; rep = 3 + bits_get(&b, 2); } else if (sym == 17) { rep = 3 + bits_get(&b, 3); } else { rep = 11 + bits_get(&b, 7); } if (b.err || i + rep > hlit + hdist) return -1; while (rep-- > 0) lengths[i++] = (unsigned char)val; } if (lengths[256] == 0) return -1; huff_build(&lencode, lengths, hlit); huff_build(&distcode, lengths + hlit, hdist); } for (;;) { int sym = huff_decode(&b, &lencode); if (sym < 0) return -1; if (sym < 256) { char ch = (char)sym; if (out->len + 1 > max || cst_buf_append(out, &ch, 1) != 0) return -1; continue; } if (sym == 256) break; sym -= 257; if (sym >= 29) return -1; int len = LENS[sym] + bits_get(&b, LEXT[sym]); int dsym = huff_decode(&b, &distcode); if (b.err || dsym < 0 || dsym >= 30) return -1; size_t dist = (size_t)(DISTS[dsym] + bits_get(&b, DEXT[dsym])); if (b.err || dist == 0 || dist > out->len || out->len + (size_t)len > max) return -1; for (int k = 0; k < len; k++) { char ch = out->buf[out->len - dist]; if (cst_buf_append(out, &ch, 1) != 0) return -1; } } } else { return -1; /* type 3 is reserved */ } if (last) break; } return 0; } /* --- stream filters ------------------------------------------------------ */ static int hex_digit(int c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static int decode_ahx(const char *in, size_t n, cst_buf_t *out, size_t max) { int hi = -1; for (size_t i = 0; i < n; i++) { int c = (unsigned char)in[i]; if (c == '>') break; if (is_ws(c)) continue; int v = hex_digit(c); if (v < 0) return -1; if (hi < 0) { hi = v; continue; } if (out->len + 1 > max) return -1; char ch = (char)(hi * 16 + v); if (cst_buf_append(out, &ch, 1) != 0) return -1; hi = -1; } if (hi >= 0) { char ch = (char)(hi * 16); if (cst_buf_append(out, &ch, 1) != 0) return -1; } return 0; } static int decode_a85(const char *in, size_t n, cst_buf_t *out, size_t max) { unsigned long tuple = 0; int count = 0; for (size_t i = 0; i < n; i++) { int c = (unsigned char)in[i]; if (is_ws(c)) continue; if (c == '~') break; if (c == 'z' && count == 0) { for (int k = 0; k < 4; k++) if (out->len + 1 > max || cst_buf_append(out, "\0", 1) != 0) return -1; continue; } if (c < '!' || c > 'u') return -1; tuple = tuple * 85 + (unsigned long)(c - '!'); if (++count == 5) { if (out->len + 4 > max) return -1; for (int k = 3; k >= 0; k--) { char ch = (char)((tuple >> (8 * k)) & 0xff); if (cst_buf_append(out, &ch, 1) != 0) return -1; } tuple = 0; count = 0; } } if (count > 1) { for (int k = count; k < 5; k++) tuple = tuple * 85 + 84; for (int k = 0; k < count - 1; k++) { char ch = (char)((tuple >> (8 * (3 - k))) & 0xff); if (cst_buf_append(out, &ch, 1) != 0) return -1; } } return 0; } static unsigned char paeth(int a, int b, int c) { int p = a + b - c; int pa = abs(p - a), pb = abs(p - b), pc = abs(p - c); if (pa <= pb && pa <= pc) return (unsigned char)a; return (unsigned char)(pb <= pc ? b : c); } /* PNG (10-15) and TIFF (2) predictors, in place. */ static int unfilter(char *d, size_t len, int pred, int colors, int bpc, int columns) { if (pred < 2) return 0; if (colors < 1) colors = 1; if (bpc < 1) bpc = 8; if (columns < 1) columns = 1; size_t bpp = ((size_t)colors * (size_t)bpc + 7) / 8; if (bpp < 1) bpp = 1; if (pred == 2) { if (bpc != 8) return -1; size_t row = (size_t)columns * (size_t)colors; if (!row) return 0; for (size_t r = 0; r + row <= len; r += row) for (size_t i = bpp; i < row; i++) d[r + i] = (char)((unsigned char)d[r + i] + (unsigned char)d[r + i - bpp]); return 0; } if (pred < 10 || pred > 15) return -1; size_t rowbytes = ((size_t)columns * (size_t)colors * (size_t)bpc + 7) / 8; size_t stride = rowbytes + 1; size_t rows = len / stride; for (size_t r = 0; r < rows; r++) { unsigned char ft = (unsigned char)d[r * stride]; char *cur = d + r * stride + 1; const char *prev = r ? d + (r - 1) * stride + 1 : NULL; if (ft == 0) continue; if (ft == 1) { for (size_t i = bpp; i < rowbytes; i++) cur[i] = (char)((unsigned char)cur[i] + (unsigned char)cur[i - bpp]); } else if (ft == 2 && prev) { for (size_t i = 0; i < rowbytes; i++) cur[i] = (char)((unsigned char)cur[i] + (unsigned char)prev[i]); } else if (ft == 3 && prev) { for (size_t i = 0; i < rowbytes; i++) { int a = i >= bpp ? (unsigned char)cur[i - bpp] : 0; cur[i] = (char)((unsigned char)cur[i] + (unsigned char)((a + (unsigned char)prev[i]) / 2)); } } else if (ft == 4 && prev) { for (size_t i = 0; i < rowbytes; i++) { int a = i >= bpp ? (unsigned char)cur[i - bpp] : 0; int cc = i >= bpp ? (unsigned char)prev[i - bpp] : 0; cur[i] = (char)((unsigned char)cur[i] + paeth(a, (unsigned char)prev[i], cc)); } } else if (ft != 2 && ft != 3) { return -1; } } return 0; } /* --- objects ------------------------------------------------------------- */ typedef struct { const unsigned char *file; size_t flen; struct obj { long num; const char *p; size_t n; } *objs; int nobjs, cap, sorted; char **owned; /* object-stream buffers we must keep alive */ int nowned, cap_owned; } doc_t; static void doc_own(doc_t *d, char *buf) { if (d->nowned == d->cap_owned) { int cap = d->cap_owned ? d->cap_owned * 2 : 4; char **n = realloc(d->owned, (size_t)cap * sizeof(*n)); if (!n) return; d->owned = n; d->cap_owned = cap; } d->owned[d->nowned++] = buf; } static void doc_add(doc_t *d, long num, const char *p, size_t n) { if (d->nobjs >= PDF_MAX_OBJECTS) return; if (d->nobjs == d->cap) { int cap = d->cap ? d->cap * 2 : 512; if (cap > PDF_MAX_OBJECTS) cap = PDF_MAX_OBJECTS; struct obj *v = realloc(d->objs, (size_t)cap * sizeof(*v)); if (!v) return; d->objs = v; d->cap = cap; } d->objs[d->nobjs].num = num; d->objs[d->nobjs].p = p; d->objs[d->nobjs].n = n; d->nobjs++; d->sorted = 0; } static int obj_cmp(const void *a, const void *b) { const struct obj *x = a, *y = b; if (x->num != y->num) return x->num < y->num ? -1 : 1; if (x->n != y->n) return x->n > y->n ? -1 : 1; /* an object stream entry is a fallback */ return 0; } static const struct obj *obj_find(doc_t *d, long num) { if (!d->sorted) { qsort(d->objs, (size_t)d->nobjs, sizeof(*d->objs), obj_cmp); d->sorted = 1; } int lo = 0, hi = d->nobjs - 1; while (lo <= hi) { int mid = (lo + hi) / 2; if (d->objs[mid].num == num) return &d->objs[mid]; if (d->objs[mid].num < num) lo = mid + 1; else hi = mid - 1; } return NULL; } /* "N G obj" ... "endobj" scan. Deliberately layout independent: no xref is * trusted, which also covers files with a broken or missing xref table. */ static void scan_objects(doc_t *d) { const char *s = (const char *)d->file; const char *end = s + d->flen; for (const char *p = s; p + 3 < end; p++) { if (p[0] != 'o' || p[1] != 'b' || p[2] != 'j') continue; if (p > s && !is_ws((unsigned char)p[-1]) && p[-1] != '>') continue; /* walk back over "num ws gen ws" */ const char *q = p; while (q > s && is_ws((unsigned char)q[-1])) q--; const char *gend = q; while (q > s && q[-1] >= '0' && q[-1] <= '9') q--; if (q == gend || (q > s && !is_ws((unsigned char)q[-1]))) continue; while (q > s && is_ws((unsigned char)q[-1])) q--; const char *numend = q; while (q > s && q[-1] >= '0' && q[-1] <= '9') q--; if (q == numend) continue; long num = 0; for (const char *t = q; t < numend; t++) num = num * 10 + (*t - '0'); const char *body = p + 3; const char *e = NULL; for (const char *t = body; t + 6 <= end; t++) if (strncmp(t, "endobj", 6) == 0) { e = t; break; } if (!e) e = end; doc_add(d, num, body, (size_t)(e - body)); p = e; } } /* "N 0 R" -> num; *p advanced past it. */ static int read_ref(const char **p, const char *end, long *num) { const char *q = skip_ws(*p, end); if (q >= end || *q < '0' || *q > '9') return -1; long v = 0; while (q < end && *q >= '0' && *q <= '9') v = v * 10 + (*q++ - '0'); q = skip_ws(q, end); if (q >= end || *q < '0' || *q > '9') return -1; while (q < end && *q >= '0' && *q <= '9') q++; q = skip_ws(q, end); if (q >= end || *q != 'R') return -1; *p = q + 1; *num = v; return 0; } /* --- dictionaries -------------------------------------------------------- */ static size_t name_len(const char *p, const char *end) { if (p >= end || *p != '/') return 0; const char *q = p + 1; while (q < end && !is_ws((unsigned char)*q) && !strchr("/<>[](){}%", *q)) q++; return (size_t)(q - (p + 1)); } /* Position just after the literal string starting at p ('(' ... ')'). */ static const char *skip_string(const char *p, const char *end) { int depth = 1; p++; while (p < end && depth) { if (*p == '\\' && p + 1 < end) { p += 2; continue; } if (*p == '(') depth++; else if (*p == ')') depth--; p++; } return p; } static int is_token_end(int c) { return is_ws(c) || strchr("/<>[](){}%", c) != NULL; } /* Position just after one object (number, name, string, array, dict, ...). * "12 0 R" counts as one value, which is what dictionary lookups want. */ static const char *skip_value(const char *p, const char *end) { p = skip_ws(p, end); if (p >= end) return p; char c = *p; if (c == '<' && p + 1 < end && p[1] == '<') { /* dictionary */ int depth = 0; while (p < end) { if (p[0] == '<' && p + 1 < end && p[1] == '<') { depth++; p += 2; continue; } if (p[0] == '>' && p + 1 < end && p[1] == '>') { depth--; p += 2; if (depth <= 0) return p; continue; } p = *p == '(' ? skip_string(p, end) : p + 1; } return p; } if (c == '[' || c == '{') { /* array */ char close = c == '[' ? ']' : '}'; int depth = 0; while (p < end) { if (*p == c) { depth++; p++; continue; } if (*p == close) { depth--; p++; if (depth <= 0) return p; continue; } if (p[0] == '<' && p + 1 < end && p[1] == '<') { p = skip_value(p, end); continue; } p = *p == '(' ? skip_string(p, end) : p + 1; } return p; } if (c == '(') return skip_string(p, end); if (c == '<') { /* hex string */ while (p < end && *p != '>') p++; return p < end ? p + 1 : end; } if (c == '/') { size_t l = name_len(p, end); return p + 1 + (l ? l : 1); } const char *q = p; while (q < end && !is_token_end((unsigned char)*q)) q++; if (q > p) { /* "N G R" is a single value */ const char *r = skip_ws(q, end); if (r > q && r < end && *r >= '0' && *r <= '9') { const char *s = r; while (s < end && *s >= '0' && *s <= '9') s++; s = skip_ws(s, end); if (s < end && *s == 'R') q = s + 1; } } return q; } /* Value of /key in the dictionary at p (single dict, shallow keys). */ static int dict_get(const char *p, const char *end, const char *key, const char **v, size_t *vlen) { p = skip_ws(p, end); if (end - p < 2 || p[0] != '<' || p[1] != '<') return -1; p = skip_ws(p + 2, end); while (p < end && !(p[0] == '>' && p + 1 < end && p[1] == '>')) { if (*p != '/') { /* stray value */ const char *e = skip_value(p, end); p = skip_ws(e > p ? e : p + 1, end); continue; } size_t nl = name_len(p, end); const char *val = skip_ws(p + 1 + nl, end); const char *e = skip_value(val, end); if (strlen(key) == nl && nl > 0 && strncmp(p + 1, key, nl) == 0) { *v = val; *vlen = (size_t)(e - val); return 0; } p = skip_ws(e, end); } return -1; } /* Follow "N 0 R" chains (bounded); returns the object the value points at, * or NULL when it is not a reference or does not resolve. */ static const struct obj *resolve(doc_t *d, const char *p, size_t n) { for (int hop = 0; hop < 8; hop++) { long num = 0; if (read_ref(&p, p + n, &num) != 0) return NULL; const struct obj *o = obj_find(d, num); if (!o) return NULL; const char *q = o->p; long next = 0; if (read_ref(&q, o->p + o->n, &next) != 0) return o; /* a plain object: done */ p = o->p; /* the object is itself a reference: follow it */ n = o->n; } return NULL; } /* Direct object if the value is one, else the object it points at. */ static const struct obj *value_obj(doc_t *d, const char *p, size_t n) { const char *q = skip_ws(p, p + n); if (q < p + n && *q >= '0' && *q <= '9') return resolve(d, q, (size_t)(p + n - q)); return NULL; } /* --- streams ------------------------------------------------------------- */ /* Decode the stream of object o; 0 on success, -1 on failure, -2 when the * stream is image data (DCT/JPX/CCITT/JBIG2). */ static int stream_decode(doc_t *d, const struct obj *o, cst_buf_t *out) { const char *p = skip_ws(o->p, o->p + o->n); const char *end = o->p + o->n; const char *dict = p; const char *kw = NULL; if (p + 1 < end && p[0] == '<' && p[1] == '<') { const char *e = skip_value(p, end); dict = p; for (const char *t = e; t + 6 <= end; t++) if (strncmp(t, "stream", 6) == 0) { kw = t; break; } } if (!kw) return -1; size_t dlen = (size_t)(end - dict); const char *data = kw + 6; if (data < end && *data == '\r') data++; if (data < end && *data == '\n') data++; size_t rawlen = (size_t)(end - data); const char *lv = NULL; size_t lvlen = 0; if (dict_get(dict, dict + dlen, "Length", &lv, &lvlen) == 0) { long n = 0; const char *q = skip_ws(lv, lv + lvlen); if (q < lv + lvlen && *q >= '0' && *q <= '9') { while (q < lv + lvlen && *q >= '0' && *q <= '9') n = n * 10 + (*q++ - '0'); } else { const struct obj *lo = value_obj(d, lv, lvlen); if (lo) { q = skip_ws(lo->p, lo->p + lo->n); while (q < lo->p + lo->n && *q >= '0' && *q <= '9') n = n * 10 + (*q++ - '0'); } } if (n > 0 && (size_t)n <= rawlen) rawlen = (size_t)n; } /* /Length is optional here: the first endstream ends the data */ for (size_t i = 0; i + 9 <= rawlen; i++) { if (memcmp(data + i, "endstream", 9) == 0) { rawlen = i; break; } } while (rawlen && (data[rawlen - 1] == '\n' || data[rawlen - 1] == '\r')) rawlen--; cst_buf_t cur = {0}; if (cst_buf_append(&cur, data, rawlen) != 0) { cst_buf_free(&cur); return -1; } /* /Filter may be one name or an array; /DecodeParms matches it */ const char *fv = NULL, *dv = NULL, *q = NULL; size_t fvlen = 0, dvlen = 0; int filters = 0; if (dict_get(dict, dict + dlen, "Filter", &fv, &fvlen) == 0) { q = skip_ws(fv, fv + fvlen); filters = (q < fv + fvlen && *q == '[') ? -1 : 1; } dict_get(dict, dict + dlen, "DecodeParms", &dv, &dvlen); if (dict_get(dict, dict + dlen, "DP", &dv, &dvlen) != 0 && !dv) dv = NULL; for (int step = 0;; step++) { const char *name = NULL; size_t nlen = 0; const char *parm = NULL; size_t parmlen = 0; if (filters == 1) { if (step > 0) break; const char *f = skip_ws(fv, fv + fvlen); size_t l = name_len(f, fv + fvlen); if (!l) break; name = f + 1; nlen = l; parm = dv; parmlen = dvlen; } else if (filters == -1) { const char *f = skip_ws(fv + 1, fv + fvlen); const char *pp = skip_ws(dv + 1, dv + dvlen); int idx = 0; while (f < fv + fvlen && idx < step) { f = skip_value(f, fv + fvlen); f = skip_ws(f, fv + fvlen); if (f < fv + fvlen && *f == ',') f = skip_ws(f + 1, fv + fvlen); if (pp && pp < dv + dvlen) { pp = skip_value(pp, dv + dvlen); pp = skip_ws(pp, dv + dvlen); if (pp < dv + dvlen && *pp == ',') pp = skip_ws(pp + 1, dv + dvlen); } idx++; } if (f >= fv + fvlen || *f == ']') break; size_t l = name_len(f, fv + fvlen); if (!l) break; name = f + 1; nlen = l; if (pp && pp < dv + dvlen) parm = pp, parmlen = (size_t)(dv + dvlen - pp); } else { break; } cst_buf_t next = {0}; if (nlen == 11 && strncmp(name, "FlateDecode", 11) == 0) { /* PDF's Flate is zlib-framed (RFC 1950): skip the 2-byte header when * it checks out. A few writers emit bare deflate, so that passes too; * the Adler-32 trailer is simply left over (and ignored). */ const unsigned char *src = (const unsigned char *)cur.buf; size_t srcn = cur.len; if (srcn >= 2 && (src[0] & 0x0f) == 8 && ((unsigned)src[0] * 256u + src[1]) % 31u == 0) { if (src[1] & 0x20) { /* FDICT: a preset dictionary we do not have */ cst_buf_free(&next); cst_buf_free(&cur); return -1; } src += 2; srcn -= 2; } if (inflate_raw(src, srcn, &next, PDF_MAX_STREAM) != 0) { cst_buf_free(&next); cst_buf_free(&cur); return -1; } if (parm && parmlen) { const char *pv = NULL, *tmp = NULL; size_t pvlen = 0, tmplen = 0; int pred = 1, colors = 1, bpc = 8, columns = 1; const char *pd = skip_ws(parm, parm + parmlen); if (dict_get(pd, parm + parmlen, "Predictor", &pv, &pvlen) == 0) { pred = atoi(pv); if (dict_get(pd, parm + parmlen, "Colors", &tmp, &tmplen) == 0) colors = atoi(tmp); if (dict_get(pd, parm + parmlen, "BitsPerComponent", &tmp, &tmplen) == 0) bpc = atoi(tmp); if (dict_get(pd, parm + parmlen, "Columns", &tmp, &tmplen) == 0) columns = atoi(tmp); } if (pred > 1) { if (unfilter(next.buf, next.len, pred, colors, bpc, columns) != 0) pred = 1; if (pred >= 10 && pred <= 15) { size_t rb = ((size_t)(columns < 1 ? 1 : columns) * (size_t)(colors < 1 ? 1 : colors) * (size_t)(bpc < 1 ? 8 : bpc) + 7) / 8; size_t stride = rb + 1; if (stride > 1 && next.len >= stride && next.len % stride) next.len -= next.len % stride; } } } } else if (nlen == 13 && strncmp(name, "ASCIIHexDecode", 13) == 0) { if (decode_ahx(cur.buf, cur.len, &next, PDF_MAX_STREAM) != 0) next.len = 0; } else if (nlen >= 13 && strncmp(name, "ASCII85Decode", 13) == 0) { if (decode_a85(cur.buf, cur.len, &next, PDF_MAX_STREAM) != 0) next.len = 0; } else { cst_buf_free(&next); cst_buf_free(&cur); return -2; } cst_buf_free(&cur); cur = next; if (filters == 1) break; } *out = cur; return 0; } /* Object streams (PDF 1.5+): the contained objects are added to the table, * their bodies pointing into the decoded buffer, which doc_t owns. */ static void expand_objstm(doc_t *d) { int n0 = d->nobjs; for (int i = 0; i < n0; i++) { struct obj o = d->objs[i]; /* by value: doc_add may move the table */ if (o.n < 6) continue; const char *tv = NULL; size_t tvlen = 0; if (dict_get(skip_ws(o.p, o.p + o.n), o.p + o.n, "Type", &tv, &tvlen) != 0) continue; if (tvlen < 6 || strncmp(tv, "/ObjStm", 7) != 0) continue; cst_buf_t dec = {0}; if (stream_decode(d, &o, &dec) != 0) { cst_buf_free(&dec); continue; } size_t len = dec.len; char *buf = cst_buf_steal(&dec); /* steal resets dec.len */ doc_own(d, buf); /* /N and /First live in the dictionary: the stream itself is just the * "num offset" pairs, with the bodies starting at /First. */ const char *body = skip_ws(o.p, o.p + o.n); const char *dv = NULL; size_t dvlen = 0; long count = 0, first = 0; if (dict_get(body, o.p + o.n, "N", &dv, &dvlen) == 0) count = strtol(skip_ws(dv, dv + dvlen), NULL, 10); dv = NULL; dvlen = 0; if (dict_get(body, o.p + o.n, "First", &dv, &dvlen) == 0) first = strtol(skip_ws(dv, dv + dvlen), NULL, 10); if (count <= 0 || first < 0 || (size_t)first > len || count > PDF_MAX_OBJECTS) continue; const char *hdr = skip_ws(buf, buf + len); for (long k = 0; k < count; k++) { long num = strtol(hdr, (char **)&hdr, 10); long off = strtol(hdr, (char **)&hdr, 10); if (num <= 0 || off < 0) continue; size_t start = (size_t)first + (size_t)off; if (start >= len) continue; size_t next = len; /* the next object's offset bounds this one */ const char *probe = hdr; long nnum = strtol(probe, (char **)&probe, 10); long noff = strtol(probe, (char **)&probe, 10); if (nnum > 0 && noff > off) next = (size_t)first + (size_t)noff; if (next > len) next = len; doc_add(d, num, buf + start, next - start); } } } /* --- ToUnicode CMaps ----------------------------------------------------- */ typedef struct { struct glyph { unsigned code; unsigned off; unsigned len; } *glyphs; int n, cap; char *bytes; size_t blen, bcap; int two_byte; } cmap_t; static void cmap_add(cmap_t *m, unsigned code, const char *s, size_t n) { if (m->n >= PDF_MAX_CMAP || !n || n > 8) return; if (m->n == m->cap) { int cap = m->cap ? m->cap * 2 : 256; struct glyph *g = realloc(m->glyphs, (size_t)cap * sizeof(*g)); if (!g) return; m->glyphs = g; m->cap = cap; } if (m->blen + n > m->bcap) { size_t cap = m->bcap ? m->bcap * 2 : 1024; while (cap < m->blen + n) cap *= 2; char *b = realloc(m->bytes, cap); if (!b) return; m->bytes = b; m->bcap = cap; } memcpy(m->bytes + m->blen, s, n); m->glyphs[m->n].code = code; m->glyphs[m->n].off = (unsigned)m->blen; m->glyphs[m->n].len = (unsigned)n; m->blen += n; m->n++; } static int glyph_cmp(const void *a, const void *b) { const struct glyph *x = a, *y = b; return x->code < y->code ? -1 : x->code > y->code ? 1 : 0; } /* One token; returns the byte count or 0. */ static size_t take_hex(const char **p, const char *end, unsigned char *out, size_t cap) { const char *q = skip_ws(*p, end); if (q >= end || *q != '<') return 0; q++; size_t k = 0; int hi = -1; while (q < end && *q != '>') { int c = (unsigned char)*q++; if (is_ws(c)) continue; int v = hex_digit(c); if (v < 0) break; if (hi < 0) { hi = v; continue; } if (k < cap) out[k++] = (unsigned char)(hi * 16 + v); hi = -1; } if (q < end && *q == '>') q++; else if (q < end) return 0; *p = q; return k; } static size_t utf16be_to_utf8(const unsigned char *s, size_t n, unsigned char *out, size_t cap) { size_t k = 0; for (size_t i = 0; i + 1 < n; i += 2) { unsigned cp = ((unsigned)s[i] << 8) | s[i + 1]; if (cp >= 0xD800 && cp <= 0xDBFF && i + 3 < n) { unsigned lo = ((unsigned)s[i + 2] << 8) | s[i + 3]; if (lo >= 0xDC00 && lo <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); i += 2; } } if (cp < 0x80) { if (k + 1 > cap) break; out[k++] = (unsigned char)cp; } else if (cp < 0x800) { if (k + 2 > cap) break; out[k++] = (unsigned char)(0xC0 | (cp >> 6)); out[k++] = (unsigned char)(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { if (k + 3 > cap) break; out[k++] = (unsigned char)(0xE0 | (cp >> 12)); out[k++] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F)); out[k++] = (unsigned char)(0x80 | (cp & 0x3F)); } else if (cp <= 0x10FFFF) { if (k + 4 > cap) break; out[k++] = (unsigned char)(0xF0 | (cp >> 18)); out[k++] = (unsigned char)(0x80 | ((cp >> 12) & 0x3F)); out[k++] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F)); out[k++] = (unsigned char)(0x80 | (cp & 0x3F)); } } return k; } static void parse_cmap(cmap_t *m, const char *s, size_t n) { const char *end = s + n; const char *p = s; while (p < end) { if ((size_t)(end - p) >= 11 && strncmp(p, "beginbfchar", 11) == 0) { p += 11; for (;;) { unsigned char src[8], dst[16], utf8[32]; p = skip_ws(p, end); if (p >= end || strncmp(p, "endbfchar", 9) == 0) break; size_t sn = take_hex(&p, end, src, sizeof(src)); if (!sn) break; size_t dn = take_hex(&p, end, dst, sizeof(dst)); unsigned code = 0; for (size_t i = 0; i < sn; i++) code = (code << 8) | src[i]; if (sn > 1) m->two_byte = 1; size_t un = dn ? utf16be_to_utf8(dst, dn, utf8, sizeof(utf8)) : 0; if (un) cmap_add(m, code, (const char *)utf8, un); } continue; } if ((size_t)(end - p) >= 12 && strncmp(p, "beginbfrange", 12) == 0) { p += 12; for (;;) { unsigned char lo[8], hi[8], dst[16], utf8[32]; p = skip_ws(p, end); if (p >= end || strncmp(p, "endbfrange", 10) == 0) break; size_t ln = take_hex(&p, end, lo, sizeof(lo)); if (!ln) break; size_t hn = take_hex(&p, end, hi, sizeof(hi)); if (!hn) break; unsigned clo = 0, chi = 0; for (size_t i = 0; i < ln; i++) clo = (clo << 8) | lo[i]; for (size_t i = 0; i < hn; i++) chi = (chi << 8) | hi[i]; if (ln > 1 || hn > 1) m->two_byte = 1; if (chi < clo) continue; p = skip_ws(p, end); if (p < end && *p == '[') { /* one destination per code */ p++; unsigned code = clo; for (;;) { p = skip_ws(p, end); if (p >= end || *p == ']') break; size_t dn = take_hex(&p, end, dst, sizeof(dst)); if (!dn) break; size_t un = utf16be_to_utf8(dst, dn, utf8, sizeof(utf8)); if (un) cmap_add(m, code, (const char *)utf8, un); code++; } if (p < end && *p == ']') p++; continue; } size_t dn = take_hex(&p, end, dst, sizeof(dst)); if (!dn) break; size_t un = utf16be_to_utf8(dst, dn, utf8, sizeof(utf8)); if (!un) continue; if (un == 1) { /* sequential code points */ for (unsigned code = clo; code <= chi && code - clo < 65536; code++) { unsigned char one = (unsigned char)(utf8[0] + (code - clo)); if (utf8[0] + (code - clo) > 0x7f) break; cmap_add(m, code, (const char *)&one, 1); } } else { for (unsigned code = clo; code <= chi && code - clo < 65536; code++) cmap_add(m, code, (const char *)utf8, un); } } continue; } p++; } if (m->n) qsort(m->glyphs, (size_t)m->n, sizeof(*m->glyphs), glyph_cmp); } static const char *cmap_lookup(const cmap_t *m, unsigned code, size_t *n) { int lo = 0, hi = m->n - 1; while (lo <= hi) { int mid = (lo + hi) / 2; if (m->glyphs[mid].code == code) { *n = m->glyphs[mid].len; return m->bytes + m->glyphs[mid].off; } if (m->glyphs[mid].code < code) lo = mid + 1; else hi = mid - 1; } return NULL; } /* --- fonts --------------------------------------------------------------- */ typedef struct { char name[64]; cmap_t map; int used; } font_t; typedef struct { doc_t *d; cst_buf_t out; font_t fonts[32]; int nfonts; int cur; /* index into fonts, -1 = default encoding */ int pages; int saw_text; double y; /* current baseline, for line breaks */ int in_text; int pending_space; } text_t; static font_t *font_find(text_t *t, const char *name, size_t n, int create) { for (int i = 0; i < t->nfonts; i++) if (strlen(t->fonts[i].name) == n && strncmp(t->fonts[i].name, name, n) == 0) return &t->fonts[i]; if (!create || t->nfonts >= 32 || n == 0 || n >= sizeof(t->fonts[0].name)) return NULL; font_t *f = &t->fonts[t->nfonts++]; memset(f, 0, sizeof(*f)); memcpy(f->name, name, n); return f; } /* Text of one string operand, decoded through the current font's CMap. */ static void emit_bytes(text_t *t, const unsigned char *s, size_t n) { if (t->out.len >= CST_PDF_TEXT_MAX) return; font_t *f = t->cur >= 0 ? &t->fonts[t->cur] : NULL; for (size_t i = 0; i < n;) { unsigned code; size_t step; if (f && f->map.n > 0 && f->map.two_byte) { if (i + 1 >= n) break; code = ((unsigned)s[i] << 8) | s[i + 1]; step = 2; } else { code = s[i]; step = 1; } size_t gl = 0; const char *g = (f && f->map.n) ? cmap_lookup(&f->map, code, &gl) : NULL; if (g) { if (t->pending_space && t->out.len) { cst_buf_puts(&t->out, " "); t->pending_space = 0; } cst_buf_append(&t->out, g, gl); t->saw_text = 1; } else if (code >= 32 && code != 127 && code < 256) { /* no CMap entry: plain Latin-1 / WinAnsi */ unsigned char utf8[4]; size_t un; if (code < 128) { utf8[0] = (unsigned char)code; un = 1; } else { utf8[0] = (unsigned char)(0xC0 | (code >> 6)); utf8[1] = (unsigned char)(0x80 | (code & 0x3F)); un = 2; } if (t->pending_space && t->out.len) { cst_buf_puts(&t->out, " "); t->pending_space = 0; } cst_buf_append(&t->out, (const char *)utf8, un); t->saw_text = 1; } else if (code == 32) { t->pending_space = 1; } i += step; } } static void emit_string_operand(text_t *t, const char *s, size_t n) { emit_bytes(t, (const unsigned char *)s, n); } static void newline(text_t *t) { if (!t->out.len) return; while (t->out.len && (t->out.buf[t->out.len - 1] == ' ' || t->out.buf[t->out.len - 1] == '\n')) t->out.len--; cst_buf_puts(&t->out, "\n"); t->pending_space = 0; } /* --- content streams ----------------------------------------------------- */ typedef struct { const char *p, *end; /* operand stack */ struct operand { int kind; /* 0 none, 1 number, 2 string, 3 name, 4 array */ double num; char name[64]; unsigned char str[PDF_MAX_RUN]; size_t slen; } ops[8]; int nops; /* the array being read, for TJ */ struct item { int is_str; double num; unsigned char s[256]; size_t n; } arr[PDF_MAX_ARR]; int narr; int in_array; } lexer_t; static void lexer_push(lexer_t *lx, struct operand *op) { if (lx->nops >= 8) { memmove(&lx->ops[0], &lx->ops[1], sizeof(lx->ops[0]) * 7); lx->nops = 7; } lx->ops[lx->nops++] = *op; } static int lex_token(lexer_t *lx, char *op_out, size_t op_sz) { const char *p = skip_ws(lx->p, lx->end); if (p >= lx->end) { lx->p = lx->end; return 0; } lx->p = p; struct operand op; memset(&op, 0, sizeof(op)); int c = (unsigned char)*p; if (c == '(') { /* literal string */ const char *q = p + 1; int depth = 1; size_t k = 0; while (q < lx->end && depth) { char ch = *q++; if (ch == '\\') { if (q >= lx->end) break; char e = *q++; switch (e) { case 'n': ch = '\n'; break; case 'r': ch = '\r'; break; case 't': ch = '\t'; break; case 'b': ch = '\b'; break; case 'f': ch = '\f'; break; case '\n': continue; /* line continuation */ default: if (e >= '0' && e <= '7') { int v = e - '0', digits = 1; while (digits < 3 && q < lx->end && *q >= '0' && *q <= '7') { v = v * 8 + (*q++ - '0'); digits++; } ch = (char)v; } else { ch = e; } } } else if (ch == '(') { depth++; } else if (ch == ')') { depth--; if (!depth) break; } else if (ch == '\r') { /* a raw CR or CRLF becomes an LF */ if (q < lx->end && *q == '\n') q++; ch = '\n'; } if (k < sizeof(op.str)) op.str[k++] = (unsigned char)ch; } op.kind = 2; op.slen = k; lx->p = q; lexer_push(lx, &op); return 1; } if (c == '<' && p + 1 < lx->end && p[1] != '<') { /* hex string */ const char *q = p + 1; size_t k = 0; int hi = -1; while (q < lx->end && *q != '>') { int ch = (unsigned char)*q++; if (is_ws(ch)) continue; int v = hex_digit(ch); if (v < 0) break; if (hi < 0) { hi = v; continue; } if (k < sizeof(op.str)) op.str[k++] = (unsigned char)(hi * 16 + v); hi = -1; } if (q < lx->end && *q == '>') q++; if (hi >= 0 && k < sizeof(op.str)) op.str[k++] = (unsigned char)(hi * 16); op.kind = 2; op.slen = k; lx->p = q; lexer_push(lx, &op); return 1; } if (c == '[') { lx->in_array = 1; lx->narr = 0; lx->p = p + 1; return 1; } if (c == ']') { op.kind = 4; lx->p = p + 1; lexer_push(lx, &op); return 1; } if (c == '/' ) { size_t l = name_len(p, lx->end); op.kind = 3; snprintf(op.name, sizeof(op.name), "%.*s", (int)(l < 63 ? l : 63), p + 1); lx->p = p + 1 + l; lexer_push(lx, &op); return 1; } if (c == '<' && p + 1 < lx->end && p[1] == '<') { /* skip a dict */ const char *e = skip_value(p, lx->end); lx->p = e > p ? e : p + 1; return 1; } if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') { char *tail = NULL; double v = strtod(p, &tail); if (tail && tail > p) { op.kind = 1; op.num = v; lx->p = tail; if (lx->in_array) { if (lx->narr < PDF_MAX_ARR) { lx->arr[lx->narr].is_str = 0; lx->arr[lx->narr].num = v; lx->narr++; } return 1; } lexer_push(lx, &op); return 1; } } /* operator / keyword */ const char *q = p; while (q < lx->end && !is_ws((unsigned char)*q) && !strchr("<>[]()/{}/%", *q)) q++; if (q == p) { lx->p = p + 1; return 1; } size_t l = (size_t)(q - p); snprintf(op_out, op_sz, "%.*s", (int)(l < op_sz - 1 ? l : op_sz - 1), p); lx->p = q; return 2; } static const struct operand *last_op(lexer_t *lx, int n_back) { if (n_back < 0 || n_back >= lx->nops) return NULL; return &lx->ops[lx->nops - 1 - n_back]; } static void run_content(text_t *t, const char *s, size_t n) { lexer_t lx; memset(&lx, 0, sizeof(lx)); lx.p = s; lx.end = s + n; char op[32]; for (;;) { op[0] = 0; int kind = lex_token(&lx, op, sizeof(op)); if (!kind) break; if (!op[0]) { /* an operand: a string inside a TJ array is collected there */ if (lx.in_array && lx.nops > 0) { struct operand *o = &lx.ops[lx.nops - 1]; if (o->kind == 2 && lx.narr < PDF_MAX_ARR) { lx.arr[lx.narr].is_str = 1; lx.arr[lx.narr].n = o->slen < 255 ? o->slen : 255; memcpy(lx.arr[lx.narr].s, o->str, lx.arr[lx.narr].n); lx.narr++; lx.nops--; } } continue; } if (strcmp(op, "BI") == 0) { /* an inline image: the bytes after ID are not text, so jump to the EI * that closes it (it must sit between delimiters) */ const char *q = lx.p; while (q + 2 <= lx.end) { if (q[0] == 'E' && q[1] == 'I' && (q == lx.p || is_ws((unsigned char)q[-1])) && (q + 2 == lx.end || is_ws((unsigned char)q[2]) || strchr("<>[]()/%", q[2]))) break; q++; } lx.p = q + 2 <= lx.end ? q + 2 : lx.end; lx.nops = 0; lx.narr = 0; t->pending_space = 0; } else if (strcmp(op, "BT") == 0) { t->in_text = 1; t->y = 0; } else if (strcmp(op, "ET") == 0) { if (t->in_text) newline(t); t->in_text = 0; lx.nops = 0; } else if (strcmp(op, "Tf") == 0) { const struct operand *nm = last_op(&lx, 1); if (nm && nm->kind == 3) { font_t *f = font_find(t, nm->name, strlen(nm->name), 1); t->cur = f ? (int)(f - t->fonts) : t->cur; } lx.nops = 0; } else if (strcmp(op, "Tj") == 0) { const struct operand *o = last_op(&lx, 0); if (t->in_text && o && o->kind == 2) emit_string_operand(t, (const char *)o->str, o->slen); lx.nops = 0; } else if (strcmp(op, "TJ") == 0) { if (t->in_text) { for (int i = 0; i < lx.narr; i++) { if (lx.arr[i].is_str) { emit_bytes(t, lx.arr[i].s, lx.arr[i].n); } else if (lx.arr[i].num <= -180) { t->pending_space = 1; /* a kerning gap wide enough to be a space */ } } } lx.narr = 0; lx.in_array = 0; lx.nops = 0; } else if (strcmp(op, "'") == 0 || strcmp(op, "\"") == 0) { const struct operand *o = last_op(&lx, 0); if (t->in_text) { newline(t); if (o && o->kind == 2) emit_string_operand(t, (const char *)o->str, o->slen); } lx.nops = 0; } else if (strcmp(op, "Td") == 0 || strcmp(op, "TD") == 0) { const struct operand *ty = last_op(&lx, 0); if (t->in_text) { double dy = ty && ty->kind == 1 ? ty->num : 0; if (dy < -0.5 || dy > 0.5) { newline(t); } else { const struct operand *tx = last_op(&lx, 1); if (tx && tx->kind == 1 && tx->num > 3) t->pending_space = 1; } t->y = t->y + dy; } lx.nops = 0; } else if (strcmp(op, "T*") == 0) { if (t->in_text) newline(t); lx.nops = 0; } else if (strcmp(op, "Tm") == 0) { const struct operand *fy = last_op(&lx, 0); const struct operand *fx = last_op(&lx, 1); if (t->in_text && fy && fy->kind == 1) { if (fy->num + 0.5 < t->y || fy->num - 0.5 > t->y) newline(t); else if (fx && fx->kind == 1 && fx->num > 3) t->pending_space = 1; t->y = fy->num; } lx.nops = 0; } else { lx.nops = 0; } } } /* --- pages --------------------------------------------------------------- */ static void page_fonts(text_t *t, const char *res, size_t rlen) { const char *fv = NULL, *fonts = NULL; size_t fvlen = 0, flen = 0; if (!res || dict_get(res, res + rlen, "Font", &fv, &fvlen) != 0) return; const struct obj *fo = value_obj(t->d, fv, fvlen); if (!fo) { fonts = fv; flen = fvlen; } else { fonts = fo->p; flen = fo->n; } const char *p = skip_ws(fonts, fonts + flen); if (!(p + 1 < fonts + flen && p[0] == '<' && p[1] == '<')) return; p = skip_ws(p + 2, fonts + flen); while (p < fonts + flen && !(p[0] == '>' && p[1] == '>')) { if (*p != '/') { const char *e = skip_value(p, fonts + flen); p = skip_ws(e > p ? e : p + 1, fonts + flen); continue; } size_t nl = name_len(p, fonts + flen); const char *val = skip_ws(p + 1 + nl, fonts + flen); const char *ve = skip_value(val, fonts + flen); font_t *f = font_find(t, p + 1, nl, 1); const struct obj *fo2 = value_obj(t->d, val, (size_t)(ve - val)); if (f && fo2) { /* subtype Type0 means 2-byte codes even without a CMap */ const char *sv = NULL, *tv = NULL; size_t svlen = 0, tvlen = 0; if (dict_get(skip_ws(fo2->p, fo2->p + fo2->n), fo2->p + fo2->n, "Subtype", &sv, &svlen) == 0 && svlen >= 5 && strncmp(sv, "/Type0", 6) == 0) f->map.two_byte = 1; if (dict_get(skip_ws(fo2->p, fo2->p + fo2->n), fo2->p + fo2->n, "ToUnicode", &tv, &tvlen) == 0) { const struct obj *tu = value_obj(t->d, tv, tvlen); cst_buf_t dec = {0}; if (tu && stream_decode(t->d, tu, &dec) == 0) { parse_cmap(&f->map, dec.buf, dec.len); cst_buf_free(&dec); } else { cst_buf_free(&dec); } } } p = skip_ws(ve, fonts + flen); } } static void page_contents(text_t *t, const char *cv, size_t cvlen) { const char *p = skip_ws(cv, cv + cvlen); if (p < cv + cvlen && *p == '[') { /* an array of streams */ p++; for (;;) { p = skip_ws(p, cv + cvlen); if (p >= cv + cvlen || *p == ']') break; const char *e = skip_value(p, cv + cvlen); const struct obj *o = value_obj(t->d, p, (size_t)(e - p)); if (o) { cst_buf_t dec = {0}; if (stream_decode(t->d, o, &dec) == 0) { run_content(t, dec.buf, dec.len); cst_buf_free(&dec); } } p = skip_ws(e, cv + cvlen); } return; } const struct obj *o = value_obj(t->d, cv, cvlen); if (o) { cst_buf_t dec = {0}; if (stream_decode(t->d, o, &dec) == 0) { run_content(t, dec.buf, dec.len); cst_buf_free(&dec); } } } static void walk_pages(text_t *t, long num, const char *inherited_res, size_t inherited_len, int *visited, int nvisited, int depth) { if (depth > 32 || t->pages >= PDF_MAX_PAGES) return; for (int i = 0; i < nvisited; i++) if (visited[i] == num) return; if (nvisited < PDF_MAX_KIDS) visited[nvisited++] = (int)num; const struct obj *o = obj_find(t->d, num); if (!o) return; const char *body = skip_ws(o->p, o->p + o->n); size_t blen = o->n - (size_t)(body - o->p); const char *res = inherited_res; size_t rlen = inherited_len; const char *rv = NULL; size_t rvlen = 0; if (dict_get(body, body + blen, "Resources", &rv, &rvlen) == 0) { const struct obj *ro = value_obj(t->d, rv, rvlen); if (ro) { res = skip_ws(ro->p, ro->p + ro->n); rlen = ro->n - (size_t)(res - ro->p); } else { res = rv; rlen = rvlen; } } const char *kidsv = NULL, *typev = NULL, *cv = NULL; size_t kidslen = 0, typelen = 0, cvlen = 0; int is_pages = dict_get(body, body + blen, "Type", &typev, &typelen) == 0 && typelen >= 6 && strncmp(typev, "/Pages", 6) == 0; int has_kids = dict_get(body, body + blen, "Kids", &kidsv, &kidslen) == 0; if (dict_get(body, body + blen, "Contents", &cv, &cvlen) == 0) { t->pages++; if (res && rlen) page_fonts(t, res, rlen); if (t->out.len) cst_buf_puts(&t->out, "\n\n"); page_contents(t, cv, cvlen); t->in_text = 0; t->cur = -1; for (int i = 0; i < t->nfonts; i++) { /* fonts are per page */ free(t->fonts[i].map.glyphs); free(t->fonts[i].map.bytes); } t->nfonts = 0; return; } if (!has_kids || (!is_pages && !kidsv)) return; const char *p = skip_ws(kidsv, kidsv + kidslen); if (p >= kidsv + kidslen || *p != '[') return; p++; for (;;) { p = skip_ws(p, kidsv + kidslen); if (p >= kidsv + kidslen || *p == ']') break; const char *e = skip_value(p, kidsv + kidslen); long kid = 0; if (read_ref(&p, e, &kid) == 0) walk_pages(t, kid, res, rlen, visited, nvisited, depth + 1); p = skip_ws(e, kidsv + kidslen); } } /* --- entry point --------------------------------------------------------- */ static int is_encrypted(doc_t *d) { for (int i = 0; i < d->nobjs; i++) { const struct obj *o = &d->objs[i]; const char *tv = NULL; size_t tvlen = 0; const char *body = skip_ws(o->p, o->p + o->n); if (dict_get(body, o->p + o->n, "Encrypt", &tv, &tvlen) == 0) return 1; if (dict_get(body, o->p + o->n, "Type", &tv, &tvlen) == 0 && tvlen >= 5 && strncmp(tv, "/XRef", 5) == 0) { const char *ev = NULL; size_t evlen = 0; if (dict_get(body, o->p + o->n, "Encrypt", &ev, &evlen) == 0) return 1; } } /* classic files keep /Encrypt in the (object-less) trailer dictionary */ const char *s = (const char *)d->file, *end = s + d->flen; for (const char *p = s; p + 7 <= end; p++) { if (strncmp(p, "trailer", 7) != 0) continue; const char *o2 = NULL; size_t o2len = 0; const char *ev = NULL; size_t evlen = 0; const char *q = skip_ws(p + 7, end); if (dict_get(q, end, "Root", &o2, &o2len) == 0 && dict_get(q, end, "Encrypt", &ev, &evlen) == 0) return 1; } return 0; } char *cst_pdf_text(const void *data, size_t n, int *pages_out, char *err, size_t err_sz) { if (pages_out) *pages_out = 0; if (!data || n < 8 || memcmp(data, "%PDF-", 5) != 0) { set_err(err, err_sz, "not a PDF (no %PDF- header)"); return NULL; } doc_t d; memset(&d, 0, sizeof(d)); d.file = data; d.flen = n; scan_objects(&d); if (is_encrypted(&d)) { set_err(err, err_sz, "the PDF is encrypted; remove its password/owner restrictions " "first"); free(d.objs); free(d.owned); return NULL; } if (!d.nobjs) { set_err(err, err_sz, "no PDF objects found"); free(d.objs); return NULL; } expand_objstm(&d); text_t t; memset(&t, 0, sizeof(t)); t.d = &d; t.cur = -1; /* root -> pages */ long root = 0; for (int i = 0; i < d.nobjs && !root; i++) { const struct obj *o = &d.objs[i]; const char *tv = NULL; size_t tvlen = 0; const char *body = skip_ws(o->p, o->p + o->n); if (dict_get(body, o->p + o->n, "Type", &tv, &tvlen) == 0 && tvlen >= 8 && strncmp(tv, "/Catalog", 8) == 0) { const char *pv = NULL; size_t pvlen = 0; if (dict_get(body, o->p + o->n, "Pages", &pv, &pvlen) == 0) { long pnum = 0; if (read_ref(&pv, pv + pvlen, &pnum) == 0) root = pnum; } } } int *visited = calloc(PDF_MAX_KIDS, sizeof(*visited)); if (visited && root) walk_pages(&t, root, NULL, 0, visited, 0, 0); if (visited && t.pages == 0) { /* no usable page tree: take every /Type /Page object in file order */ for (int i = 0; i < d.nobjs && t.pages < PDF_MAX_PAGES; i++) { const struct obj *o = &d.objs[i]; const char *tv = NULL, *cv = NULL; size_t tvlen = 0, cvlen = 0; const char *body = skip_ws(o->p, o->p + o->n); if (dict_get(body, o->p + o->n, "Type", &tv, &tvlen) != 0 || tvlen < 5 || strncmp(tv, "/Page", 5) != 0) continue; if (dict_get(body, o->p + o->n, "Contents", &cv, &cvlen) != 0) continue; t.pages++; if (t.out.len) cst_buf_puts(&t.out, "\n\n"); const char *rv = NULL; size_t rvlen = 0; if (dict_get(body, o->p + o->n, "Resources", &rv, &rvlen) == 0) { const struct obj *ro = value_obj(&d, rv, rvlen); if (ro) page_fonts(&t, ro->p, ro->n); else page_fonts(&t, rv, rvlen); } page_contents(&t, cv, cvlen); t.in_text = 0; t.cur = -1; for (int k = 0; k < t.nfonts; k++) { free(t.fonts[k].map.glyphs); free(t.fonts[k].map.bytes); } t.nfonts = 0; } } free(visited); for (int i = 0; i < t.nfonts; i++) { free(t.fonts[i].map.glyphs); free(t.fonts[i].map.bytes); } if (pages_out) *pages_out = t.pages; free(d.objs); for (int i = 0; i < d.nowned; i++) free(d.owned[i]); free(d.owned); if (!t.saw_text || !t.out.len) { cst_buf_free(&t.out); set_err(err, err_sz, t.pages ? "the PDF has no extractable text (the pages are " "probably scanned images; OCR the file first)" : "no readable pages or text in the PDF"); return NULL; } size_t len = 0; char *out = cst_utf8_sanitize(t.out.buf ? t.out.buf : "", t.out.len, &len); cst_buf_free(&t.out); if (!out) { set_err(err, err_sz, "out of memory"); return NULL; } /* collapse runs of blank lines the operators leave behind */ char *w = out; int nl = 0; for (char *r = out; *r;) { if (*r == '\n') { if (++nl <= 2) *w++ = *r; r++; continue; } nl = 0; *w++ = *r++; } *w = 0; return out; }