Files
CST-agent/src/pdf_text.c
T
WHC e0b0eddf3a v0.8.6: general mode, clipboard text, PDF text extraction
Working mode: --mode coding|general (CST_MODE, settings.json, /mode,
WebUI run settings). general swaps the default framing for a general
assistant one -- workflow, safety notes, tool wording (run_shell, the
subagents) -- and /status shows it; minimal (the paper profile) stays
byte-frozen. The prompt state carries the mode so a switch leaves a
system note in history.

Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read,
default 64 KiB window; writing is a WRITE permission with a preview) on
pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32
clipboard. In the TUI, /attach paste falls back to clipboard text when
there is no image on it.

PDF: a dependency-free text extractor (src/pdf_text.c): inflate,
Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object
scan, object streams, page tree walk with inherited resources, content
lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font
ToUnicode CMaps and inline images skipped. Encrypted files and pages
with no text layer are refused with the reason.

Attachments: /attach (a path or files dropped on the terminal) now take
images, PDFs and text. A PDF is stored as its text layer with the mime
still saying application/pdf and its page count; anything else has to be
UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and
text-only models no longer drop them, while images keep their old path.

Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image,
CID + ToUnicode, object stream, error paths), documents in test_attach,
inlined documents in test_llm_body, mode in test_config.
2026-10-11 11:09:39 +08:00

1902 lines
54 KiB
C

#include "cst_pdf.h"
#include "cst_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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 <hex> 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;
}