Files
MontaukOS/programs/src/pdfviewer/pdf_parser.cpp
T

1645 lines
54 KiB
C++

/*
* pdf_parser.cpp
* PDF file structure parsing and DEFLATE decompression
* Copyright (c) 2026 Daniel Hammer
*/
#include "pdfviewer.h"
// ============================================================================
// DEFLATE Decompression (RFC 1951)
// ============================================================================
struct BitStream {
const uint8_t* data;
int len;
int byte_pos;
uint32_t buf;
int buf_bits;
};
static void bs_init(BitStream* bs, const uint8_t* data, int len) {
bs->data = data;
bs->len = len;
bs->byte_pos = 0;
bs->buf = 0;
bs->buf_bits = 0;
}
static void bs_ensure(BitStream* bs, int n) {
while (bs->buf_bits < n && bs->byte_pos < bs->len) {
bs->buf |= (uint32_t)bs->data[bs->byte_pos++] << bs->buf_bits;
bs->buf_bits += 8;
}
}
static uint32_t bs_read(BitStream* bs, int n) {
if (n == 0) return 0;
bs_ensure(bs, n);
uint32_t val = bs->buf & ((1u << n) - 1);
bs->buf >>= n;
bs->buf_bits -= n;
return val;
}
// Huffman tree: children[node][bit]
// Leaf: -(symbol + 1), Internal: child node index (>= 0)
#define HUFF_UNSET ((int16_t)0x7FFF)
#define HUFF_MAX_NODES 620
struct HuffTree {
int16_t ch[HUFF_MAX_NODES][2];
int cnt;
};
static void huff_init(HuffTree* t) {
t->cnt = 1;
t->ch[0][0] = t->ch[0][1] = HUFF_UNSET;
}
static void huff_build(HuffTree* t, const int* lens, int n) {
huff_init(t);
int max_len = 0;
for (int i = 0; i < n; i++)
if (lens[i] > max_len) max_len = lens[i];
if (max_len == 0) return;
int bl_count[16] = {};
for (int i = 0; i < n; i++)
if (lens[i]) bl_count[lens[i]]++;
int next_code[16] = {};
int code = 0;
for (int b = 1; b <= max_len; b++) {
code = (code + bl_count[b - 1]) << 1;
next_code[b] = code;
}
for (int sym = 0; sym < n; sym++) {
int len = lens[sym];
if (!len) continue;
int c = next_code[len]++;
int node = 0;
for (int bit = len - 1; bit >= 0; bit--) {
int b = (c >> bit) & 1;
if (bit == 0) {
t->ch[node][b] = (int16_t)(-(sym + 1));
} else {
if (t->ch[node][b] == HUFF_UNSET || t->ch[node][b] < 0) {
int nn = t->cnt++;
if (nn >= HUFF_MAX_NODES) return;
t->ch[nn][0] = t->ch[nn][1] = HUFF_UNSET;
t->ch[node][b] = (int16_t)nn;
node = nn;
} else {
node = t->ch[node][b];
}
}
}
}
}
static int huff_decode(HuffTree* t, BitStream* bs) {
int node = 0;
for (;;) {
bs_ensure(bs, 1);
if (bs->buf_bits == 0) return -1;
int bit = bs->buf & 1;
bs->buf >>= 1;
bs->buf_bits--;
int16_t val = t->ch[node][bit];
if (val == HUFF_UNSET) return -1;
if (val < 0) return -(val) - 1;
node = val;
}
}
static const int LEN_BASE[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 int LEN_EXTRA[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 int DIST_BASE[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 int DIST_EXTRA[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 int CL_ORDER[19] = {
16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15
};
// Inflate raw DEFLATE data (no zlib header). Returns bytes written or -1.
static int inflate_raw(BitStream* bs, uint8_t* out, int out_cap) {
HuffTree* lit = (HuffTree*)montauk::malloc(sizeof(HuffTree));
HuffTree* dist = (HuffTree*)montauk::malloc(sizeof(HuffTree));
HuffTree* cl = (HuffTree*)montauk::malloc(sizeof(HuffTree));
if (!lit || !dist || !cl) goto fail;
{
int out_pos = 0;
int bfinal = 0;
while (!bfinal) {
bfinal = (int)bs_read(bs, 1);
int btype = (int)bs_read(bs, 2);
if (btype == 0) {
// Stored block: align to byte boundary
bs->buf = 0;
bs->buf_bits = 0;
if (bs->byte_pos + 4 > bs->len) goto fail;
int len = bs->data[bs->byte_pos] | (bs->data[bs->byte_pos + 1] << 8);
bs->byte_pos += 4; // skip len and nlen
for (int i = 0; i < len && out_pos < out_cap; i++) {
if (bs->byte_pos >= bs->len) goto fail;
out[out_pos++] = bs->data[bs->byte_pos++];
}
} else if (btype == 1 || btype == 2) {
if (btype == 1) {
// Fixed Huffman codes
int ll[288];
for (int i = 0; i <= 143; i++) ll[i] = 8;
for (int i = 144; i <= 255; i++) ll[i] = 9;
for (int i = 256; i <= 279; i++) ll[i] = 7;
for (int i = 280; i <= 287; i++) ll[i] = 8;
huff_build(lit, ll, 288);
int dd[32];
for (int i = 0; i < 32; i++) dd[i] = 5;
huff_build(dist, dd, 32);
} else {
// Dynamic Huffman codes
int hlit = (int)bs_read(bs, 5) + 257;
int hdist = (int)bs_read(bs, 5) + 1;
int hclen = (int)bs_read(bs, 4) + 4;
int cl_lens[19] = {};
for (int i = 0; i < hclen; i++)
cl_lens[CL_ORDER[i]] = (int)bs_read(bs, 3);
huff_build(cl, cl_lens, 19);
int all_lens[320] = {};
int total = hlit + hdist;
int idx = 0;
while (idx < total) {
int sym = huff_decode(cl, bs);
if (sym < 0) goto fail;
if (sym < 16) {
all_lens[idx++] = sym;
} else if (sym == 16) {
int rep = (int)bs_read(bs, 2) + 3;
int val = idx > 0 ? all_lens[idx - 1] : 0;
for (int j = 0; j < rep && idx < total; j++)
all_lens[idx++] = val;
} else if (sym == 17) {
int rep = (int)bs_read(bs, 3) + 3;
for (int j = 0; j < rep && idx < total; j++)
all_lens[idx++] = 0;
} else {
int rep = (int)bs_read(bs, 7) + 11;
for (int j = 0; j < rep && idx < total; j++)
all_lens[idx++] = 0;
}
}
huff_build(lit, all_lens, hlit);
huff_build(dist, all_lens + hlit, hdist);
}
// Decode symbols
for (;;) {
int sym = huff_decode(lit, bs);
if (sym < 0) goto fail;
if (sym < 256) {
if (out_pos >= out_cap) goto fail;
out[out_pos++] = (uint8_t)sym;
} else if (sym == 256) {
break;
} else {
int li = sym - 257;
if (li >= 29) goto fail;
int length = LEN_BASE[li];
if (LEN_EXTRA[li])
length += (int)bs_read(bs, LEN_EXTRA[li]);
int di = huff_decode(dist, bs);
if (di < 0 || di >= 30) goto fail;
int distance = DIST_BASE[di];
if (DIST_EXTRA[di])
distance += (int)bs_read(bs, DIST_EXTRA[di]);
if (distance > out_pos) goto fail;
for (int j = 0; j < length; j++) {
if (out_pos >= out_cap) goto fail;
out[out_pos] = out[out_pos - distance];
out_pos++;
}
}
}
} else {
goto fail;
}
}
montauk::mfree(lit);
montauk::mfree(dist);
montauk::mfree(cl);
return out_pos;
}
fail:
if (lit) montauk::mfree(lit);
if (dist) montauk::mfree(dist);
if (cl) montauk::mfree(cl);
return -1;
}
// Inflate zlib-wrapped data (2-byte header + deflate + 4-byte checksum).
// Returns heap-allocated buffer and length, or nullptr on failure.
static uint8_t* inflate_zlib(const uint8_t* src, int src_len, int* out_len) {
if (src_len < 6) return nullptr;
// Skip 2-byte zlib header
int deflate_start = 2;
// Check for FDICT flag
if (src[1] & 0x20) deflate_start += 4;
BitStream bs;
bs_init(&bs, src + deflate_start, src_len - deflate_start - 4);
// Try progressively larger buffers
for (int mult = 8; mult <= 64; mult *= 2) {
int cap = src_len * mult;
if (cap < 65536) cap = 65536;
uint8_t* dst = (uint8_t*)montauk::malloc(cap);
if (!dst) return nullptr;
bs_init(&bs, src + deflate_start, src_len - deflate_start - 4);
int result = inflate_raw(&bs, dst, cap);
if (result >= 0) {
*out_len = result;
return dst;
}
montauk::mfree(dst);
}
return nullptr;
}
// ============================================================================
// PDF Parsing Utilities
// ============================================================================
int skip_ws(const uint8_t* d, int len, int p) {
while (p < len) {
if (d[p] == '%') {
while (p < len && d[p] != '\n' && d[p] != '\r') p++;
continue;
}
if (d[p] == ' ' || d[p] == '\t' || d[p] == '\n' || d[p] == '\r' || d[p] == '\0')
p++;
else
break;
}
return p;
}
bool starts_with(const uint8_t* d, int len, int p, const char* s) {
for (int i = 0; s[i]; i++) {
if (p + i >= len || d[p + i] != (uint8_t)s[i]) return false;
}
return true;
}
int parse_int_at(const uint8_t* d, int len, int p, int* val) {
p = skip_ws(d, len, p);
bool neg = false;
if (p < len && d[p] == '-') { neg = true; p++; }
else if (p < len && d[p] == '+') p++;
int v = 0;
bool has = false;
while (p < len && d[p] >= '0' && d[p] <= '9') {
v = v * 10 + (d[p] - '0');
has = true;
p++;
}
if (!has) return -1;
*val = neg ? -v : v;
return p;
}
int parse_real_at(const uint8_t* d, int len, int p, float* val) {
p = skip_ws(d, len, p);
bool neg = false;
if (p < len && d[p] == '-') { neg = true; p++; }
else if (p < len && d[p] == '+') p++;
float v = 0;
bool has = false;
while (p < len && d[p] >= '0' && d[p] <= '9') {
v = v * 10 + (d[p] - '0');
has = true;
p++;
}
if (p < len && d[p] == '.') {
p++;
float frac = 0.1f;
while (p < len && d[p] >= '0' && d[p] <= '9') {
v += (d[p] - '0') * frac;
frac *= 0.1f;
has = true;
p++;
}
}
if (!has) return -1;
*val = neg ? -v : v;
return p;
}
// Parse an indirect reference "N G R" at position p.
// Returns position after 'R', or -1 if not a reference.
int parse_ref_at(const uint8_t* d, int len, int p, int* obj_num) {
int saved = p;
int num;
p = parse_int_at(d, len, p, &num);
if (p < 0) return -1;
int gen;
p = parse_int_at(d, len, p, &gen);
if (p < 0) return -1;
p = skip_ws(d, len, p);
if (p >= len || d[p] != 'R') return -1;
p++;
*obj_num = num;
return p;
(void)saved;
}
// Skip over a PDF value (number, string, name, array, dict, ref, bool, null).
// Returns position after the value.
static int skip_value(const uint8_t* d, int len, int p) {
p = skip_ws(d, len, p);
if (p >= len) return p;
// Dictionary
if (p + 1 < len && d[p] == '<' && d[p + 1] == '<') {
p += 2;
int depth = 1;
while (p + 1 < len && depth > 0) {
if (d[p] == '<' && d[p + 1] == '<') { depth++; p += 2; }
else if (d[p] == '>' && d[p + 1] == '>') { depth--; p += 2; }
else p++;
}
return p;
}
// Hex string
if (d[p] == '<') {
p++;
while (p < len && d[p] != '>') p++;
if (p < len) p++;
return p;
}
// Literal string
if (d[p] == '(') {
p++;
int depth = 1;
while (p < len && depth > 0) {
if (d[p] == '\\') p += 2;
else if (d[p] == '(') { depth++; p++; }
else if (d[p] == ')') { depth--; p++; }
else p++;
}
return p;
}
// Array
if (d[p] == '[') {
p++;
while (p < len && d[p] != ']') {
p = skip_value(d, len, p);
p = skip_ws(d, len, p);
}
if (p < len) p++;
return p;
}
// Name
if (d[p] == '/') {
p++;
while (p < len && d[p] != ' ' && d[p] != '\t' && d[p] != '\n' &&
d[p] != '\r' && d[p] != '/' && d[p] != '<' && d[p] != '>' &&
d[p] != '[' && d[p] != ']' && d[p] != '(' && d[p] != ')')
p++;
return p;
}
// Try reference (N G R)
{
int obj_num;
int rp = parse_ref_at(d, len, p, &obj_num);
if (rp > 0) return rp;
}
// Number (int or real)
{
float v;
int rp = parse_real_at(d, len, p, &v);
if (rp > 0) return rp;
}
// Boolean or null
if (starts_with(d, len, p, "true")) return p + 4;
if (starts_with(d, len, p, "false")) return p + 5;
if (starts_with(d, len, p, "null")) return p + 4;
// Unknown - skip one byte
return p + 1;
}
// Find a key in a PDF dictionary. pos should point at or after "<<".
// Returns position of the value (after the key name), or -1 if not found.
int dict_lookup(const uint8_t* d, int len, int pos, const char* key) {
int p = pos;
// Find the start of the dictionary
while (p + 1 < len) {
if (d[p] == '<' && d[p + 1] == '<') { p += 2; break; }
p++;
}
int depth = 1;
int key_len = 0;
while (key[key_len]) key_len++;
while (p < len && depth > 0) {
p = skip_ws(d, len, p);
if (p >= len) return -1;
// End of dict
if (p + 1 < len && d[p] == '>' && d[p + 1] == '>') {
depth--;
p += 2;
if (depth == 0) return -1;
continue;
}
// Nested dict start (skip it)
if (p + 1 < len && d[p] == '<' && d[p + 1] == '<') {
if (depth > 1) {
p = skip_value(d, len, p);
continue;
}
}
// Key must be a name
if (d[p] != '/') {
p = skip_value(d, len, p);
continue;
}
// Check if this key matches
p++; // skip '/'
bool match = true;
int kp = 0;
int name_start = p;
while (p < len && d[p] != ' ' && d[p] != '\t' && d[p] != '\n' &&
d[p] != '\r' && d[p] != '/' && d[p] != '<' && d[p] != '>' &&
d[p] != '[' && d[p] != ']' && d[p] != '(' && d[p] != ')') {
if (kp < key_len && d[p] == (uint8_t)key[kp]) kp++;
else match = false;
p++;
}
if (match && kp == key_len && (p - name_start) == key_len) {
// Found the key, return position of the value
return skip_ws(d, len, p);
}
// Skip the value
p = skip_ws(d, len, p);
p = skip_value(d, len, p);
}
return -1;
}
// ============================================================================
// Object Access
// ============================================================================
// Find object content by number. Sets start to first byte after "N G obj\n"
// and end to position of "endobj". Returns 0 on success, -1 on failure.
int find_obj_content(int obj_num, int* start, int* end) {
if (obj_num < 0 || obj_num >= g_doc.xref_count) return -1;
int off = g_doc.xref[obj_num];
if (off <= 0) return -1;
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int p = off;
// Skip "N G obj"
while (p < len && d[p] != 'o') p++;
if (!starts_with(d, len, p, "obj")) return -1;
p += 3;
p = skip_ws(d, len, p);
*start = p;
// Find endobj
// Search from the object start for "endobj"
int ep = p;
while (ep + 5 < len) {
if (starts_with(d, len, ep, "endobj")) {
*end = ep;
return 0;
}
ep++;
}
*end = len;
return 0;
}
// ============================================================================
// Stream Data Extraction
// ============================================================================
// Get decompressed stream data for a stream object.
// Returns heap-allocated buffer (caller must free) and sets out_len.
uint8_t* get_stream_data(int obj_num, int* out_len) {
int obj_start, obj_end;
if (find_obj_content(obj_num, &obj_start, &obj_end) < 0)
return nullptr;
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
// Check for /Filter
int filter_pos = dict_lookup(d, len, obj_start, "Filter");
bool is_flate = false;
if (filter_pos >= 0) {
int fp = skip_ws(d, len, filter_pos);
// Could be /FlateDecode or [/FlateDecode]
if (fp < len && d[fp] == '/') {
if (starts_with(d, len, fp + 1, "FlateDecode")) is_flate = true;
} else if (fp < len && d[fp] == '[') {
// Array of filters - check first one
fp++;
fp = skip_ws(d, len, fp);
if (fp < len && d[fp] == '/' && starts_with(d, len, fp + 1, "FlateDecode"))
is_flate = true;
}
}
// Get /Length
int stream_len = 0;
int len_pos = dict_lookup(d, len, obj_start, "Length");
if (len_pos >= 0) {
int lp = skip_ws(d, len, len_pos);
// Length could be a direct int or an indirect reference
int ref_num;
int rp = parse_ref_at(d, len, lp, &ref_num);
if (rp > 0) {
// Resolve indirect length
int rs, re;
if (find_obj_content(ref_num, &rs, &re) == 0) {
parse_int_at(d, len, rs, &stream_len);
}
} else {
parse_int_at(d, len, lp, &stream_len);
}
}
// Find "stream" keyword
int sp = obj_start;
while (sp + 6 < obj_end) {
if (starts_with(d, len, sp, "stream")) {
sp += 6;
// Skip \r\n or \n
if (sp < len && d[sp] == '\r') sp++;
if (sp < len && d[sp] == '\n') sp++;
break;
}
sp++;
}
if (sp >= len || stream_len <= 0) return nullptr;
// Clamp stream_len to available file data
// (Don't clamp to obj_end — binary streams may contain false "endobj" matches)
if (sp + stream_len > len) stream_len = len - sp;
if (is_flate) {
return inflate_zlib(d + sp, stream_len, out_len);
} else {
// Uncompressed - copy the data
uint8_t* buf = (uint8_t*)montauk::malloc(stream_len);
if (!buf) return nullptr;
montauk::memcpy(buf, d + sp, stream_len);
*out_len = stream_len;
return buf;
}
}
// ============================================================================
// Xref Parsing
// ============================================================================
static bool parse_xref_table(int pos) {
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int p = pos;
// Skip "xref" and whitespace
if (!starts_with(d, len, p, "xref")) return false;
p += 4;
p = skip_ws(d, len, p);
// Parse subsections
while (p < len && d[p] >= '0' && d[p] <= '9') {
int start_num, count;
p = parse_int_at(d, len, p, &start_num);
if (p < 0) return false;
p = parse_int_at(d, len, p, &count);
if (p < 0) return false;
p = skip_ws(d, len, p);
// Ensure xref array is big enough
int need = start_num + count;
if (need > g_doc.xref_count) {
int* new_xref = (int*)montauk::malloc(need * sizeof(int));
if (!new_xref) return false;
montauk::memset(new_xref, 0, need * sizeof(int));
if (g_doc.xref) {
montauk::memcpy(new_xref, g_doc.xref, g_doc.xref_count * sizeof(int));
montauk::mfree(g_doc.xref);
}
g_doc.xref = new_xref;
g_doc.xref_count = need;
}
for (int i = 0; i < count; i++) {
// Each entry: 10 digits offset, space, 5 digits gen, space, f/n, end
if (p + 18 > len) return false;
int offset = 0;
for (int j = 0; j < 10 && p + j < len; j++) {
if (d[p + j] >= '0' && d[p + j] <= '9')
offset = offset * 10 + (d[p + j] - '0');
}
char type = (char)d[p + 17];
if (type == 'n' && offset > 0) {
g_doc.xref[start_num + i] = offset;
}
// Advance past this entry (20 bytes typical, but handle variable endings)
p += 18;
while (p < len && (d[p] == ' ' || d[p] == '\r' || d[p] == '\n')) p++;
}
}
return true;
}
static bool parse_xref_stream(int pos) {
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
// At pos we have "N 0 obj << ... >> stream ..."
// Parse as a stream object, then decode binary xref data
// First, let's find the object number
int obj_num;
int p = parse_int_at(d, len, pos, &obj_num);
if (p < 0) return false;
int gen;
p = parse_int_at(d, len, p, &gen);
if (p < 0) return false;
p = skip_ws(d, len, p);
if (!starts_with(d, len, p, "obj")) return false;
p += 3;
p = skip_ws(d, len, p);
int dict_start = p;
// Get /Size
int size = 0;
int size_pos = dict_lookup(d, len, dict_start, "Size");
if (size_pos >= 0) parse_int_at(d, len, size_pos, &size);
if (size <= 0) return false;
// Get /W array [w1 w2 w3]
int w[3] = {1, 2, 1};
int w_pos = dict_lookup(d, len, dict_start, "W");
if (w_pos >= 0) {
int wp = skip_ws(d, len, w_pos);
if (wp < len && d[wp] == '[') {
wp++;
for (int i = 0; i < 3; i++)
wp = parse_int_at(d, len, wp, &w[i]);
}
}
// Get /Index array (optional, defaults to [0 Size])
int index_pairs[32]; // start, count pairs
int index_count = 0;
int idx_pos = dict_lookup(d, len, dict_start, "Index");
if (idx_pos >= 0) {
int ip = skip_ws(d, len, idx_pos);
if (ip < len && d[ip] == '[') {
ip++;
while (index_count < 30) {
ip = skip_ws(d, len, ip);
if (ip >= len || d[ip] == ']') break;
int v;
ip = parse_int_at(d, len, ip, &v);
if (ip < 0) break;
index_pairs[index_count++] = v;
}
}
}
if (index_count == 0) {
index_pairs[0] = 0;
index_pairs[1] = size;
index_count = 2;
}
// Allocate xref array and type-2 tracking arrays
g_doc.xref = (int*)montauk::malloc(size * sizeof(int));
if (!g_doc.xref) return false;
montauk::memset(g_doc.xref, 0, size * sizeof(int));
g_doc.xref_count = size;
g_doc.xref_stm = (int*)montauk::malloc(size * sizeof(int));
g_doc.xref_idx = (int*)montauk::malloc(size * sizeof(int));
if (g_doc.xref_stm) montauk::memset(g_doc.xref_stm, 0, size * sizeof(int));
if (g_doc.xref_idx) montauk::memset(g_doc.xref_idx, 0, size * sizeof(int));
// Temporarily add this xref stream object to the xref table
// so get_stream_data can find it
if (obj_num < size) g_doc.xref[obj_num] = pos;
// Get /Filter and /Length, then extract stream data
int filter_pos = dict_lookup(d, len, dict_start, "Filter");
bool is_flate = false;
if (filter_pos >= 0) {
int fp = skip_ws(d, len, filter_pos);
if (fp < len && d[fp] == '/' && starts_with(d, len, fp + 1, "FlateDecode"))
is_flate = true;
else if (fp < len && d[fp] == '[') {
fp++;
fp = skip_ws(d, len, fp);
if (fp < len && d[fp] == '/' && starts_with(d, len, fp + 1, "FlateDecode"))
is_flate = true;
}
}
int stream_len = 0;
int lp_pos = dict_lookup(d, len, dict_start, "Length");
if (lp_pos >= 0) parse_int_at(d, len, lp_pos, &stream_len);
// Find "stream" keyword
int sp = dict_start;
while (sp + 6 < len) {
if (starts_with(d, len, sp, "stream")) {
sp += 6;
if (sp < len && d[sp] == '\r') sp++;
if (sp < len && d[sp] == '\n') sp++;
break;
}
sp++;
}
if (stream_len <= 0) return false;
uint8_t* stream_data;
int stream_data_len;
if (is_flate) {
stream_data = inflate_zlib(d + sp, stream_len, &stream_data_len);
} else {
stream_data = (uint8_t*)montauk::malloc(stream_len);
if (stream_data) {
montauk::memcpy(stream_data, d + sp, stream_len);
stream_data_len = stream_len;
}
}
if (!stream_data) return false;
// Parse binary xref entries
int entry_size = w[0] + w[1] + w[2];
int data_off = 0;
for (int pair = 0; pair + 1 < index_count; pair += 2) {
int start_obj = index_pairs[pair];
int count = index_pairs[pair + 1];
for (int i = 0; i < count && data_off + entry_size <= stream_data_len; i++) {
// Read type field
int type = 0;
for (int b = 0; b < w[0]; b++)
type = (type << 8) | stream_data[data_off + b];
// Read field 2
int field2 = 0;
for (int b = 0; b < w[1]; b++)
field2 = (field2 << 8) | stream_data[data_off + w[0] + b];
// Read field 3
int field3 = 0;
for (int b = 0; b < w[2]; b++)
field3 = (field3 << 8) | stream_data[data_off + w[0] + w[1] + b];
int obj_idx = start_obj + i;
if (obj_idx < size) {
if (type == 1 && field2 > 0) {
g_doc.xref[obj_idx] = field2;
} else if (type == 2 && g_doc.xref_stm && g_doc.xref_idx) {
g_doc.xref_stm[obj_idx] = field2; // containing ObjStm number
g_doc.xref_idx[obj_idx] = field3; // index within stream
}
}
data_off += entry_size;
}
}
montauk::mfree(stream_data);
return true;
}
// ============================================================================
// Object Stream Decompression (xref type 2)
// ============================================================================
static void decompress_object_streams() {
if (!g_doc.xref_stm) return;
// Collect unique object stream numbers
int stm_objs[256];
int stm_count = 0;
for (int i = 0; i < g_doc.xref_count; i++) {
if (g_doc.xref_stm[i] == 0) continue;
int sobj = g_doc.xref_stm[i];
bool found = false;
for (int j = 0; j < stm_count; j++) {
if (stm_objs[j] == sobj) { found = true; break; }
}
if (!found && stm_count < 256)
stm_objs[stm_count++] = sobj;
}
if (stm_count == 0) return;
// Build synthetic "N 0 obj\n<data>\nendobj\n" wrappers for each
// compressed object and append to g_doc.data
int synth_cap = 64 * 1024;
uint8_t* synth = (uint8_t*)montauk::malloc(synth_cap);
if (!synth) return;
int synth_len = 0;
for (int si = 0; si < stm_count; si++) {
int stm_obj = stm_objs[si];
// Read /N and /First from the ObjStm dict
int os, oe;
if (find_obj_content(stm_obj, &os, &oe) < 0) continue;
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int n_objects = 0, first = 0;
int np = dict_lookup(d, len, os, "N");
if (np >= 0) parse_int_at(d, len, np, &n_objects);
int fp = dict_lookup(d, len, os, "First");
if (fp >= 0) parse_int_at(d, len, fp, &first);
if (n_objects <= 0 || first <= 0) continue;
// Decompress stream data
int data_len;
uint8_t* data = get_stream_data(stm_obj, &data_len);
if (!data) continue;
if (first >= data_len) { montauk::mfree(data); continue; }
// Parse header: N pairs of (obj_num, byte_offset relative to /First)
int* hdr_nums = (int*)montauk::malloc(n_objects * sizeof(int));
int* hdr_offs = (int*)montauk::malloc(n_objects * sizeof(int));
if (!hdr_nums || !hdr_offs) {
if (hdr_nums) montauk::mfree(hdr_nums);
if (hdr_offs) montauk::mfree(hdr_offs);
montauk::mfree(data);
continue;
}
int hp = 0;
int parsed = 0;
for (int i = 0; i < n_objects; i++) {
hp = parse_int_at(data, first, hp, &hdr_nums[i]);
if (hp < 0) break;
hp = parse_int_at(data, first, hp, &hdr_offs[i]);
if (hp < 0) break;
parsed++;
}
// Extract each object and build synthetic wrapper
for (int i = 0; i < parsed; i++) {
int obj_num = hdr_nums[i];
if (obj_num < 0 || obj_num >= g_doc.xref_count) continue;
if (g_doc.xref[obj_num] > 0) continue; // already has a direct offset
int obj_off = first + hdr_offs[i];
int obj_end_off;
if (i + 1 < parsed)
obj_end_off = first + hdr_offs[i + 1];
else
obj_end_off = data_len;
if (obj_off >= data_len) continue;
if (obj_end_off > data_len) obj_end_off = data_len;
int obj_data_len = obj_end_off - obj_off;
if (obj_data_len <= 0) continue;
// "N 0 obj\n" + data + "\nendobj\n"
char prefix[32];
int prefix_len = snprintf(prefix, 32, "%d 0 obj\n", obj_num);
int needed = prefix_len + obj_data_len + 8;
// Grow buffer if needed
while (synth_len + needed > synth_cap) {
int new_cap = synth_cap * 2;
uint8_t* ns = (uint8_t*)montauk::malloc(new_cap);
if (!ns) goto done_stream;
montauk::memcpy(ns, synth, synth_len);
montauk::mfree(synth);
synth = ns;
synth_cap = new_cap;
}
// Record xref offset into the future expanded buffer
g_doc.xref[obj_num] = g_doc.data_len + synth_len;
// Write synthetic object
montauk::memcpy(synth + synth_len, prefix, prefix_len);
synth_len += prefix_len;
montauk::memcpy(synth + synth_len, data + obj_off, obj_data_len);
synth_len += obj_data_len;
montauk::memcpy(synth + synth_len, "\nendobj\n", 8);
synth_len += 8;
}
done_stream:
montauk::mfree(hdr_nums);
montauk::mfree(hdr_offs);
montauk::mfree(data);
}
// Append synthetic data to g_doc.data
if (synth_len > 0) {
int new_total = g_doc.data_len + synth_len;
uint8_t* new_data = (uint8_t*)montauk::malloc(new_total);
if (new_data) {
montauk::memcpy(new_data, g_doc.data, g_doc.data_len);
montauk::memcpy(new_data + g_doc.data_len, synth, synth_len);
montauk::mfree(g_doc.data);
g_doc.data = new_data;
g_doc.data_len = new_total;
} else {
// Xref entries already point past old buffer; undo them
for (int i = 0; i < g_doc.xref_count; i++)
if (g_doc.xref[i] >= g_doc.data_len) g_doc.xref[i] = 0;
}
}
montauk::mfree(synth);
}
// ============================================================================
// Page Tree Traversal
// ============================================================================
static void add_page(int obj_num) {
if (g_doc.page_count >= g_doc.page_cap) {
int new_cap = g_doc.page_cap ? g_doc.page_cap * 2 : 64;
int* new_objs = (int*)montauk::malloc(new_cap * sizeof(int));
PdfPage* new_pages = (PdfPage*)montauk::malloc(new_cap * sizeof(PdfPage));
if (!new_objs || !new_pages) return;
montauk::memset(new_pages, 0, new_cap * sizeof(PdfPage));
if (g_doc.page_objs) {
montauk::memcpy(new_objs, g_doc.page_objs, g_doc.page_count * sizeof(int));
montauk::mfree(g_doc.page_objs);
}
if (g_doc.pages) {
montauk::memcpy(new_pages, g_doc.pages, g_doc.page_count * sizeof(PdfPage));
montauk::mfree(g_doc.pages);
}
g_doc.page_objs = new_objs;
g_doc.pages = new_pages;
g_doc.page_cap = new_cap;
}
g_doc.page_objs[g_doc.page_count] = obj_num;
g_doc.pages[g_doc.page_count].items = nullptr;
g_doc.pages[g_doc.page_count].item_count = 0;
g_doc.pages[g_doc.page_count].item_cap = 0;
g_doc.pages[g_doc.page_count].gfx_items = nullptr;
g_doc.pages[g_doc.page_count].gfx_count = 0;
g_doc.pages[g_doc.page_count].gfx_cap = 0;
g_doc.pages[g_doc.page_count].width = 612;
g_doc.pages[g_doc.page_count].height = 792;
g_doc.page_count++;
}
static void collect_pages(int obj_num, int depth) {
if (depth > 20) return; // prevent infinite recursion
int start, end;
if (find_obj_content(obj_num, &start, &end) < 0) return;
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
// Check /Type
int type_pos = dict_lookup(d, len, start, "Type");
if (type_pos < 0) return;
if (starts_with(d, len, type_pos + 1, "Page") &&
!starts_with(d, len, type_pos + 1, "Pages")) {
// This is a leaf page
add_page(obj_num);
// Get MediaBox
int mb_pos = dict_lookup(d, len, start, "MediaBox");
if (mb_pos >= 0) {
int mp = skip_ws(d, len, mb_pos);
// Could be a reference
int ref_num;
int rp = parse_ref_at(d, len, mp, &ref_num);
if (rp > 0) {
int rs, re;
if (find_obj_content(ref_num, &rs, &re) == 0)
mp = skip_ws(d, len, rs);
}
if (mp < len && d[mp] == '[') {
mp++;
float vals[4];
for (int i = 0; i < 4; i++)
mp = parse_real_at(d, len, mp, &vals[i]);
PdfPage* page = &g_doc.pages[g_doc.page_count - 1];
page->width = vals[2] - vals[0];
page->height = vals[3] - vals[1];
}
}
} else if (starts_with(d, len, type_pos + 1, "Pages")) {
// This is a pages node - recurse into /Kids
int kids_pos = dict_lookup(d, len, start, "Kids");
if (kids_pos < 0) return;
int kp = skip_ws(d, len, kids_pos);
if (kp >= len || d[kp] != '[') return;
kp++;
while (kp < len && d[kp] != ']') {
kp = skip_ws(d, len, kp);
if (kp >= len || d[kp] == ']') break;
int child_num;
int rp = parse_ref_at(d, len, kp, &child_num);
if (rp > 0) {
collect_pages(child_num, depth + 1);
kp = rp;
} else {
kp++;
}
}
}
}
// ============================================================================
// Font Map Building
// ============================================================================
static bool str_contains(const char* haystack, const char* needle) {
int nlen = 0;
while (needle[nlen]) nlen++;
for (int i = 0; haystack[i]; i++) {
bool match = true;
for (int j = 0; j < nlen; j++) {
char h = haystack[i + j];
char n = needle[j];
// Case-insensitive
if (h >= 'a' && h <= 'z') h -= 32;
if (n >= 'a' && n <= 'z') n -= 32;
if (h != n) { match = false; break; }
}
if (match) return true;
}
return false;
}
// Parse a ToUnicode CMap stream and build a glyph->Unicode mapping table.
// Returns heap-allocated array of 256 uint16_t entries, or nullptr on failure.
static uint16_t* parse_tounicode(int cmap_obj_num) {
int stream_len;
uint8_t* cmap_data = get_stream_data(cmap_obj_num, &stream_len);
if (!cmap_data) return nullptr;
uint16_t* table = (uint16_t*)montauk::malloc(256 * sizeof(uint16_t));
if (!table) { montauk::mfree(cmap_data); return nullptr; }
// Initialize: identity mapping for printable ASCII, 0 for rest
for (int i = 0; i < 256; i++) table[i] = 0;
// Scan for "beginbfchar" sections
for (int p = 0; p + 11 < stream_len; p++) {
if (starts_with(cmap_data, stream_len, p, "beginbfchar")) {
p += 11;
// Parse entries until "endbfchar"
while (p < stream_len) {
// Skip whitespace
while (p < stream_len && (cmap_data[p] == ' ' || cmap_data[p] == '\n' ||
cmap_data[p] == '\r' || cmap_data[p] == '\t'))
p++;
if (starts_with(cmap_data, stream_len, p, "endbfchar")) break;
// Parse <XX> <XXXX>
if (p >= stream_len || cmap_data[p] != '<') break;
p++; // skip <
// Read source code (1-2 hex bytes)
int src = 0;
while (p < stream_len && cmap_data[p] != '>') {
int v = -1;
if (cmap_data[p] >= '0' && cmap_data[p] <= '9') v = cmap_data[p] - '0';
else if (cmap_data[p] >= 'a' && cmap_data[p] <= 'f') v = cmap_data[p] - 'a' + 10;
else if (cmap_data[p] >= 'A' && cmap_data[p] <= 'F') v = cmap_data[p] - 'A' + 10;
if (v >= 0) src = (src << 4) | v;
p++;
}
if (p < stream_len) p++; // skip >
// Skip whitespace
while (p < stream_len && (cmap_data[p] == ' ' || cmap_data[p] == '\n' ||
cmap_data[p] == '\r' || cmap_data[p] == '\t'))
p++;
// Read dest Unicode (2-4 hex bytes)
if (p >= stream_len || cmap_data[p] != '<') break;
p++; // skip <
int dst = 0;
while (p < stream_len && cmap_data[p] != '>') {
int v = -1;
if (cmap_data[p] >= '0' && cmap_data[p] <= '9') v = cmap_data[p] - '0';
else if (cmap_data[p] >= 'a' && cmap_data[p] <= 'f') v = cmap_data[p] - 'a' + 10;
else if (cmap_data[p] >= 'A' && cmap_data[p] <= 'F') v = cmap_data[p] - 'A' + 10;
if (v >= 0) dst = (dst << 4) | v;
p++;
}
if (p < stream_len) p++; // skip >
if (src >= 0 && src < 256 && dst > 0)
table[src] = (uint16_t)dst;
}
}
// Also handle "beginbfrange" sections: <XX> <XX> <XXXX>
if (starts_with(cmap_data, stream_len, p, "beginbfrange")) {
p += 12;
while (p < stream_len) {
while (p < stream_len && (cmap_data[p] == ' ' || cmap_data[p] == '\n' ||
cmap_data[p] == '\r' || cmap_data[p] == '\t'))
p++;
if (starts_with(cmap_data, stream_len, p, "endbfrange")) break;
// Parse <start> <end> <dst_start>
int vals[3] = {};
for (int vi = 0; vi < 3; vi++) {
if (p >= stream_len || cmap_data[p] != '<') goto done_range;
p++;
while (p < stream_len && cmap_data[p] != '>') {
int v = -1;
if (cmap_data[p] >= '0' && cmap_data[p] <= '9') v = cmap_data[p] - '0';
else if (cmap_data[p] >= 'a' && cmap_data[p] <= 'f') v = cmap_data[p] - 'a' + 10;
else if (cmap_data[p] >= 'A' && cmap_data[p] <= 'F') v = cmap_data[p] - 'A' + 10;
if (v >= 0) vals[vi] = (vals[vi] << 4) | v;
p++;
}
if (p < stream_len) p++;
while (p < stream_len && (cmap_data[p] == ' ' || cmap_data[p] == '\n' ||
cmap_data[p] == '\r' || cmap_data[p] == '\t'))
p++;
}
for (int c = vals[0]; c <= vals[1] && c < 256; c++)
table[c] = (uint16_t)(vals[2] + (c - vals[0]));
}
done_range:;
}
}
montauk::mfree(cmap_data);
return table;
}
// Load an embedded font from a FontFile stream, with caching.
static TrueTypeFont* load_embedded_font(int stream_obj_num) {
// Check cache
for (int i = 0; i < g_doc.emb_font_count; i++) {
if (g_doc.emb_fonts[i].stream_obj == stream_obj_num)
return g_doc.emb_fonts[i].font;
}
// Extract stream data
int data_len;
uint8_t* data = get_stream_data(stream_obj_num, &data_len);
if (!data || data_len < 12) {
if (data) montauk::mfree(data);
return nullptr;
}
// Init TrueTypeFont
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (!f) { montauk::mfree(data); return nullptr; }
montauk::memset(f, 0, sizeof(TrueTypeFont));
f->data = data;
f->cache_count = 0;
f->valid = false;
int offset = stbtt_GetFontOffsetForIndex(data, 0);
if (offset < 0 || !stbtt_InitFont(&f->info, data, offset)) {
montauk::mfree(data);
montauk::mfree(f);
return nullptr;
}
f->valid = true;
f->em_scaling = true; // PDF fonts use em-square sizing
// Add to cache
int idx = g_doc.emb_font_count;
if (idx == 0) {
g_doc.emb_fonts = (EmbeddedFontEntry*)montauk::malloc(16 * sizeof(EmbeddedFontEntry));
if (!g_doc.emb_fonts) { return f; } // still return the font, just don't cache
} else if ((idx & (idx - 1)) == 0 && idx >= 16) {
// Power of 2 - grow
auto* nf = (EmbeddedFontEntry*)montauk::malloc(idx * 2 * sizeof(EmbeddedFontEntry));
if (nf) {
montauk::memcpy(nf, g_doc.emb_fonts, idx * sizeof(EmbeddedFontEntry));
montauk::mfree(g_doc.emb_fonts);
g_doc.emb_fonts = nf;
}
}
if (g_doc.emb_fonts) {
g_doc.emb_fonts[idx].stream_obj = stream_obj_num;
g_doc.emb_fonts[idx].font = f;
g_doc.emb_fonts[idx].font_data = data;
g_doc.emb_font_count++;
}
return f;
}
void build_font_map(int page_obj_num, FontMap* out) {
out->count = 0;
int start, end;
if (find_obj_content(page_obj_num, &start, &end) < 0) return;
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
// Find /Resources - could be inline or a reference
int res_pos = dict_lookup(d, len, start, "Resources");
if (res_pos < 0) {
// Try parent /Pages for inherited resources
int parent_pos = dict_lookup(d, len, start, "Parent");
if (parent_pos >= 0) {
int parent_num;
if (parse_ref_at(d, len, parent_pos, &parent_num) > 0) {
int ps, pe;
if (find_obj_content(parent_num, &ps, &pe) == 0) {
res_pos = dict_lookup(d, len, ps, "Resources");
}
}
}
}
if (res_pos < 0) return;
// Resolve if it's a reference
int rp = skip_ws(d, len, res_pos);
int ref_num;
int rrp = parse_ref_at(d, len, rp, &ref_num);
int res_dict_start;
if (rrp > 0) {
int rs, re;
if (find_obj_content(ref_num, &rs, &re) < 0) return;
res_dict_start = rs;
} else {
res_dict_start = rp;
}
// Find /Font within resources
int font_pos = dict_lookup(d, len, res_dict_start, "Font");
if (font_pos < 0) return;
// Resolve if reference
int fp = skip_ws(d, len, font_pos);
int font_ref;
int frp = parse_ref_at(d, len, fp, &font_ref);
int font_dict_start;
if (frp > 0) {
int fs, fe;
if (find_obj_content(font_ref, &fs, &fe) < 0) return;
font_dict_start = fs;
} else {
font_dict_start = fp;
}
// Parse font dictionary: /F1 5 0 R /F2 6 0 R etc.
int fdp = skip_ws(d, len, font_dict_start);
if (fdp >= len || d[fdp] != '<') return;
fdp += 2; // skip <<
while (fdp < len && out->count < 32) {
fdp = skip_ws(d, len, fdp);
if (fdp >= len) break;
if (d[fdp] == '>' && fdp + 1 < len && d[fdp + 1] == '>') break;
// Read font name (e.g., /F1)
if (d[fdp] != '/') { fdp++; continue; }
fdp++; // skip '/'
FontInfo* fi = &out->fonts[out->count];
int ni = 0;
while (fdp < len && ni < 31 && d[fdp] != ' ' && d[fdp] != '\t' &&
d[fdp] != '\n' && d[fdp] != '\r' && d[fdp] != '/' &&
d[fdp] != '<' && d[fdp] != '>') {
fi->name[ni++] = (char)d[fdp++];
}
fi->name[ni] = '\0';
fi->flags = 0;
fi->tounicode = nullptr;
// Read font reference
fdp = skip_ws(d, len, fdp);
int font_obj;
int ref_end = parse_ref_at(d, len, fdp, &font_obj);
if (ref_end > 0) {
fdp = ref_end;
// Look up /BaseFont in the font object
int fs2, fe2;
if (find_obj_content(font_obj, &fs2, &fe2) == 0) {
int bf_pos = dict_lookup(d, len, fs2, "BaseFont");
if (bf_pos >= 0 && bf_pos < len && d[bf_pos] == '/') {
bf_pos++;
char base_font[64] = {};
int bi = 0;
while (bf_pos < len && bi < 63 && d[bf_pos] != ' ' &&
d[bf_pos] != '\t' && d[bf_pos] != '\n' &&
d[bf_pos] != '\r' && d[bf_pos] != '/' &&
d[bf_pos] != '<' && d[bf_pos] != '>') {
base_font[bi++] = (char)d[bf_pos++];
}
base_font[bi] = '\0';
if (str_contains(base_font, "Bold"))
fi->flags |= 1;
if (str_contains(base_font, "Italic") || str_contains(base_font, "Oblique"))
fi->flags |= 2;
if (str_contains(base_font, "Courier") || str_contains(base_font, "Mono"))
fi->flags |= 4;
}
// Parse /ToUnicode CMap if present
fi->tounicode = nullptr;
int tu_pos = dict_lookup(d, len, fs2, "ToUnicode");
if (tu_pos >= 0) {
int tu_ref;
if (parse_ref_at(d, len, tu_pos, &tu_ref) > 0) {
fi->tounicode = parse_tounicode(tu_ref);
}
}
// Try to load embedded font from FontDescriptor
fi->embedded_font = nullptr;
int fdesc_pos = dict_lookup(d, len, fs2, "FontDescriptor");
if (fdesc_pos >= 0) {
int fdesc_num;
if (parse_ref_at(d, len, fdesc_pos, &fdesc_num) > 0) {
int fds, fde;
if (find_obj_content(fdesc_num, &fds, &fde) == 0) {
// Try /FontFile2 (TrueType)
int ff_pos = dict_lookup(d, len, fds, "FontFile2");
if (ff_pos >= 0) {
int ff_num;
if (parse_ref_at(d, len, ff_pos, &ff_num) > 0)
fi->embedded_font = load_embedded_font(ff_num);
}
// Try /FontFile3 (CFF/OpenType)
if (!fi->embedded_font) {
ff_pos = dict_lookup(d, len, fds, "FontFile3");
if (ff_pos >= 0) {
int ff_num;
if (parse_ref_at(d, len, ff_pos, &ff_num) > 0)
fi->embedded_font = load_embedded_font(ff_num);
}
}
}
}
}
}
} else {
// Inline font dict or unknown - skip value
fdp = skip_value(d, len, fdp);
}
out->count++;
}
}
// ============================================================================
// Top-Level Load / Free
// ============================================================================
bool load_pdf(const char* path) {
free_pdf();
int fd = montauk::open(path);
if (fd < 0) {
str_cpy(g_status_msg, "Cannot open file", 128);
return false;
}
uint64_t fsize = montauk::getsize(fd);
if (fsize < 32 || fsize > 64 * 1024 * 1024) {
montauk::close(fd);
str_cpy(g_status_msg, "File too small or too large", 128);
return false;
}
g_doc.data = (uint8_t*)montauk::malloc((int)fsize);
if (!g_doc.data) {
montauk::close(fd);
str_cpy(g_status_msg, "Out of memory", 128);
return false;
}
montauk::read(fd, g_doc.data, 0, fsize);
montauk::close(fd);
g_doc.data_len = (int)fsize;
// Check PDF header
if (!starts_with(g_doc.data, g_doc.data_len, 0, "%PDF")) {
str_cpy(g_status_msg, "Not a PDF file", 128);
free_pdf();
return false;
}
// Find startxref from end of file
int p = g_doc.data_len - 1;
while (p > 0 && (g_doc.data[p] == '\n' || g_doc.data[p] == '\r' ||
g_doc.data[p] == ' ' || g_doc.data[p] == '%'))
p--;
// Skip past %%EOF
while (p > 0 && g_doc.data[p] != '\n' && g_doc.data[p] != '\r') p--;
// Now find "startxref"
while (p > 0) {
if (starts_with(g_doc.data, g_doc.data_len, p, "startxref")) break;
p--;
}
if (p <= 0) {
str_cpy(g_status_msg, "Cannot find startxref", 128);
free_pdf();
return false;
}
int xref_off = 0;
parse_int_at(g_doc.data, g_doc.data_len, p + 9, &xref_off);
if (xref_off <= 0 || xref_off >= g_doc.data_len) {
str_cpy(g_status_msg, "Invalid xref offset", 128);
free_pdf();
return false;
}
// Parse xref (traditional table or cross-reference stream)
bool xref_ok = false;
if (starts_with(g_doc.data, g_doc.data_len, xref_off, "xref")) {
xref_ok = parse_xref_table(xref_off);
} else {
xref_ok = parse_xref_stream(xref_off);
}
if (!xref_ok || g_doc.xref_count == 0) {
str_cpy(g_status_msg, "Failed to parse xref", 128);
free_pdf();
return false;
}
// Decompress compressed objects from object streams (xref type 2)
decompress_object_streams();
// Find /Root in trailer
int root_num = -1;
if (starts_with(g_doc.data, g_doc.data_len, xref_off, "xref")) {
// Traditional: find "trailer" after xref
int tp = xref_off;
while (tp + 7 < g_doc.data_len) {
if (starts_with(g_doc.data, g_doc.data_len, tp, "trailer")) {
tp += 7;
int root_pos = dict_lookup(g_doc.data, g_doc.data_len, tp, "Root");
if (root_pos >= 0)
parse_ref_at(g_doc.data, g_doc.data_len, root_pos, &root_num);
break;
}
tp++;
}
} else {
// Xref stream: /Root is in the stream object's dict
// The xref stream obj was already added to xref table
// Find its dict and look for /Root
int obj_num;
int op = parse_int_at(g_doc.data, g_doc.data_len, xref_off, &obj_num);
if (op > 0) {
int os, oe;
if (find_obj_content(obj_num, &os, &oe) == 0) {
int root_pos = dict_lookup(g_doc.data, g_doc.data_len, os, "Root");
if (root_pos >= 0)
parse_ref_at(g_doc.data, g_doc.data_len, root_pos, &root_num);
}
}
}
if (root_num < 0) {
str_cpy(g_status_msg, "Cannot find document root", 128);
free_pdf();
return false;
}
// Find /Pages from catalog
int cat_start, cat_end;
if (find_obj_content(root_num, &cat_start, &cat_end) < 0) {
str_cpy(g_status_msg, "Cannot read catalog", 128);
free_pdf();
return false;
}
int pages_pos = dict_lookup(g_doc.data, g_doc.data_len, cat_start, "Pages");
if (pages_pos < 0) {
str_cpy(g_status_msg, "Cannot find pages", 128);
free_pdf();
return false;
}
int pages_num;
if (parse_ref_at(g_doc.data, g_doc.data_len, pages_pos, &pages_num) < 0) {
str_cpy(g_status_msg, "Invalid pages reference", 128);
free_pdf();
return false;
}
// Collect all pages
collect_pages(pages_num, 0);
if (g_doc.page_count == 0) {
str_cpy(g_status_msg, "No pages found", 128);
free_pdf();
return false;
}
// Parse content for each page
for (int i = 0; i < g_doc.page_count; i++) {
parse_page(i, g_doc.page_objs[i]);
}
g_doc.valid = true;
snprintf(g_status_msg, 128, "%d page%s loaded", g_doc.page_count,
g_doc.page_count == 1 ? "" : "s");
return true;
}
void free_pdf() {
if (g_doc.data) { montauk::mfree(g_doc.data); g_doc.data = nullptr; }
if (g_doc.xref) { montauk::mfree(g_doc.xref); g_doc.xref = nullptr; }
if (g_doc.xref_stm) { montauk::mfree(g_doc.xref_stm); g_doc.xref_stm = nullptr; }
if (g_doc.xref_idx) { montauk::mfree(g_doc.xref_idx); g_doc.xref_idx = nullptr; }
if (g_doc.pages) {
for (int i = 0; i < g_doc.page_count; i++) {
if (g_doc.pages[i].items) montauk::mfree(g_doc.pages[i].items);
if (g_doc.pages[i].gfx_items) montauk::mfree(g_doc.pages[i].gfx_items);
}
montauk::mfree(g_doc.pages);
g_doc.pages = nullptr;
}
if (g_doc.page_objs) { montauk::mfree(g_doc.page_objs); g_doc.page_objs = nullptr; }
if (g_doc.emb_fonts) {
for (int i = 0; i < g_doc.emb_font_count; i++) {
if (g_doc.emb_fonts[i].font_data) montauk::mfree(g_doc.emb_fonts[i].font_data);
if (g_doc.emb_fonts[i].font) montauk::mfree(g_doc.emb_fonts[i].font);
}
montauk::mfree(g_doc.emb_fonts);
g_doc.emb_fonts = nullptr;
}
g_doc.emb_font_count = 0;
g_doc.data_len = 0;
g_doc.xref_count = 0;
g_doc.page_count = 0;
g_doc.page_cap = 0;
g_doc.valid = false;
}