feat: pdfviewer - expand PDF rendering support

This commit is contained in:
2026-08-10 15:03:54 +02:00
parent 9b23d99082
commit 990d184bd0
8 changed files with 1609 additions and 263 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 96
#define MONTAUK_BUILD_NUMBER 99
+1 -1
View File
@@ -173,7 +173,7 @@ wordprocessor: libc libloader dialogs
$(MAKE) -C src/wordprocessor
# Build PDF viewer standalone GUI client (depends on libc and libdialogs).
pdfviewer: libc libloader dialogs
pdfviewer: libc libjpeg libloader dialogs
$(MAKE) -C src/pdfviewer
# Build disks standalone GUI tool (depends on libc).
+3 -2
View File
@@ -20,6 +20,7 @@ LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
JPEG_LIB := $(LIBDIR)/libjpeg/libjpeg.a
# ---- Compiler flags ----
@@ -63,9 +64,9 @@ LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(LIBDIR)/libc/liblibc.a
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(JPEG_LIB) $(LIBDIR)/libc/liblibc.a
mkdir -p $(BINDIR)/apps/pdfviewer
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(LIBDIR)/libc/liblibc.a -o $@
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(JPEG_LIB) $(LIBDIR)/libc/liblibc.a -o $@
HEADERS := pdfviewer.h
+2
View File
@@ -85,6 +85,8 @@ static void go_to_page(int page) {
if (page >= g_doc.page_count) page = g_doc.page_count - 1;
g_current_page = page;
g_scroll_y = 0;
ensure_page_parsed(page);
trim_page_cache(page);
}
static void zoom_in() {
File diff suppressed because it is too large Load Diff
+589 -74
View File
@@ -6,6 +6,11 @@
#include "pdfviewer.h"
extern "C" {
#include <stdlib.h>
#include <gui/stb_image.h>
}
// ============================================================================
// DEFLATE Decompression (RFC 1951)
// ============================================================================
@@ -155,9 +160,10 @@ static int inflate_raw(BitStream* bs, uint8_t* out, int out_cap) {
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);
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 (len > out_cap - out_pos) goto fail;
for (int i = 0; i < len; i++) {
if (bs->byte_pos >= bs->len) goto fail;
out[out_pos++] = bs->data[bs->byte_pos++];
}
@@ -274,8 +280,10 @@ static uint8_t* inflate_zlib(const uint8_t* src, int src_len, int* out_len) {
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;
for (int mult = 8; mult <= 512; mult *= 2) {
uint64_t wanted = (uint64_t)src_len * mult;
if (wanted > 256ULL * 1024 * 1024) wanted = 256ULL * 1024 * 1024;
int cap = (int)wanted;
if (cap < 65536) cap = 65536;
uint8_t* dst = (uint8_t*)montauk::malloc(cap);
if (!dst) return nullptr;
@@ -287,6 +295,7 @@ static uint8_t* inflate_zlib(const uint8_t* src, int src_len, int* out_len) {
return dst;
}
montauk::mfree(dst);
if (cap == 256 * 1024 * 1024) break;
}
return nullptr;
}
@@ -582,12 +591,15 @@ uint8_t* get_stream_data(int obj_num, int* out_len) {
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;
if (starts_with(d, len, fp + 1, "FlateDecode") ||
starts_with(d, len, fp + 1, "Fl")) 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"))
if (fp < len && d[fp] == '/' &&
(starts_with(d, len, fp + 1, "FlateDecode") ||
starts_with(d, len, fp + 1, "Fl")))
is_flate = true;
}
}
@@ -690,7 +702,9 @@ static bool parse_xref_table(int pos) {
}
char type = (char)d[p + 17];
if (type == 'n' && offset > 0) {
// Sections are parsed newest-to-oldest, so never let an older
// revision overwrite the current location of an object.
if (type == 'n' && offset > 0 && g_doc.xref[start_num + i] == 0) {
g_doc.xref[start_num + i] = offset;
}
@@ -743,14 +757,14 @@ static bool parse_xref_stream(int pos) {
}
// Get /Index array (optional, defaults to [0 Size])
int index_pairs[32]; // start, count pairs
int index_pairs[256]; // 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) {
while (index_count < 254) {
ip = skip_ws(d, len, ip);
if (ip >= len || d[ip] == ']') break;
int v;
@@ -766,19 +780,42 @@ static bool parse_xref_stream(int pos) {
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));
// Grow rather than replace: incremental PDFs chain several xref sections.
if (size > g_doc.xref_count) {
int old_count = g_doc.xref_count;
int* nx = (int*)montauk::malloc(size * sizeof(int));
int* ns = (int*)montauk::malloc(size * sizeof(int));
int* ni = (int*)montauk::malloc(size * sizeof(int));
if (!nx || !ns || !ni) {
if (nx) montauk::mfree(nx);
if (ns) montauk::mfree(ns);
if (ni) montauk::mfree(ni);
return false;
}
montauk::memset(nx, 0, size * sizeof(int));
montauk::memset(ns, 0, size * sizeof(int));
montauk::memset(ni, 0, size * sizeof(int));
if (old_count) {
montauk::memcpy(nx, g_doc.xref, old_count * sizeof(int));
if (g_doc.xref_stm) montauk::memcpy(ns, g_doc.xref_stm, old_count * sizeof(int));
if (g_doc.xref_idx) montauk::memcpy(ni, g_doc.xref_idx, old_count * sizeof(int));
}
if (g_doc.xref) montauk::mfree(g_doc.xref);
if (g_doc.xref_stm) montauk::mfree(g_doc.xref_stm);
if (g_doc.xref_idx) montauk::mfree(g_doc.xref_idx);
g_doc.xref = nx; g_doc.xref_stm = ns; g_doc.xref_idx = ni;
g_doc.xref_count = size;
} else if (!g_doc.xref_stm || !g_doc.xref_idx) {
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 || !g_doc.xref_idx) return false;
montauk::memset(g_doc.xref_stm, 0, size * sizeof(int));
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;
if (obj_num < size && g_doc.xref[obj_num] == 0) g_doc.xref[obj_num] = pos;
// Get /Filter and /Length, then extract stream data
int filter_pos = dict_lookup(d, len, dict_start, "Filter");
@@ -852,9 +889,11 @@ static bool parse_xref_stream(int pos) {
int obj_idx = start_obj + i;
if (obj_idx < size) {
if (type == 1 && field2 > 0) {
if (type == 1 && field2 > 0 && g_doc.xref[obj_idx] == 0 &&
g_doc.xref_stm[obj_idx] == 0) {
g_doc.xref[obj_idx] = field2;
} else if (type == 2 && g_doc.xref_stm && g_doc.xref_idx) {
} else if (type == 2 && g_doc.xref[obj_idx] == 0 &&
g_doc.xref_stm[obj_idx] == 0) {
g_doc.xref_stm[obj_idx] = field2; // containing ObjStm number
g_doc.xref_idx[obj_idx] = field3; // index within stream
}
@@ -868,6 +907,33 @@ static bool parse_xref_stream(int pos) {
return true;
}
static int xref_dict_value(int pos, const char* key) {
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int dict_start = -1;
if (starts_with(d, len, pos, "xref")) {
int p = pos + 4;
while (p + 7 < len) {
if (starts_with(d, len, p, "trailer")) { dict_start = p + 7; break; }
if (starts_with(d, len, p, "startxref")) break;
p++;
}
} else {
int obj, gen;
int p = parse_int_at(d, len, pos, &obj);
if (p > 0) p = parse_int_at(d, len, p, &gen);
if (p > 0) {
p = skip_ws(d, len, p);
if (starts_with(d, len, p, "obj")) dict_start = skip_ws(d, len, p + 3);
}
}
if (dict_start < 0) return 0;
int value_pos = dict_lookup(d, len, dict_start, key);
int value = 0;
if (value_pos >= 0) parse_int_at(d, len, value_pos, &value);
return value;
}
// ============================================================================
// Object Stream Decompression (xref type 2)
// ============================================================================
@@ -876,7 +942,8 @@ static void decompress_object_streams() {
if (!g_doc.xref_stm) return;
// Collect unique object stream numbers
int stm_objs[256];
int* stm_objs = (int*)montauk::malloc(g_doc.xref_count * sizeof(int));
if (!stm_objs) return;
int stm_count = 0;
for (int i = 0; i < g_doc.xref_count; i++) {
@@ -886,17 +953,17 @@ static void decompress_object_streams() {
for (int j = 0; j < stm_count; j++) {
if (stm_objs[j] == sobj) { found = true; break; }
}
if (!found && stm_count < 256)
if (!found)
stm_objs[stm_count++] = sobj;
}
if (stm_count == 0) return;
if (stm_count == 0) { montauk::mfree(stm_objs); 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;
if (!synth) { montauk::mfree(stm_objs); return; }
int synth_len = 0;
for (int si = 0; si < stm_count; si++) {
@@ -1014,6 +1081,7 @@ static void decompress_object_streams() {
}
montauk::mfree(synth);
montauk::mfree(stm_objs);
}
// ============================================================================
@@ -1046,11 +1114,38 @@ static void add_page(int obj_num) {
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].fill_items = nullptr;
g_doc.pages[g_doc.page_count].fill_count = 0;
g_doc.pages[g_doc.page_count].fill_cap = 0;
g_doc.pages[g_doc.page_count].image_items = nullptr;
g_doc.pages[g_doc.page_count].image_count = 0;
g_doc.pages[g_doc.page_count].image_cap = 0;
g_doc.pages[g_doc.page_count].paint_commands = nullptr;
g_doc.pages[g_doc.page_count].paint_count = 0;
g_doc.pages[g_doc.page_count].paint_cap = 0;
g_doc.pages[g_doc.page_count].width = 612;
g_doc.pages[g_doc.page_count].height = 792;
g_doc.pages[g_doc.page_count].parsed = false;
g_doc.page_count++;
}
// Return the position of a page attribute, following the /Parent chain. PDF
// page resources and boxes are inheritable and are commonly stored only once
// at the root of a large page tree.
int find_inherited_page_value(int page_obj_num, const char* key) {
int obj = page_obj_num;
for (int depth = 0; depth < 64; depth++) {
int start, end;
if (find_obj_content(obj, &start, &end) < 0) return -1;
int value = dict_lookup(g_doc.data, g_doc.data_len, start, key);
if (value >= 0) return value;
int parent = dict_lookup(g_doc.data, g_doc.data_len, start, "Parent");
if (parent < 0 || parse_ref_at(g_doc.data, g_doc.data_len, parent, &obj) < 0)
return -1;
}
return -1;
}
static void collect_pages(int obj_num, int depth) {
if (depth > 20) return; // prevent infinite recursion
int start, end;
@@ -1069,7 +1164,7 @@ static void collect_pages(int obj_num, int depth) {
add_page(obj_num);
// Get MediaBox
int mb_pos = dict_lookup(d, len, start, "MediaBox");
int mb_pos = find_inherited_page_value(obj_num, "MediaBox");
if (mb_pos >= 0) {
int mp = skip_ws(d, len, mb_pos);
// Could be a reference
@@ -1139,6 +1234,23 @@ static bool str_contains(const char* haystack, const char* needle) {
// 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 bool parse_cmap_hex(const uint8_t* data, int len, int* position, int* value) {
int p=*position;
while(p<len&&(data[p]==' '||data[p]=='\n'||data[p]=='\r'||data[p]=='\t'))p++;
if(p>=len||data[p]!='<')return false;
p++;int result=0;
while(p<len&&data[p]!='>') {
int digit=-1;
if(data[p]>='0'&&data[p]<='9')digit=data[p]-'0';
else if(data[p]>='a'&&data[p]<='f')digit=data[p]-'a'+10;
else if(data[p]>='A'&&data[p]<='F')digit=data[p]-'A'+10;
if(digit>=0)result=(result<<4)|digit;
p++;
}
if(p>=len)return false;
*position=p+1;*value=result;return true;
}
static uint16_t* parse_tounicode(int cmap_obj_num) {
int stream_len;
uint8_t* cmap_data = get_stream_data(cmap_obj_num, &stream_len);
@@ -1209,28 +1321,29 @@ static uint16_t* parse_tounicode(int cmap_obj_num) {
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;
int first,end;
if(!parse_cmap_hex(cmap_data,stream_len,&p,&first)||
!parse_cmap_hex(cmap_data,stream_len,&p,&end))break;
while(p<stream_len&&(cmap_data[p]==' '||cmap_data[p]=='\n'||
cmap_data[p]=='\r'||cmap_data[p]=='\t'))p++;
if(p<stream_len&&cmap_data[p]=='<') {
int destination;
if(!parse_cmap_hex(cmap_data,stream_len,&p,&destination))break;
for(int code=first;code<=end&&code<256;code++)
if(code>=0)table[code]=(uint16_t)(destination+code-first);
} else if(p<stream_len&&cmap_data[p]=='[') {
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++;
for(int code=first;code<=end;code++) {
int destination;
if(!parse_cmap_hex(cmap_data,stream_len,&p,&destination))break;
if(code>=0&&code<256)table[code]=(uint16_t)destination;
}
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++;
while(p<stream_len&&cmap_data[p]!=']')p++;
if(p<stream_len)p++;
} else {
break;
}
for (int c = vals[0]; c <= vals[1] && c < 256; c++)
table[c] = (uint16_t)(vals[2] + (c - vals[0]));
}
done_range:;
}
}
@@ -1295,7 +1408,76 @@ static TrueTypeFont* load_embedded_font(int stream_obj_num) {
return f;
}
void build_font_map(int page_obj_num, FontMap* out) {
static TrueTypeFont* load_font_from_descriptor(int font_start) {
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int descriptor_pos = dict_lookup(d, len, font_start, "FontDescriptor");
int descriptor_obj;
if (descriptor_pos < 0 ||
parse_ref_at(d, len, descriptor_pos, &descriptor_obj) < 0)
return nullptr;
int ds, de;
if (find_obj_content(descriptor_obj, &ds, &de) < 0) return nullptr;
int file_pos = dict_lookup(d, len, ds, "FontFile2");
int file_obj;
if (file_pos >= 0 && parse_ref_at(d, len, file_pos, &file_obj) > 0)
return load_embedded_font(file_obj);
file_pos = dict_lookup(d, len, ds, "FontFile3");
if (file_pos >= 0 && parse_ref_at(d, len, file_pos, &file_obj) > 0)
return load_embedded_font(file_obj);
return nullptr;
}
static void parse_cid_widths(int font_start, FontInfo* font) {
const uint8_t* d = g_doc.data;
int len = g_doc.data_len;
int default_width = 1000;
int default_pos = dict_lookup(d, len, font_start, "DW");
if (default_pos >= 0) parse_int_at(d, len, default_pos, &default_width);
for (int i = 0; i < 256; i++) font->widths[i] = (float)default_width;
font->has_widths = true;
int widths_pos = dict_lookup(d, len, font_start, "W");
if (widths_pos < 0) return;
int p = skip_ws(d, len, widths_pos);
if (p >= len || d[p] != '[') return;
p++;
while (p < len) {
p = skip_ws(d, len, p);
if (p >= len || d[p] == ']') break;
int first;
int next = parse_int_at(d, len, p, &first);
if (next < 0) break;
p = skip_ws(d, len, next);
if (p < len && d[p] == '[') {
p++;
int code = first;
while (p < len) {
p = skip_ws(d, len, p);
if (p >= len || d[p] == ']') { if (p < len) p++; break; }
float width;
next = parse_real_at(d, len, p, &width);
if (next < 0) return;
if (code >= 0 && code < 256) font->widths[code] = width;
code++;
p = next;
}
} else {
int last;
next = parse_int_at(d, len, p, &last);
if (next < 0) break;
float width;
next = parse_real_at(d, len, next, &width);
if (next < 0) break;
if (first < 0) first = 0;
if (last > 255) last = 255;
for (int code = first; code <= last; code++) font->widths[code] = width;
p = next;
}
}
}
static void build_font_map_impl(int page_obj_num, bool inherit_page_resources, FontMap* out) {
out->count = 0;
int start, end;
@@ -1305,20 +1487,9 @@ void build_font_map(int page_obj_num, FontMap* out) {
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");
}
}
}
}
int res_pos = inherit_page_resources
? find_inherited_page_value(page_obj_num, "Resources")
: dict_lookup(d, len, start, "Resources");
if (res_pos < 0) return;
// Resolve if it's a reference
@@ -1356,7 +1527,7 @@ void build_font_map(int page_obj_num, FontMap* out) {
if (fdp >= len || d[fdp] != '<') return;
fdp += 2; // skip <<
while (fdp < len && out->count < 32) {
while (fdp < len && out->count < 64) {
fdp = skip_ws(d, len, fdp);
if (fdp >= len) break;
if (d[fdp] == '>' && fdp + 1 < len && d[fdp + 1] == '>') break;
@@ -1375,6 +1546,10 @@ void build_font_map(int page_obj_num, FontMap* out) {
fi->name[ni] = '\0';
fi->flags = 0;
fi->tounicode = nullptr;
fi->embedded_font = nullptr;
fi->has_widths = false;
fi->two_byte = false;
for (int wi = 0; wi < 256; wi++) fi->widths[wi] = 0;
// Read font reference
fdp = skip_ws(d, len, fdp);
@@ -1417,8 +1592,43 @@ void build_font_map(int page_obj_num, FontMap* out) {
}
}
// Simple fonts describe text advances independently of the
// embedded TrueType program. Keeping these values avoids
// cumulative rounding gaps when a producer splits a word
// across several positioned text runs.
int first_char = 0;
int first_pos = dict_lookup(d, len, fs2, "FirstChar");
if (first_pos >= 0) parse_int_at(d, len, first_pos, &first_char);
int widths_pos = dict_lookup(d, len, fs2, "Widths");
if (widths_pos >= 0) {
int wp = skip_ws(d, len, widths_pos);
int widths_obj;
if (parse_ref_at(d, len, wp, &widths_obj) > 0) {
int ws, we;
if (find_obj_content(widths_obj, &ws, &we) == 0)
wp = skip_ws(d, len, ws);
}
if (wp < len && d[wp] == '[') {
wp++;
for (int code = first_char; code < 256; code++) {
wp = skip_ws(d, len, wp);
if (wp >= len || d[wp] == ']') break;
float width;
int next = parse_real_at(d, len, wp, &width);
if (next < 0) break;
fi->widths[code] = width;
fi->has_widths = true;
wp = next;
}
}
}
int encoding_pos = dict_lookup(d, len, fs2, "Encoding");
if (encoding_pos >= 0 && d[encoding_pos] == '/' &&
starts_with(d, len, encoding_pos + 1, "Identity-H"))
fi->two_byte = true;
// 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;
@@ -1444,6 +1654,23 @@ void build_font_map(int page_obj_num, FontMap* out) {
}
}
}
// Type0 fonts keep metrics and the embedded program in their
// CIDFont descendant rather than in the outer font object.
int descendants_pos = dict_lookup(d, len, fs2, "DescendantFonts");
if (descendants_pos >= 0) {
int dp = skip_ws(d, len, descendants_pos);
if (dp < len && d[dp] == '[') dp = skip_ws(d, len, dp + 1);
int descendant_obj;
if (parse_ref_at(d, len, dp, &descendant_obj) > 0) {
int cds, cde;
if (find_obj_content(descendant_obj, &cds, &cde) == 0) {
parse_cid_widths(cds, fi);
if (!fi->embedded_font)
fi->embedded_font = load_font_from_descriptor(cds);
}
}
}
}
} else {
// Inline font dict or unknown - skip value
@@ -1454,6 +1681,259 @@ void build_font_map(int page_obj_num, FontMap* out) {
}
}
void build_font_map(int page_obj_num, FontMap* out) {
build_font_map_impl(page_obj_num, true, out);
}
void build_object_font_map(int obj_num, FontMap* out) {
build_font_map_impl(obj_num, false, out);
}
static int image_int_value(int start, const char* key, int fallback) {
int p = dict_lookup(g_doc.data, g_doc.data_len, start, key);
int value = fallback;
if (p >= 0) parse_int_at(g_doc.data, g_doc.data_len, p, &value);
return value;
}
static void undo_png_predictor(uint8_t* data, int len, int row_bytes, int bpp) {
if (row_bytes <= 0 || bpp <= 0) return;
int stride = row_bytes + 1;
int rows = len / stride;
for (int y = 0; y < rows; y++) {
uint8_t filter = data[y * stride];
uint8_t* row = data + y * stride + 1;
uint8_t* prev = y ? data + (y - 1) * stride + 1 : nullptr;
for (int x = 0; x < row_bytes; x++) {
int a = x >= bpp ? row[x - bpp] : 0;
int b = prev ? prev[x] : 0;
int c = (prev && x >= bpp) ? prev[x - bpp] : 0;
if (filter == 1) row[x] = (uint8_t)(row[x] + a);
else if (filter == 2) row[x] = (uint8_t)(row[x] + b);
else if (filter == 3) row[x] = (uint8_t)(row[x] + ((a + b) >> 1));
else if (filter == 4) {
int p = a + b - c, pa = p - a, pb = p - b, pc = p - c;
if (pa < 0) pa = -pa;
if (pb < 0) pb = -pb;
if (pc < 0) pc = -pc;
row[x] = (uint8_t)(row[x] + (pa <= pb && pa <= pc ? a : (pb <= pc ? b : c)));
}
}
}
// Remove the per-row filter bytes in place.
for (int y = 0; y < rows; y++) {
uint8_t* dst = data + y * row_bytes;
uint8_t* src = data + y * stride + 1;
for (int x = 0; x < row_bytes; x++) dst[x] = src[x];
}
}
static int image_mask_stack[16];
static int image_mask_depth = 0;
static int skip_name_token(const uint8_t* d,int len,int p) {
p=skip_ws(d,len,p);if(p<len&&d[p]=='/')p++;
while(p<len&&d[p]!=' '&&d[p]!='\t'&&d[p]!='\r'&&d[p]!='\n'&&
d[p]!='/'&&d[p]!='['&&d[p]!=']'&&d[p]!='<'&&d[p]!='>'&&
d[p]!='('&&d[p]!=')')p++;
return p;
}
static int read_lookup_bytes(int p,uint8_t* out,int cap) {
const uint8_t* d=g_doc.data;int len=g_doc.data_len;
p=skip_ws(d,len,p);int obj;
if(parse_ref_at(d,len,p,&obj)>0) {
int n=0;uint8_t* stream=get_stream_data(obj,&n);
if(!stream)return 0;
if(n>cap)n=cap;
montauk::memcpy(out,stream,n);
montauk::mfree(stream);
return n;
}
int count=0;
if(p<len&&d[p]=='<') {
p++;int high=-1;
while(p<len&&d[p]!='>'&&count<cap){int v=-1;if(d[p]>='0'&&d[p]<='9')v=d[p]-'0';else if(d[p]>='A'&&d[p]<='F')v=d[p]-'A'+10;else if(d[p]>='a'&&d[p]<='f')v=d[p]-'a'+10;p++;if(v<0)continue;if(high<0)high=v;else{out[count++]=(uint8_t)((high<<4)|v);high=-1;}}
if(high>=0&&count<cap)out[count++]=(uint8_t)(high<<4);
} else if(p<len&&d[p]=='(') {
p++;int depth=1;
while(p<len&&depth&&count<cap){uint8_t c=d[p++];if(c=='\\'&&p<len){c=d[p++];if(c>='0'&&c<='7'){int v=c-'0';for(int k=0;k<2&&p<len&&d[p]>='0'&&d[p]<='7';k++)v=v*8+d[p++]-'0';c=(uint8_t)v;}else if(c=='n')c='\n';else if(c=='r')c='\r';else if(c=='t')c='\t';out[count++]=c;}else if(c=='('){depth++;out[count++]=c;}else if(c==')'){depth--;if(depth)out[count++]=c;}else out[count++]=c;}
}
return count;
}
static bool read_indexed_palette(int cs,uint32_t* palette,int* palette_count) {
const uint8_t* d=g_doc.data;int len=g_doc.data_len;int ref,s,e;
cs=skip_ws(d,len,cs);
if(parse_ref_at(d,len,cs,&ref)>0){if(find_obj_content(ref,&s,&e)<0)return false;cs=skip_ws(d,len,s);}
if(cs>=len||d[cs]!='[')return false;
int p=skip_ws(d,len,cs+1);
if(p>=len||d[p]!='/'||
!(starts_with(d,len,p+1,"Indexed")||
(p+2<len&&d[p+1]=='I'&&(d[p+2]==' '||d[p+2]=='\t'||d[p+2]=='\r'||d[p+2]=='\n'))))return false;
p=skip_name_token(d,len,p);
p=skip_ws(d,len,p);if(p>=len||d[p]!='/')return false;
int components=3;
if(starts_with(d,len,p+1,"DeviceGray"))components=1;
else if(starts_with(d,len,p+1,"DeviceCMYK"))components=4;
p=skip_name_token(d,len,p);
int high=0;p=parse_int_at(d,len,p,&high);if(p<0||high<0)return false;if(high>255)high=255;
uint8_t bytes[1024];int got=read_lookup_bytes(p,bytes,sizeof(bytes));
if(got<(high+1)*components)return false;
for(int i=0;i<=high;i++) {
uint8_t r,g,b;
if(components==1)r=g=b=bytes[i];
else if(components==3){r=bytes[i*3];g=bytes[i*3+1];b=bytes[i*3+2];}
else{int c=bytes[i*4],m=bytes[i*4+1],y=bytes[i*4+2],k=bytes[i*4+3];r=(uint8_t)(255-((c+k)>255?255:c+k));g=(uint8_t)(255-((m+k)>255?255:m+k));b=(uint8_t)(255-((y+k)>255?255:y+k));}
palette[i]=0xFF000000u|((uint32_t)r<<16)|((uint32_t)g<<8)|b;
}
*palette_count=high+1;return true;
}
static int packed_sample(const uint8_t* row,int sample,int bits) {
int bit=sample*bits,shift=8-bits-(bit&7);
return (row[bit>>3]>>shift)&((1<<bits)-1);
}
bool load_pdf_image(int obj_num, uint32_t** out_pixels, int* out_width, int* out_height) {
for (int i = 0; i < g_doc.image_count; i++) {
if (g_doc.images[i].obj_num == obj_num) {
*out_pixels = g_doc.images[i].pixels;
*out_width = g_doc.images[i].width;
*out_height = g_doc.images[i].height;
return true;
}
}
int start, end;
if (find_obj_content(obj_num, &start, &end) < 0) return false;
int width = image_int_value(start, "Width", 0);
int height = image_int_value(start, "Height", 0);
int image_mask_pos=dict_lookup(g_doc.data,g_doc.data_len,start,"ImageMask");
bool image_mask=image_mask_pos>=0&&starts_with(g_doc.data,g_doc.data_len,image_mask_pos,"true");
int bits = image_int_value(start, "BitsPerComponent", image_mask?1:8);
if (width <= 0 || height <= 0 || width > 16384 || height > 16384 ||
(bits != 1 && bits != 2 && bits != 4 && bits != 8))
return false;
int filter = dict_lookup(g_doc.data, g_doc.data_len, start, "Filter");
bool jpeg = filter >= 0 && (starts_with(g_doc.data, g_doc.data_len, filter + 1, "DCTDecode") ||
starts_with(g_doc.data, g_doc.data_len, filter + 1, "DCT") ||
(g_doc.data[filter] == '[' && starts_with(g_doc.data, g_doc.data_len,
skip_ws(g_doc.data, g_doc.data_len, filter + 1) + 1, "DCTDecode")));
int data_len = 0;
uint8_t* data = get_stream_data(obj_num, &data_len);
if (!data) return false;
uint32_t* pixels = nullptr;
if (jpeg) {
int jw = 0, jh = 0, channels = 0;
unsigned char* rgba = stbi_load_from_memory(data, data_len, &jw, &jh, &channels, 4);
if (rgba && jw > 0 && jh > 0 && jw <= 16384 && jh <= 16384) {
width = jw; height = jh;
pixels = (uint32_t*)montauk::malloc((uint64_t)width * height * sizeof(uint32_t));
if (pixels) for (int i = 0; i < width * height; i++)
pixels[i] = 0xFF000000u | ((uint32_t)rgba[i*4] << 16) |
((uint32_t)rgba[i*4+1] << 8) | rgba[i*4+2];
}
if (rgba) free(rgba);
} else {
int cs = dict_lookup(g_doc.data, g_doc.data_len, start, "ColorSpace");
int resolved_cs=cs,cs_ref,cs_start,cs_end;
if(cs>=0&&parse_ref_at(g_doc.data,g_doc.data_len,cs,&cs_ref)>0&&
find_obj_content(cs_ref,&cs_start,&cs_end)==0)resolved_cs=cs_start;
int components = 3;
uint32_t palette[256];int palette_count=0;
bool indexed=cs>=0&&read_indexed_palette(cs,palette,&palette_count);
if(indexed||image_mask)components=1;
if (resolved_cs >= 0 && starts_with(g_doc.data, g_doc.data_len, resolved_cs + 1, "DeviceGray")) components = 1;
else if (resolved_cs >= 0 && starts_with(g_doc.data, g_doc.data_len, resolved_cs + 1, "DeviceCMYK")) components = 4;
int row_bytes = (width * components * bits + 7) / 8;
int predictor_pos = dict_lookup(g_doc.data, g_doc.data_len, start, "DecodeParms");
if (predictor_pos >= 0) {
int params_ref, ps, pe;
if (parse_ref_at(g_doc.data, g_doc.data_len, predictor_pos, &params_ref) > 0 &&
find_obj_content(params_ref, &ps, &pe) == 0) predictor_pos = ps;
int predictor = image_int_value(predictor_pos, "Predictor", 1);
int bpp=(components*bits+7)/8;if(bpp<1)bpp=1;
if (predictor >= 10) undo_png_predictor(data, data_len, row_bytes, bpp);
}
if ((uint64_t)row_bytes * height <= (uint64_t)data_len) {
pixels = (uint32_t*)montauk::malloc((uint64_t)width * height * sizeof(uint32_t));
int sample_max=(1<<bits)-1;
bool invert=false;
int decode_pos=dict_lookup(g_doc.data,g_doc.data_len,start,"Decode");
if(decode_pos>=0){int dp=skip_ws(g_doc.data,g_doc.data_len,decode_pos);if(dp<g_doc.data_len&&g_doc.data[dp]=='['){float first=0,last=1;int next=parse_real_at(g_doc.data,g_doc.data_len,dp+1,&first);if(next>0&&parse_real_at(g_doc.data,g_doc.data_len,next,&last)>0)invert=first>last;}}
if (pixels) for (int y_pos=0;y_pos<height;y_pos++) for(int x_pos=0;x_pos<width;x_pos++) {
const uint8_t* row=data+y_pos*row_bytes;
uint8_t r, g, b;
int first_sample=packed_sample(row,x_pos*components,bits);
if(invert)first_sample=sample_max-first_sample;
if(image_mask) {
uint32_t alpha=first_sample==0?255:0;
pixels[y_pos*width+x_pos]=(alpha<<24)|0x00FFFFFFu;continue;
}
if(indexed) {
int index=first_sample;
uint32_t color=index<palette_count?palette[index]:0xFF000000u;
pixels[y_pos*width+x_pos]=color;continue;
}
int sample=x_pos*components;
if (components == 1) {int v=first_sample*255/sample_max;r=g=b=(uint8_t)v;}
else if (components == 3) { r=(uint8_t)(packed_sample(row,sample,bits)*255/sample_max);g=(uint8_t)(packed_sample(row,sample+1,bits)*255/sample_max);b=(uint8_t)(packed_sample(row,sample+2,bits)*255/sample_max); }
else {
int c=packed_sample(row,sample,bits)*255/sample_max,m=packed_sample(row,sample+1,bits)*255/sample_max,y=packed_sample(row,sample+2,bits)*255/sample_max,k=packed_sample(row,sample+3,bits)*255/sample_max;
r=(uint8_t)(255 - ((c+k)>255?255:c+k));
g=(uint8_t)(255 - ((m+k)>255?255:m+k));
b=(uint8_t)(255 - ((y+k)>255?255:y+k));
}
pixels[y_pos*width+x_pos] = 0xFF000000u | ((uint32_t)r<<16) | ((uint32_t)g<<8) | b;
}
}
}
montauk::mfree(data);
if (!pixels) return false;
// Apply a soft mask when present. Masks may have a different resolution;
// sample them proportionally into the base image.
int smask_pos = dict_lookup(g_doc.data, g_doc.data_len, start, "SMask");
int smask_obj = -1;
if (smask_pos >= 0 && parse_ref_at(g_doc.data, g_doc.data_len, smask_pos, &smask_obj) > 0 &&
smask_obj != obj_num) {
bool cycle = image_mask_depth >= 16;
for (int i=0;i<image_mask_depth;i++) if (image_mask_stack[i] == smask_obj) cycle=true;
uint32_t* mask_pixels=nullptr; int mask_w=0, mask_h=0;
bool mask_ok = false;
if (!cycle) {
image_mask_stack[image_mask_depth++] = obj_num;
mask_ok = load_pdf_image(smask_obj, &mask_pixels, &mask_w, &mask_h);
image_mask_depth--;
}
if (mask_ok) {
for (int y=0;y<height;y++) for (int x=0;x<width;x++) {
uint32_t mask=mask_pixels[(y*mask_h/height)*mask_w+(x*mask_w/width)];
uint32_t alpha=(mask>>16)&255;
pixels[y*width+x]=(pixels[y*width+x]&0x00FFFFFFu)|(alpha<<24);
}
}
}
if (g_doc.image_count >= g_doc.image_cap) {
int cap = g_doc.image_cap ? g_doc.image_cap * 2 : 16;
PdfImageCacheEntry* entries = (PdfImageCacheEntry*)montauk::malloc(cap * sizeof(PdfImageCacheEntry));
if (!entries) { montauk::mfree(pixels); return false; }
if (g_doc.images) {
montauk::memcpy(entries, g_doc.images, g_doc.image_count * sizeof(PdfImageCacheEntry));
montauk::mfree(g_doc.images);
}
g_doc.images = entries; g_doc.image_cap = cap;
}
g_doc.images[g_doc.image_count++] = {obj_num, pixels, width, height};
*out_pixels = pixels; *out_width = width; *out_height = height;
return true;
}
// ============================================================================
// Top-Level Load / Free
// ============================================================================
@@ -1468,7 +1948,7 @@ bool load_pdf(const char* path) {
}
uint64_t fsize = montauk::getsize(fd);
if (fsize < 32 || fsize > 64 * 1024 * 1024) {
if (fsize < 32 || fsize > 512ULL * 1024 * 1024) {
montauk::close(fd);
str_cpy(g_status_msg, "File too small or too large", 128);
return false;
@@ -1480,8 +1960,20 @@ bool load_pdf(const char* path) {
str_cpy(g_status_msg, "Out of memory", 128);
return false;
}
montauk::read(fd, g_doc.data, 0, fsize);
uint64_t read_off = 0;
while (read_off < fsize) {
uint64_t chunk = fsize - read_off;
if (chunk > 1024 * 1024) chunk = 1024 * 1024;
int got = montauk::read(fd, g_doc.data + read_off, read_off, chunk);
if (got <= 0) break;
read_off += (uint64_t)got;
}
montauk::close(fd);
if (read_off != fsize) {
str_cpy(g_status_msg, "Could not read complete PDF", 128);
free_pdf();
return false;
}
g_doc.data_len = (int)fsize;
// Check PDF header
@@ -1517,12 +2009,22 @@ bool load_pdf(const char* path) {
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);
// Parse newest-to-oldest so incremental updates retain all unchanged
// objects. Also honor /XRefStm in hybrid-reference files.
bool xref_ok = true;
int section = xref_off;
for (int revision = 0; section > 0 && revision < 64; revision++) {
bool table = starts_with(g_doc.data, g_doc.data_len, section, "xref");
bool ok = table ? parse_xref_table(section) : parse_xref_stream(section);
if (!ok) { xref_ok = false; break; }
if (table) {
int hybrid = xref_dict_value(section, "XRefStm");
if (hybrid > 0 && hybrid < g_doc.data_len &&
!parse_xref_stream(hybrid)) { xref_ok = false; break; }
}
int prev = xref_dict_value(section, "Prev");
if (prev <= 0 || prev >= g_doc.data_len || prev == section) break;
section = prev;
}
if (!xref_ok || g_doc.xref_count == 0) {
@@ -1602,12 +2104,9 @@ bool load_pdf(const char* path) {
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;
ensure_page_parsed(0);
trim_page_cache(0);
snprintf(g_status_msg, 128, "%d page%s loaded", g_doc.page_count,
g_doc.page_count == 1 ? "" : "s");
return true;
@@ -1622,6 +2121,14 @@ void free_pdf() {
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);
if (g_doc.pages[i].fill_items) {
for (int j=0;j<g_doc.pages[i].fill_count;j++)
if (g_doc.pages[i].fill_items[j].edges)
montauk::mfree(g_doc.pages[i].fill_items[j].edges);
montauk::mfree(g_doc.pages[i].fill_items);
}
if (g_doc.pages[i].image_items) montauk::mfree(g_doc.pages[i].image_items);
if (g_doc.pages[i].paint_commands) montauk::mfree(g_doc.pages[i].paint_commands);
}
montauk::mfree(g_doc.pages);
g_doc.pages = nullptr;
@@ -1635,6 +2142,14 @@ void free_pdf() {
montauk::mfree(g_doc.emb_fonts);
g_doc.emb_fonts = nullptr;
}
if (g_doc.images) {
for (int i = 0; i < g_doc.image_count; i++)
if (g_doc.images[i].pixels) montauk::mfree(g_doc.images[i].pixels);
montauk::mfree(g_doc.images);
g_doc.images = nullptr;
}
g_doc.image_count = 0;
g_doc.image_cap = 0;
g_doc.emb_font_count = 0;
g_doc.data_len = 0;
g_doc.xref_count = 0;
+59 -1
View File
@@ -63,8 +63,13 @@ static constexpr Color HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
struct TextItem {
float x, y; // PDF user-space coordinates (origin bottom-left)
float font_size; // effective font size in points
float basis[4]; // text x/y axes transformed into page user space
float advance; // PDF text-space advance for this run (0 = font metrics)
float glyph_advances[MAX_TEXT_LEN]; // PDF advance following each character
char text[MAX_TEXT_LEN];
int text_len;
uint8_t flags; // bit 0: bold, bit 1: italic, bit 2: monospace
uint8_t r, g, b;
TrueTypeFont* font; // embedded font, or nullptr for system font
};
@@ -77,6 +82,31 @@ struct GraphicsItem {
uint8_t r, g, b;
};
struct FilledPathEdge { float x1, y1, x2, y2; };
struct FilledPathItem {
FilledPathEdge* edges;
int edge_count;
uint8_t r, g, b;
bool even_odd;
};
enum PaintType : uint8_t { PAINT_IMAGE, PAINT_FILL, PAINT_GRAPHICS, PAINT_TEXT };
struct PaintCommand {
PaintType type;
int index;
float clip_x0, clip_y0, clip_x1, clip_y1;
};
struct ImageItem {
uint32_t* pixels; // shared decoded ARGB image owned by PdfDoc
int width, height;
float matrix[6]; // image unit square -> PDF page coordinates
bool stencil;
uint8_t stencil_r, stencil_g, stencil_b;
};
struct PdfPage {
TextItem* items;
int item_count;
@@ -84,7 +114,23 @@ struct PdfPage {
GraphicsItem* gfx_items;
int gfx_count;
int gfx_cap;
FilledPathItem* fill_items;
int fill_count;
int fill_cap;
ImageItem* image_items;
int image_count;
int image_cap;
PaintCommand* paint_commands;
int paint_count;
int paint_cap;
float width, height; // page dimensions in points (from MediaBox)
bool parsed; // content is decoded on demand for large documents
};
struct PdfImageCacheEntry {
int obj_num;
uint32_t* pixels;
int width, height;
};
struct EmbeddedFontEntry {
@@ -110,6 +156,10 @@ struct PdfDoc {
EmbeddedFontEntry* emb_fonts; // cached embedded fonts
int emb_font_count;
PdfImageCacheEntry* images; // decoded image XObjects, shared across pages
int image_count;
int image_cap;
bool valid;
};
@@ -118,10 +168,13 @@ struct FontInfo {
uint8_t flags; // bit 0: bold, bit 1: italic, bit 2: mono
uint16_t* tounicode; // glyph ID -> Unicode codepoint (256 entries), heap-allocated
TrueTypeFont* embedded_font; // loaded from PDF font stream, or nullptr
float widths[256]; // glyph widths in thousandths of a text-space unit
bool has_widths;
bool two_byte; // composite Identity-H character codes
};
struct FontMap {
FontInfo fonts[32];
FontInfo fonts[64];
int count;
};
@@ -181,12 +234,17 @@ int parse_ref_at(const uint8_t* d, int len, int p, int* obj_num);
int find_obj_content(int obj_num, int* start, int* end);
uint8_t* get_stream_data(int obj_num, int* out_len);
void build_font_map(int page_obj_num, FontMap* out);
void build_object_font_map(int obj_num, FontMap* out);
int find_inherited_page_value(int page_obj_num, const char* key);
bool load_pdf_image(int obj_num, uint32_t** pixels, int* width, int* height);
// ============================================================================
// pdf_page.cpp
// ============================================================================
void parse_page(int page_idx, int page_obj_num);
void ensure_page_parsed(int page_idx);
void trim_page_cache(int keep_page);
// ============================================================================
// render.cpp
+283 -46
View File
@@ -6,6 +6,143 @@
#include "pdfviewer.h"
static int line_outcode(float x,float y,int x0,int y0,int x1,int y1) {
int code=0;if(x<x0)code|=1;else if(x>=x1)code|=2;
if(y<y0)code|=4;else if(y>=y1)code|=8;return code;
}
static void draw_clipped_line(uint32_t* pixels,int x0,int y0,int x1,int y1,
int thick,Color color,int cx0,int cy0,int cx1,int cy1) {
float ax=x0,ay=y0,bx=x1,by=y1;
for(int guard=0;guard<12;guard++) {
int a=line_outcode(ax,ay,cx0,cy0,cx1,cy1),b=line_outcode(bx,by,cx0,cy0,cx1,cy1);
if(!(a|b)){px_line(pixels,g_win_w,g_win_h,(int)ax,(int)ay,(int)bx,(int)by,thick,color);return;}
if(a&b)return;
int out=a?a:b;float x=0,y=0;
if(out&4){y=(float)cy0;x=ax+(bx-ax)*(y-ay)/(by-ay);}
else if(out&8){y=(float)(cy1-1);x=ax+(bx-ax)*(y-ay)/(by-ay);}
else if(out&2){x=(float)(cx1-1);y=ay+(by-ay)*(x-ax)/(bx-ax);}
else {x=(float)cx0;y=ay+(by-ay)*(x-ax)/(bx-ax);}
if(out==a){ax=x;ay=y;}else{bx=x;by=y;}
}
}
static int floor_pixel(float value) {
int result = (int)value;
if ((float)result > value) result--;
return result;
}
static int ceil_pixel(float value) {
int result = (int)value;
if ((float)result < value) result++;
return result;
}
static void blend_glyph_pixel(uint32_t* dst, uint8_t alpha, const Color& color) {
if (!alpha) return;
if (alpha == 255) {
*dst = 0xFF000000u | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b;
return;
}
uint32_t old = *dst, inv = 255 - alpha;
uint32_t r = (alpha * color.r + inv * ((old >> 16) & 255) + 127) / 255;
uint32_t g = (alpha * color.g + inv * ((old >> 8) & 255) + 127) / 255;
uint32_t b = (alpha * color.b + inv * (old & 255) + 127) / 255;
*dst = 0xFF000000u | (r << 16) | (g << 8) | b;
}
static void draw_affine_text(uint32_t* pixels, const TextItem& item,
TrueTypeFont* font, const char* text,
const float* advances, int text_len,
int base_x, int base_y, int pixel_size,
int clip_x0, int clip_y0, int clip_x1, int clip_y1) {
GlyphCache* cache = font->get_cache(pixel_size);
if (!cache) return;
Color color = Color::from_rgb(item.r, item.g, item.b);
// The common untransformed case should use the cached glyph bitmap
// verbatim. Resampling it merely to reconcile PDF advances with rounded
// cache advances softens stems, especially at fractional zoom levels.
float bx0=item.basis[0]-1,bx1=item.basis[1];
float by0=item.basis[2],by1=item.basis[3]-1;
if(bx0>-0.0001f&&bx0<0.0001f&&bx1>-0.0001f&&bx1<0.0001f&&
by0>-0.0001f&&by0<0.0001f&&by1>-0.0001f&&by1<0.0001f) {
float pen=0;
for(int i=0;i<text_len;i++) {
CachedGlyph* glyph=font->get_glyph(cache,(uint8_t)text[i]);
if(!glyph)continue;
if(glyph->bitmap) {
int gx=base_x+floor_pixel(pen*g_zoom+0.5f)+glyph->xoff;
int gy=base_y+glyph->yoff;
for(int row=0;row<glyph->height;row++) {
int y=gy+row;
if(y<clip_y0||y>=clip_y1)continue;
for(int col=0;col<glyph->width;col++) {
int x=gx+col;
if(x<clip_x0||x>=clip_x1)continue;
uint8_t alpha=glyph->bitmap[row*glyph->width+col];
if(alpha)blend_glyph_pixel(&pixels[y*g_win_w+x],alpha,color);
}
}
}
pen+=advances?advances[i]:glyph->advance/g_zoom;
}
return;
}
float a = item.basis[0];
float b = -item.basis[2];
float c = -item.basis[1];
float d = item.basis[3];
float determinant = a * d - b * c;
if (determinant > -0.000001f && determinant < 0.000001f) return;
float pen = 0;
for (int i = 0; i < text_len; i++) {
CachedGlyph* glyph = font->get_glyph(cache, (uint8_t)text[i]);
if (!glyph) continue;
if (glyph->bitmap) {
float gx0 = (float)(pen + glyph->xoff);
float gy0 = (float)glyph->yoff;
float gx1 = gx0 + glyph->width;
float gy1 = gy0 + glyph->height;
float xs[4] = {a*gx0+b*gy0, a*gx1+b*gy0,
a*gx0+b*gy1, a*gx1+b*gy1};
float ys[4] = {c*gx0+d*gy0, c*gx1+d*gy0,
c*gx0+d*gy1, c*gx1+d*gy1};
float min_x=xs[0],max_x=xs[0],min_y=ys[0],max_y=ys[0];
for(int corner=1;corner<4;corner++) {
if(xs[corner]<min_x)min_x=xs[corner];
if(xs[corner]>max_x)max_x=xs[corner];
if(ys[corner]<min_y)min_y=ys[corner];
if(ys[corner]>max_y)max_y=ys[corner];
}
int x0=base_x+floor_pixel(min_x),x1=base_x+ceil_pixel(max_x);
int y0=base_y+floor_pixel(min_y),y1=base_y+ceil_pixel(max_y);
if(x0<clip_x0)x0=clip_x0;
if(x1>clip_x1)x1=clip_x1;
if(y0<clip_y0)y0=clip_y0;
if(y1>clip_y1)y1=clip_y1;
for(int y=y0;y<y1;y++) {
for(int x=x0;x<x1;x++) {
float dx=x-base_x+0.5f,dy=y-base_y+0.5f;
float source_x=(d*dx-b*dy)/determinant;
float source_y=(-c*dx+a*dy)/determinant;
int col=floor_pixel(source_x-gx0);
int row=floor_pixel(source_y-gy0);
if(col>=0&&col<glyph->width&&row>=0&&row<glyph->height) {
uint8_t alpha=glyph->bitmap[row*glyph->width+col];
if(alpha)blend_glyph_pixel(&pixels[y*g_win_w+x],alpha,color);
}
}
}
}
pen += (advances?advances[i]:glyph->advance/g_zoom)*g_zoom;
}
}
void render(uint32_t* pixels) {
// ---- Background ----
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR);
@@ -48,22 +185,137 @@ void render(uint32_t* pixels) {
pixels[row * g_win_w + col] = white;
}
// Draw graphics items (lines, filled rectangles)
for (int i = 0; i < page->gfx_count; i++) {
GraphicsItem* gi = &page->gfx_items[i];
// Replay the page in original PDF painter order. Each command also
// snapshots the active clip at the point where it was emitted.
for(int ci=0;ci<page->paint_count;ci++) {
PaintCommand* command=&page->paint_commands[ci];
int cmd_x0=page_x+(int)(command->clip_x0*g_zoom);
int cmd_x1=page_x+(int)(command->clip_x1*g_zoom+1);
int cmd_y0=page_y+(int)((page->height-command->clip_y1)*g_zoom);
int cmd_y1=page_y+(int)((page->height-command->clip_y0)*g_zoom+1);
if(cmd_x0<0)cmd_x0=0;
if(cmd_x1>g_win_w)cmd_x1=g_win_w;
if(cmd_y0<clip_y0)cmd_y0=clip_y0;
if(cmd_y1>clip_y1)cmd_y1=clip_y1;
if(cmd_x0>=cmd_x1||cmd_y0>=cmd_y1)continue;
if(command->type==PAINT_IMAGE) {
ImageItem* image = &page->image_items[command->index];
const float* m = image->matrix;
float xs[4] = {m[4], m[0]+m[4], m[2]+m[4], m[0]+m[2]+m[4]};
float ys[4] = {m[5], m[1]+m[5], m[3]+m[5], m[1]+m[3]+m[5]};
float minx=xs[0], maxx=xs[0], miny=ys[0], maxy=ys[0];
for (int c=1;c<4;c++) { if(xs[c]<minx)minx=xs[c]; if(xs[c]>maxx)maxx=xs[c]; if(ys[c]<miny)miny=ys[c]; if(ys[c]>maxy)maxy=ys[c]; }
int x0=page_x+(int)(minx*g_zoom), x1=page_x+(int)(maxx*g_zoom+1);
int y0=page_y+(int)((page->height-maxy)*g_zoom), y1=page_y+(int)((page->height-miny)*g_zoom+1);
if(x0<cmd_x0)x0=cmd_x0;
if(x1>cmd_x1)x1=cmd_x1;
if(y0<cmd_y0)y0=cmd_y0;
if(y1>cmd_y1)y1=cmd_y1;
float det=m[0]*m[3]-m[1]*m[2];
if (det > -0.000001f && det < 0.000001f) continue;
for(int sy=y0;sy<y1;sy++) for(int sx=x0;sx<x1;sx++) {
float px=(sx-page_x+0.5f)/g_zoom;
float py=page->height-(sy-page_y+0.5f)/g_zoom;
float dx=px-m[4],dy=py-m[5];
float u=(m[3]*dx-m[2]*dy)/det;
float v=(-m[1]*dx+m[0]*dy)/det;
if(u>=0 && u<1 && v>=0 && v<1) {
int ix=(int)(u*image->width);
int iy=(int)((1.0f-v)*image->height);
if(iy>=image->height)iy=image->height-1;
uint32_t src=image->pixels[iy*image->width+ix];
if(image->stencil)src=(src&0xFF000000u)|((uint32_t)image->stencil_r<<16)|
((uint32_t)image->stencil_g<<8)|image->stencil_b;
uint32_t alpha=src>>24;
if(alpha==255) pixels[sy*g_win_w+sx]=src;
else if(alpha) {
uint32_t dst=pixels[sy*g_win_w+sx], inv=255-alpha;
uint32_t r=(((src>>16)&255)*alpha+((dst>>16)&255)*inv+127)/255;
uint32_t g=(((src>>8)&255)*alpha+((dst>>8)&255)*inv+127)/255;
uint32_t b=((src&255)*alpha+(dst&255)*inv+127)/255;
pixels[sy*g_win_w+sx]=0xFF000000u|(r<<16)|(g<<8)|b;
}
}
}
} else if(command->type==PAINT_FILL) {
FilledPathItem* path=&page->fill_items[command->index];
float* hits=(float*)montauk::malloc(path->edge_count*sizeof(float));
int* winds=(int*)montauk::malloc(path->edge_count*sizeof(int));
if(!hits||!winds){if(hits)montauk::mfree(hits);if(winds)montauk::mfree(winds);continue;}
float min_y=path->edges[0].y1,max_y=min_y;
for(int e=0;e<path->edge_count;e++) {
float a=path->edges[e].y1,b=path->edges[e].y2;
if(a<min_y)min_y=a;
if(b<min_y)min_y=b;
if(a>max_y)max_y=a;
if(b>max_y)max_y=b;
}
int row0=page_y+(int)((page->height-max_y)*g_zoom);
int row1=page_y+(int)((page->height-min_y)*g_zoom+1);
if(row0<cmd_y0)row0=cmd_y0;
if(row1>cmd_y1)row1=cmd_y1;
Color color=Color::from_rgb(path->r,path->g,path->b);
for(int row=row0;row<row1;row++) {
float scan=page->height-(row-page_y+0.5f)/g_zoom;
int count=0;
for(int e=0;e<path->edge_count;e++) {
FilledPathEdge* edge=&path->edges[e];
if((edge->y1<=scan&&edge->y2>scan)||(edge->y2<=scan&&edge->y1>scan)) {
float t=(scan-edge->y1)/(edge->y2-edge->y1);
hits[count]=page_x+(edge->x1+t*(edge->x2-edge->x1))*g_zoom;
winds[count]=edge->y2>edge->y1?1:-1;
count++;
}
}
for(int i=1;i<count;i++) {
float hx=hits[i];int hw=winds[i],j=i-1;
while(j>=0&&hits[j]>hx){hits[j+1]=hits[j];winds[j+1]=winds[j];j--;}
hits[j+1]=hx;winds[j+1]=hw;
}
if(path->even_odd) {
for(int i=0;i+1<count;i+=2) {
int x0=(int)(hits[i]+0.5f),x1=(int)(hits[i+1]+0.5f);
if(x0<cmd_x0)x0=cmd_x0;
if(x1>cmd_x1)x1=cmd_x1;
if(x1>x0)px_fill(pixels,g_win_w,g_win_h,x0,row,x1-x0,1,color);
}
} else {
int winding=0,start_x=0;
for(int i=0;i<count;i++) {
int before=winding;winding+=winds[i];
if(before==0&&winding!=0)start_x=(int)(hits[i]+0.5f);
else if(before!=0&&winding==0) {
int end_x=(int)(hits[i]+0.5f);
if(start_x<cmd_x0)start_x=cmd_x0;
if(end_x>cmd_x1)end_x=cmd_x1;
if(end_x>start_x)px_fill(pixels,g_win_w,g_win_h,start_x,row,end_x-start_x,1,color);
}
}
}
}
montauk::mfree(hits);montauk::mfree(winds);
} else if(command->type==PAINT_GRAPHICS) {
GraphicsItem* gi = &page->gfx_items[command->index];
Color gfx_color = Color::from_rgb(gi->r, gi->g, gi->b);
if (gi->type == GFX_RECT_FILL) {
// gi->x1,y1 = rect origin (PDF coords), gi->x2,y2 = width,height
int rx = page_x + (int)(gi->x1 * g_zoom);
int ry = page_y + (int)((page->height - gi->y1 - gi->y2) * g_zoom);
int rw = (int)(gi->x2 * g_zoom);
int rh = (int)(gi->y2 * g_zoom);
if (rw < 0) { rx += rw; rw = -rw; }
if (rh < 0) { ry += rh; rh = -rh; }
float pdf_x0=gi->x1,pdf_x1=gi->x1+gi->x2;
float pdf_y0=gi->y1,pdf_y1=gi->y1+gi->y2;
if(pdf_x1<pdf_x0){float t=pdf_x0;pdf_x0=pdf_x1;pdf_x1=t;}
if(pdf_y1<pdf_y0){float t=pdf_y0;pdf_y0=pdf_y1;pdf_y1=t;}
int rx=page_x+floor_pixel(pdf_x0*g_zoom+0.5f);
int ry=page_y+floor_pixel((page->height-pdf_y1)*g_zoom+0.5f);
int right=page_x+floor_pixel(pdf_x1*g_zoom+0.5f);
int bottom=page_y+floor_pixel((page->height-pdf_y0)*g_zoom+0.5f);
int rw=right-rx,rh=bottom-ry;
if(gi->x2!=0&&rw<1)rw=1;
if(gi->y2!=0&&rh<1)rh=1;
// Clip to content area
if (ry + rh > clip_y0 && ry < clip_y1)
px_fill(pixels, g_win_w, g_win_h, rx, ry, rw, rh, gfx_color);
int x0=rx<cmd_x0?cmd_x0:rx,y0=ry<cmd_y0?cmd_y0:ry;
int x1=rx+rw>cmd_x1?cmd_x1:rx+rw,y1=ry+rh>cmd_y1?cmd_y1:ry+rh;
if(x1>x0&&y1>y0)px_fill(pixels,g_win_w,g_win_h,x0,y0,x1-x0,y1-y0,gfx_color);
} else {
// GFX_LINE or GFX_RECT_STROKE
int lx0 = page_x + (int)(gi->x1 * g_zoom);
@@ -75,25 +327,16 @@ void render(uint32_t* pixels) {
// Basic clip check
int min_y = ly0 < ly1 ? ly0 : ly1;
int max_y = ly0 > ly1 ? ly0 : ly1;
if (max_y + lw >= clip_y0 && min_y - lw < clip_y1)
px_line(pixels, g_win_w, g_win_h, lx0, ly0, lx1, ly1, lw, gfx_color);
if (max_y + lw >= cmd_y0 && min_y - lw < cmd_y1)
draw_clipped_line(pixels,lx0,ly0,lx1,ly1,lw,gfx_color,
cmd_x0,cmd_y0,cmd_x1,cmd_y1);
}
}
} else if(command->type==PAINT_TEXT) {
TextItem* item = &page->items[command->index];
// Draw text items
for (int i = 0; i < page->item_count; i++) {
TextItem* item = &page->items[i];
// Convert PDF coords to screen coords
// PDF: origin bottom-left, y up
// Screen: origin top-left, y down
int sx = page_x + (int)(item->x * g_zoom);
int sy = page_y + (int)((page->height - item->y) * g_zoom);
// Skip if outside visible area
if (sy < clip_y0 - 30 || sy > clip_y1) continue;
// Choose font: prefer embedded, fall back to system fonts
TrueTypeFont* font = item->font;
if (!font) {
font = g_font;
@@ -102,41 +345,35 @@ void render(uint32_t* pixels) {
}
if (!font) continue;
// Scale font size
int px_size = (int)(item->font_size * g_zoom + 0.5f);
if (px_size < 4) px_size = 4;
if (px_size > 120) px_size = 120;
// draw_to_buffer treats y as top of text box, but PDF
// specifies the baseline. Subtract ascent so the rendered
// baseline lands at the correct position.
GlyphCache* gc = font->get_cache(px_size);
int baseline_adj = gc ? gc->ascent : (int)(px_size * 0.8f);
int ty = sy - baseline_adj;
if (item->font) {
// Embedded font: pass raw character codes directly
// (subset fonts use codes 0-N that map through the font's cmap)
font->draw_to_buffer(pixels, g_win_w, g_win_h,
sx, ty, item->text, TEXT_COLOR, px_size);
draw_affine_text(pixels, *item, font, item->text,
item->glyph_advances,item->text_len,
sx, sy, px_size, cmd_x0, cmd_y0, cmd_x1, cmd_y1);
} else {
// System font: filter out non-printable characters
char render_text[MAX_TEXT_LEN];
float render_advances[MAX_TEXT_LEN];
int ri = 0;
for (int j = 0; item->text[j] && ri < MAX_TEXT_LEN - 1; j++) {
char c = item->text[j];
if (c >= 32 && c < 127) {
render_text[ri++] = c;
for (int j = 0; j < item->text_len && ri < MAX_TEXT_LEN - 1; j++) {
uint8_t c = (uint8_t)item->text[j];
if (c >= 32 && c != 127) {
render_text[ri++] = (char)c;
render_advances[ri-1]=item->glyph_advances[j];
} else if (c == '\t') {
render_text[ri++] = ' ';
render_advances[ri-1]=item->glyph_advances[j];
}
}
render_text[ri] = '\0';
if (ri > 0) {
font->draw_to_buffer(pixels, g_win_w, g_win_h,
sx, ty, render_text, TEXT_COLOR, px_size);
}
if (ri > 0)
draw_affine_text(pixels, *item, font, render_text,
render_advances,ri,
sx, sy, px_size, cmd_x0, cmd_y0, cmd_x1, cmd_y1);
}
}
}