/* * 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\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 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: 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 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; }