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.
1902 lines
54 KiB
C
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;
|
|
}
|