diff --git a/programs/GNUmakefile b/programs/GNUmakefile index a8b40b1..420134e 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, games are handled separately. @@ -84,9 +84,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Home directory placeholder. HOMEKEEP := $(BINDIR)/home/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop icons fonts bearssl libc tls libjpeg +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop icons fonts bearssl libc tls libjpeg -all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -139,6 +139,10 @@ fontpreview: spreadsheet: libc $(MAKE) -C src/spreadsheet +# Build PDF viewer standalone GUI client (depends on libc). +pdfviewer: libc + $(MAKE) -C src/pdfviewer + # Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper). desktop: libc libjpeg $(MAKE) -C src/desktop @@ -200,3 +204,4 @@ clean: $(MAKE) -C src/imageviewer clean $(MAKE) -C src/fontpreview clean $(MAKE) -C src/spreadsheet clean + $(MAKE) -C src/pdfviewer clean diff --git a/programs/include/gui/stb_truetype.h b/programs/include/gui/stb_truetype.h index 90a5c2e..d3d1faa 100644 --- a/programs/include/gui/stb_truetype.h +++ b/programs/include/gui/stb_truetype.h @@ -1484,6 +1484,11 @@ static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, in // all the encodingIDs are unicode, so we don't bother to check it info->index_map = cmap + ttULONG(data+encoding_record+4); break; + case STBTT_PLATFORM_ID_MAC: + // Accept Mac Roman as fallback (common in PDF subset fonts) + if (info->index_map == 0) + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; } } if (info->index_map == 0) diff --git a/programs/include/gui/truetype.hpp b/programs/include/gui/truetype.hpp index 0e20787..f438438 100644 --- a/programs/include/gui/truetype.hpp +++ b/programs/include/gui/truetype.hpp @@ -63,9 +63,11 @@ struct TrueTypeFont { GlyphCache caches[4]; int cache_count; bool valid; + bool em_scaling; // true for PDF embedded fonts: scale by em square, not ascent-descent bool init(const char* vfs_path) { valid = false; + em_scaling = false; data = nullptr; cache_count = 0; @@ -97,19 +99,20 @@ struct TrueTypeFont { return true; } - GlyphCache* get_cache(int pixel_size) { - // Search existing caches - for (int i = 0; i < cache_count; i++) { - if (caches[i].pixel_size == pixel_size) - return &caches[i]; + void init_cache(GlyphCache* gc, int pixel_size) { + // Free any existing glyph bitmaps + for (int i = 0; i < 256; i++) { + if (gc->glyphs[i].bitmap) { + montauk::mfree(gc->glyphs[i].bitmap); + } + gc->glyphs[i].bitmap = nullptr; + gc->glyphs[i].loaded = false; } - // Create new cache - if (cache_count >= 4) return &caches[0]; // fallback to first - - GlyphCache* gc = &caches[cache_count++]; gc->pixel_size = pixel_size; - gc->scale = stbtt_ScaleForPixelHeight(&info, (float)pixel_size); + gc->scale = em_scaling + ? stbtt_ScaleForMappingEmToPixels(&info, (float)pixel_size) + : stbtt_ScaleForPixelHeight(&info, (float)pixel_size); int asc, desc, lg; stbtt_GetFontVMetrics(&info, &asc, &desc, &lg); @@ -117,13 +120,29 @@ struct TrueTypeFont { gc->descent = (int)(desc * gc->scale); gc->line_gap = (int)(lg * gc->scale); gc->line_height = gc->ascent - gc->descent + gc->line_gap; + } - for (int i = 0; i < 128; i++) { - gc->glyphs[i].bitmap = nullptr; - gc->glyphs[i].loaded = false; + GlyphCache* get_cache(int pixel_size) { + // Search existing caches + for (int i = 0; i < cache_count; i++) { + if (caches[i].pixel_size == pixel_size) + return &caches[i]; } - return gc; + // Use a free slot if available + if (cache_count < 4) { + GlyphCache* gc = &caches[cache_count++]; + init_cache(gc, pixel_size); + return gc; + } + + // Evict oldest cache (slot 0) and shift others down + GlyphCache evicted = caches[0]; + for (int i = 0; i < 3; i++) + caches[i] = caches[i + 1]; + caches[3] = evicted; + init_cache(&caches[3], pixel_size); + return &caches[3]; } CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) { diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/app_filemanager.cpp index 9e9cd6a..507577d 100644 --- a/programs/src/desktop/app_filemanager.cpp +++ b/programs/src/desktop/app_filemanager.cpp @@ -65,6 +65,14 @@ static bool is_font_file(const char* name) { return str_ends_with(name, ".ttf"); } +static bool is_pdf_file(const char* name) { + return str_ends_with(name, ".pdf"); +} + +static bool is_spreadsheet_file(const char* name) { + return str_ends_with(name, ".mss"); +} + static int detect_file_type(const char* name, bool is_dir) { if (is_dir) return 1; if (str_ends_with(name, ".elf")) return 2; @@ -567,6 +575,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { montauk::spawn("0:/os/imageviewer.elf", fullpath); } else if (is_font_file(fm->entry_names[clicked_idx])) { montauk::spawn("0:/os/fontpreview.elf", fullpath); + } else if (is_pdf_file(fm->entry_names[clicked_idx])) { + montauk::spawn("0:/os/pdfviewer.elf", fullpath); + } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { + montauk::spawn("0:/os/spreadsheet.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } @@ -608,6 +620,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { montauk::spawn("0:/os/imageviewer.elf", fullpath); } else if (is_font_file(fm->entry_names[clicked_idx])) { montauk::spawn("0:/os/fontpreview.elf", fullpath); + } else if (is_pdf_file(fm->entry_names[clicked_idx])) { + montauk::spawn("0:/os/pdfviewer.elf", fullpath); + } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { + montauk::spawn("0:/os/spreadsheet.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } diff --git a/programs/src/pdfviewer/Makefile b/programs/src/pdfviewer/Makefile new file mode 100644 index 0000000..e1e4a3b --- /dev/null +++ b/programs/src/pdfviewer/Makefile @@ -0,0 +1,88 @@ +# Makefile for pdfviewer (standalone Window Server app) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp helpers.cpp pdf_parser.cpp pdf_page.cpp render.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/os/pdfviewer.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +HEADERS := pdfviewer.h + +$(OBJDIR)/%.o: %.cpp $(HEADERS) Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/pdfviewer/helpers.cpp b/programs/src/pdfviewer/helpers.cpp new file mode 100644 index 0000000..d287a53 --- /dev/null +++ b/programs/src/pdfviewer/helpers.cpp @@ -0,0 +1,97 @@ +/* + * helpers.cpp + * Pixel drawing and string helpers + * Copyright (c) 2026 Daniel Hammer + */ + +#include "pdfviewer.h" + +void px_fill(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, Color c) { + uint32_t v = c.to_pixel(); + int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; + int x1 = x + w > bw ? bw : x + w; + int y1 = y + h > bh ? bh : y + h; + for (int row = y0; row < y1; row++) + for (int col = x0; col < x1; col++) + px[row * bw + col] = v; +} + +void px_hline(uint32_t* px, int bw, int bh, + int x, int y, int w, Color c) { + if (y < 0 || y >= bh) return; + uint32_t v = c.to_pixel(); + int x0 = x < 0 ? 0 : x; + int x1 = x + w > bw ? bw : x + w; + for (int col = x0; col < x1; col++) + px[y * bw + col] = v; +} + +void px_vline(uint32_t* px, int bw, int bh, + int x, int y, int h, Color c) { + if (x < 0 || x >= bw) return; + uint32_t v = c.to_pixel(); + int y0 = y < 0 ? 0 : y; + int y1 = y + h > bh ? bh : y + h; + for (int row = y0; row < y1; row++) + px[row * bw + x] = v; +} + +void px_rect(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, Color c) { + px_hline(px, bw, bh, x, y, w, c); + px_hline(px, bw, bh, x, y + h - 1, w, c); + px_vline(px, bw, bh, x, y, h, c); + px_vline(px, bw, bh, x + w - 1, y, h, c); +} + +void px_fill_rounded(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, int r, Color c) { + uint32_t v = c.to_pixel(); + for (int row = 0; row < h; row++) { + int py = y + row; + if (py < 0 || py >= bh) continue; + int inset = 0; + if (row < r) { + int dy = r - 1 - row; + if (r == 3) { + if (dy >= 2) inset = 2; + else if (dy >= 1) inset = 1; + } else { + for (int i = r; i > 0; i--) { + int dx = r - i; + if (dx * dx + dy * dy < r * r) { inset = i; break; } + } + } + } else if (row >= h - r) { + int dy = row - (h - r); + if (r == 3) { + if (dy >= 2) inset = 2; + else if (dy >= 1) inset = 1; + } else { + for (int i = r; i > 0; i--) { + int dx = r - i; + if (dx * dx + dy * dy < r * r) { inset = i; break; } + } + } + } + int x0 = x + inset; + int x1 = x + w - inset; + if (x0 < 0) x0 = 0; + if (x1 > bw) x1 = bw; + for (int col = x0; col < x1; col++) + px[py * bw + col] = v; + } +} + +int str_len(const char* s) { + int n = 0; + while (s[n]) n++; + return n; +} + +void str_cpy(char* dst, const char* src, int max) { + int i = 0; + while (src[i] && i < max - 1) { dst[i] = src[i]; i++; } + dst[i] = '\0'; +} diff --git a/programs/src/pdfviewer/main.cpp b/programs/src/pdfviewer/main.cpp new file mode 100644 index 0000000..dd90cfd --- /dev/null +++ b/programs/src/pdfviewer/main.cpp @@ -0,0 +1,329 @@ +/* + * main.cpp + * PDF Viewer app -- global state, entry point, event loop + * Copyright (c) 2026 Daniel Hammer + */ + +#include "pdfviewer.h" + +// ============================================================================ +// Global state definitions +// ============================================================================ + +int g_win_w = INIT_W; +int g_win_h = INIT_H; + +PdfDoc g_doc = {}; + +int g_current_page = 0; +int g_scroll_y = 0; +float g_zoom = 1.0f; + +char g_filepath[256] = {}; + +bool g_pathbar_open = false; +char g_pathbar_text[256] = {}; +int g_pathbar_len = 0; +int g_pathbar_cursor = 0; + +TrueTypeFont* g_font = nullptr; +TrueTypeFont* g_font_bold = nullptr; +TrueTypeFont* g_font_mono = nullptr; + +char g_status_msg[128] = {}; + +// ============================================================================ +// Toolbar hit-testing constants +// ============================================================================ + +// Toolbar buttons: Open | < > | - + +static constexpr int TB_OPEN_X0 = 4, TB_OPEN_X1 = 40; +// separator at 48 +static constexpr int TB_PREV_X0 = 56, TB_PREV_X1 = 80; +static constexpr int TB_NEXT_X0 = 84, TB_NEXT_X1 = 108; +// separator at 116 +static constexpr int TB_ZOUT_X0 = 124, TB_ZOUT_X1 = 148; +static constexpr int TB_ZIN_X0 = 152, TB_ZIN_X1 = 176; + +// ============================================================================ +// Helpers +// ============================================================================ + +static int max_scroll() { + if (!g_doc.valid || g_current_page >= g_doc.page_count) return 0; + PdfPage* page = &g_doc.pages[g_current_page]; + int page_h = (int)(page->height * g_zoom) + PAGE_MARGIN * 2; + int pathbar_h = g_pathbar_open ? PATHBAR_H : 0; + int content_h = g_win_h - TOOLBAR_H - pathbar_h - STATUS_BAR_H; + int ms = page_h - content_h; + return ms > 0 ? ms : 0; +} + +static void clamp_scroll() { + int ms = max_scroll(); + if (g_scroll_y < 0) g_scroll_y = 0; + if (g_scroll_y > ms) g_scroll_y = ms; +} + +static void go_to_page(int page) { + if (page < 0) page = 0; + if (page >= g_doc.page_count) page = g_doc.page_count - 1; + g_current_page = page; + g_scroll_y = 0; +} + +static void zoom_in() { + if (g_zoom < 4.0f) { + g_zoom += 0.25f; + clamp_scroll(); + } +} + +static void zoom_out() { + if (g_zoom > 0.25f) { + g_zoom -= 0.25f; + clamp_scroll(); + } +} + +static void open_file(const char* path) { + str_cpy(g_filepath, path, 256); + g_current_page = 0; + g_scroll_y = 0; + + if (load_pdf(path)) { + // Success + } else { + g_filepath[0] = '\0'; + } +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + // Load fonts + auto load_font = [](const char* path) -> TrueTypeFont* { + TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); + if (!f) return nullptr; + montauk::memset(f, 0, sizeof(TrueTypeFont)); + if (!f->init(path)) { montauk::mfree(f); return nullptr; } + return f; + }; + g_font = load_font("0:/fonts/Roboto-Medium.ttf"); + g_font_bold = load_font("0:/fonts/Roboto-Bold.ttf"); + g_font_mono = load_font("0:/fonts/JetBrainsMono-Regular.ttf"); + + // Check for file argument + char args[512] = {}; + int arglen = montauk::getargs(args, sizeof(args)); + if (arglen > 0 && args[0]) { + open_file(args); + } + + // Build window title + char title[64] = "PDF Viewer"; + if (g_filepath[0]) { + const char* fname = g_filepath; + for (int i = 0; g_filepath[i]; i++) + if (g_filepath[i] == '/') fname = g_filepath + i + 1; + snprintf(title, 64, "%s - PDF Viewer", fname); + } + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0) + montauk::exit(1); + + int win_id = wres.id; + uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + + render(pixels); + montauk::win_present(win_id); + + while (true) { + Montauk::WinEvent ev; + int r = montauk::win_poll(win_id, &ev); + + if (r < 0) break; + + if (r == 0) { + montauk::sleep_ms(16); + continue; + } + + // Close + if (ev.type == 3) break; + + // Resize + if (ev.type == 2) { + g_win_w = ev.resize.w; + g_win_h = ev.resize.h; + pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h); + clamp_scroll(); + render(pixels); + montauk::win_present(win_id); + continue; + } + + bool redraw = false; + + // Keyboard + if (ev.type == 0 && ev.key.pressed) { + auto& key = ev.key; + + // Path bar input + if (g_pathbar_open) { + if (key.ascii == '\n' || key.ascii == '\r') { + if (g_pathbar_text[0]) { + open_file(g_pathbar_text); + } + g_pathbar_open = false; + } else if (key.scancode == 0x01) { + g_pathbar_open = false; + } else if (key.ascii == '\b' || key.scancode == 0x0E) { + if (g_pathbar_cursor > 0) { + for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) + g_pathbar_text[i] = g_pathbar_text[i + 1]; + g_pathbar_len--; + g_pathbar_cursor--; + g_pathbar_text[g_pathbar_len] = '\0'; + } + } else if (key.scancode == 0x4B) { + if (g_pathbar_cursor > 0) g_pathbar_cursor--; + } else if (key.scancode == 0x4D) { + if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; + } else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) { + for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) + g_pathbar_text[i] = g_pathbar_text[i - 1]; + g_pathbar_text[g_pathbar_cursor] = key.ascii; + g_pathbar_cursor++; + g_pathbar_len++; + g_pathbar_text[g_pathbar_len] = '\0'; + } + redraw = true; + goto done_keys; + } + + // Escape: close + if (key.scancode == 0x01) { + break; + } + // Ctrl+O: open file + else if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O' || key.ascii == 15)) { + g_pathbar_open = true; + g_pathbar_text[0] = '\0'; + g_pathbar_len = 0; + g_pathbar_cursor = 0; + redraw = true; + } + // Page Down / Right arrow: next page + else if (key.scancode == 0x51 || key.scancode == 0x4D) { + if (g_doc.valid) { + if (key.scancode == 0x4D && g_scroll_y < max_scroll()) { + // Right arrow scrolls down within page + } else { + go_to_page(g_current_page + 1); + } + redraw = true; + } + } + // Page Up / Left arrow: previous page + else if (key.scancode == 0x49 || key.scancode == 0x4B) { + if (g_doc.valid) { + if (key.scancode == 0x4B && g_scroll_y > 0) { + // Left arrow scrolls up within page + } else { + go_to_page(g_current_page - 1); + } + redraw = true; + } + } + // Up arrow: scroll up + else if (key.scancode == 0x48) { + g_scroll_y -= SCROLL_STEP; + clamp_scroll(); + redraw = true; + } + // Down arrow: scroll down + else if (key.scancode == 0x50) { + g_scroll_y += SCROLL_STEP; + clamp_scroll(); + redraw = true; + } + // Home: first page + else if (key.scancode == 0x47) { + if (g_doc.valid) { go_to_page(0); redraw = true; } + } + // End: last page + else if (key.scancode == 0x4F) { + if (g_doc.valid) { go_to_page(g_doc.page_count - 1); redraw = true; } + } + // Ctrl++ or Ctrl+=: zoom in + else if (key.ctrl && (key.ascii == '+' || key.ascii == '=')) { + zoom_in(); + redraw = true; + } + // Ctrl+-: zoom out + else if (key.ctrl && (key.ascii == '-' || key.ascii == '_')) { + zoom_out(); + redraw = true; + } + // Ctrl+0: reset zoom + else if (key.ctrl && key.ascii == '0') { + g_zoom = 1.0f; + clamp_scroll(); + redraw = true; + } + } + done_keys: + + // Mouse + if (ev.type == 1) { + int mx = ev.mouse.x; + int my = ev.mouse.y; + uint8_t btns = ev.mouse.buttons; + uint8_t prev = ev.mouse.prev_buttons; + bool clicked = (btns & 1) && !(prev & 1); + + if (clicked && my < TOOLBAR_H && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_SIZE) { + if (mx >= TB_OPEN_X0 && mx < TB_OPEN_X1) { + g_pathbar_open = true; + g_pathbar_text[0] = '\0'; + g_pathbar_len = 0; + g_pathbar_cursor = 0; + redraw = true; + } else if (mx >= TB_PREV_X0 && mx < TB_PREV_X1 && g_doc.valid) { + go_to_page(g_current_page - 1); + redraw = true; + } else if (mx >= TB_NEXT_X0 && mx < TB_NEXT_X1 && g_doc.valid) { + go_to_page(g_current_page + 1); + redraw = true; + } else if (mx >= TB_ZOUT_X0 && mx < TB_ZOUT_X1) { + zoom_out(); + redraw = true; + } else if (mx >= TB_ZIN_X0 && mx < TB_ZIN_X1) { + zoom_in(); + redraw = true; + } + } + + // Mouse scroll + if (ev.mouse.scroll != 0) { + g_scroll_y -= ev.mouse.scroll * SCROLL_STEP; + clamp_scroll(); + redraw = true; + } + } + + if (redraw) { + render(pixels); + montauk::win_present(win_id); + } + } + + free_pdf(); + montauk::win_destroy(win_id); + montauk::exit(0); +} diff --git a/programs/src/pdfviewer/pdf_page.cpp b/programs/src/pdfviewer/pdf_page.cpp new file mode 100644 index 0000000..edfe9d3 --- /dev/null +++ b/programs/src/pdfviewer/pdf_page.cpp @@ -0,0 +1,629 @@ +/* + * pdf_page.cpp + * PDF content stream parsing and text extraction + * Copyright (c) 2026 Daniel Hammer + */ + +#include "pdfviewer.h" + +// ============================================================================ +// Content Stream Tokenizer +// ============================================================================ + +enum TokType { + TOK_EOF, TOK_INT, TOK_REAL, TOK_STRING, TOK_HEX_STRING, + TOK_NAME, TOK_ARRAY_START, TOK_ARRAY_END, TOK_OPERATOR +}; + +struct Token { + TokType type; + float num; + char str[MAX_TEXT_LEN]; + int str_len; +}; + +static int next_token(const uint8_t* d, int len, int p, Token* tok) { + // Skip whitespace + while (p < len && (d[p] == ' ' || d[p] == '\t' || d[p] == '\n' || d[p] == '\r')) + p++; + + // Skip comments + while (p < len && d[p] == '%') { + while (p < len && d[p] != '\n' && d[p] != '\r') p++; + while (p < len && (d[p] == ' ' || d[p] == '\t' || d[p] == '\n' || d[p] == '\r')) + p++; + } + + if (p >= len) { tok->type = TOK_EOF; return p; } + + // Literal string + if (d[p] == '(') { + p++; + int depth = 1; + tok->type = TOK_STRING; + tok->str_len = 0; + while (p < len && depth > 0) { + if (d[p] == '\\' && p + 1 < len) { + p++; + char c; + switch (d[p]) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case '(': c = '('; break; + case ')': c = ')'; break; + case '\\': c = '\\'; break; + default: + // Octal escape + if (d[p] >= '0' && d[p] <= '7') { + int val = d[p] - '0'; + if (p + 1 < len && d[p + 1] >= '0' && d[p + 1] <= '7') { + p++; val = val * 8 + (d[p] - '0'); + if (p + 1 < len && d[p + 1] >= '0' && d[p + 1] <= '7') { + p++; val = val * 8 + (d[p] - '0'); + } + } + c = (char)val; + } else { + c = (char)d[p]; + } + break; + } + if (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = c; + p++; + } else if (d[p] == '(') { + depth++; + if (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = '('; + p++; + } else if (d[p] == ')') { + depth--; + if (depth > 0 && tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = ')'; + p++; + } else { + if (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = (char)d[p]; + p++; + } + } + tok->str[tok->str_len] = '\0'; + return p; + } + + // Hex string + if (d[p] == '<' && (p + 1 >= len || d[p + 1] != '<')) { + p++; + tok->type = TOK_HEX_STRING; + tok->str_len = 0; + int nibble = -1; + while (p < len && d[p] != '>') { + int val = -1; + if (d[p] >= '0' && d[p] <= '9') val = d[p] - '0'; + else if (d[p] >= 'a' && d[p] <= 'f') val = d[p] - 'a' + 10; + else if (d[p] >= 'A' && d[p] <= 'F') val = d[p] - 'A' + 10; + p++; + if (val < 0) continue; + if (nibble < 0) { + nibble = val; + } else { + if (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = (char)((nibble << 4) | val); + nibble = -1; + } + } + if (nibble >= 0 && tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = (char)(nibble << 4); + tok->str[tok->str_len] = '\0'; + if (p < len) p++; // skip '>' + return p; + } + + // Array start/end + if (d[p] == '[') { tok->type = TOK_ARRAY_START; return p + 1; } + if (d[p] == ']') { tok->type = TOK_ARRAY_END; return p + 1; } + + // Name + if (d[p] == '/') { + p++; + tok->type = TOK_NAME; + tok->str_len = 0; + 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 (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = (char)d[p]; + p++; + } + tok->str[tok->str_len] = '\0'; + return p; + } + + // Number or operator + if ((d[p] >= '0' && d[p] <= '9') || d[p] == '-' || d[p] == '+' || d[p] == '.') { + // Try to parse as number + float val; + int np = parse_real_at(d, len, p, &val); + if (np > p) { + tok->type = TOK_REAL; + tok->num = val; + return np; + } + } + + // Operator (keyword) + tok->type = TOK_OPERATOR; + tok->str_len = 0; + 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 (tok->str_len < MAX_TEXT_LEN - 1) + tok->str[tok->str_len++] = (char)d[p]; + p++; + } + tok->str[tok->str_len] = '\0'; + return p; +} + +// ============================================================================ +// Text State and Item Output +// ============================================================================ + +struct TextState { + float tm[6]; // text matrix [a b c d e f] + float lm[6]; // text line matrix + float tl; // text leading (TL) + float font_size; + char font_name[32]; + uint8_t font_flags; + uint16_t* tounicode; // current font's glyph->Unicode table (may be nullptr) + TrueTypeFont* embedded_font; // embedded font from PDF, or nullptr +}; + +// Apply ToUnicode mapping to raw glyph bytes, producing a UTF-8-ish output. +// For BMP codepoints (which is all we handle), just output as Latin-1 where possible. +static int apply_tounicode(const char* raw, int raw_len, char* out, int out_max, + const uint16_t* tounicode) { + int oi = 0; + for (int i = 0; i < raw_len && oi < out_max - 1; i++) { + uint8_t glyph = (uint8_t)raw[i]; + uint16_t cp = tounicode ? tounicode[glyph] : (uint16_t)glyph; + if (cp == 0) cp = glyph; // fallback to raw byte + if (cp < 128) { + out[oi++] = (char)cp; + } else if (cp < 0x800) { + // 2-byte UTF-8 - but our font renderer likely only handles Latin-1 + // For now, output as Latin-1 if <= 255, otherwise skip + if (cp <= 255) { + out[oi++] = (char)cp; + } else { + out[oi++] = '?'; + } + } else { + out[oi++] = '?'; + } + } + out[oi] = '\0'; + return oi; +} + +static void add_text_item(PdfPage* page, float x, float y, float size, + const char* text, int text_len, uint8_t flags, + const uint16_t* tounicode, TrueTypeFont* embedded_font) { + if (text_len <= 0) return; + + // Apply ToUnicode mapping if available (only when not using embedded font, + // since embedded subset fonts need the raw character codes for cmap lookup) + char mapped[MAX_TEXT_LEN]; + if (tounicode && !embedded_font) { + text_len = apply_tounicode(text, text_len, mapped, MAX_TEXT_LEN, tounicode); + text = mapped; + } + if (text_len <= 0) return; + + // Grow items array if needed + if (page->item_count >= page->item_cap) { + int new_cap = page->item_cap ? page->item_cap * 2 : 64; + TextItem* new_items = (TextItem*)montauk::malloc(new_cap * sizeof(TextItem)); + if (!new_items) return; + if (page->items) { + montauk::memcpy(new_items, page->items, page->item_count * sizeof(TextItem)); + montauk::mfree(page->items); + } + page->items = new_items; + page->item_cap = new_cap; + } + + TextItem* item = &page->items[page->item_count++]; + item->x = x; + item->y = y; + item->font_size = size; + item->flags = flags; + item->font = embedded_font; + + int copy = text_len < MAX_TEXT_LEN - 1 ? text_len : MAX_TEXT_LEN - 1; + montauk::memcpy(item->text, text, copy); + item->text[copy] = '\0'; +} + +static void matrix_multiply(float* result, const float* a, const float* b) { + // Multiply two 3x3 matrices represented as [a b c d e f] + // where the matrix is: [a b 0] + // [c d 0] + // [e f 1] + result[0] = a[0] * b[0] + a[1] * b[2]; + result[1] = a[0] * b[1] + a[1] * b[3]; + result[2] = a[2] * b[0] + a[3] * b[2]; + result[3] = a[2] * b[1] + a[3] * b[3]; + result[4] = a[4] * b[0] + a[5] * b[2] + b[4]; + result[5] = a[4] * b[1] + a[5] * b[3] + b[5]; +} + +// ============================================================================ +// Operand Stack +// ============================================================================ + +struct Operand { + float num; + char str[MAX_TEXT_LEN]; + int str_len; + bool is_name; + bool is_string; +}; + +// ============================================================================ +// Content Stream Parser +// ============================================================================ + +static FontInfo* lookup_font(FontMap* fonts, const char* name) { + for (int i = 0; i < fonts->count; i++) { + if (str_len(fonts->fonts[i].name) == str_len(name)) { + bool match = true; + for (int j = 0; name[j]; j++) { + if (fonts->fonts[i].name[j] != name[j]) { match = false; break; } + } + if (match) return &fonts->fonts[i]; + } + } + return nullptr; +} + +void parse_page(int page_idx, int page_obj_num) { + PdfPage* page = &g_doc.pages[page_idx]; + + // Build font map for this page + FontMap* fonts = (FontMap*)montauk::malloc(sizeof(FontMap)); + if (!fonts) return; + build_font_map(page_obj_num, fonts); + + // Get content stream(s) + int start, end; + if (find_obj_content(page_obj_num, &start, &end) < 0) { + for (int fi = 0; fi < fonts->count; fi++) + if (fonts->fonts[fi].tounicode) montauk::mfree(fonts->fonts[fi].tounicode); + montauk::mfree(fonts); + return; + } + + const uint8_t* d = g_doc.data; + int len = g_doc.data_len; + + int contents_pos = dict_lookup(d, len, start, "Contents"); + if (contents_pos < 0) { + for (int fi = 0; fi < fonts->count; fi++) + if (fonts->fonts[fi].tounicode) montauk::mfree(fonts->fonts[fi].tounicode); + montauk::mfree(fonts); + return; + } + + // Collect content stream object numbers + int content_objs[32]; + int content_count = 0; + + int cp = skip_ws(d, len, contents_pos); + if (cp < len && d[cp] == '[') { + // Array of references + cp++; + while (cp < len && d[cp] != ']' && content_count < 32) { + cp = skip_ws(d, len, cp); + if (cp >= len || d[cp] == ']') break; + int ref_num; + int rp = parse_ref_at(d, len, cp, &ref_num); + if (rp > 0) { + content_objs[content_count++] = ref_num; + cp = rp; + } else { + cp++; + } + } + } else { + // Single reference + int ref_num; + if (parse_ref_at(d, len, cp, &ref_num) > 0) { + content_objs[content_count++] = ref_num; + } + } + + // Concatenate and parse all content streams + for (int ci = 0; ci < content_count; ci++) { + int stream_len; + uint8_t* stream_data = get_stream_data(content_objs[ci], &stream_len); + if (!stream_data) continue; + + // Parse the content stream + Operand* ops = (Operand*)montauk::malloc(MAX_OPERANDS * sizeof(Operand)); + if (!ops) { montauk::mfree(stream_data); continue; } + int op_count = 0; + + TextState ts; + ts.tm[0] = 1; ts.tm[1] = 0; ts.tm[2] = 0; ts.tm[3] = 1; ts.tm[4] = 0; ts.tm[5] = 0; + ts.lm[0] = 1; ts.lm[1] = 0; ts.lm[2] = 0; ts.lm[3] = 1; ts.lm[4] = 0; ts.lm[5] = 0; + ts.tl = 0; + ts.font_size = 12; + ts.font_name[0] = '\0'; + ts.font_flags = 0; + ts.tounicode = nullptr; + ts.embedded_font = nullptr; + + bool in_text = false; + Token tok; + int pos = 0; + + while (pos < stream_len) { + pos = next_token(stream_data, stream_len, pos, &tok); + if (tok.type == TOK_EOF) break; + + // Accumulate operands + if (tok.type == TOK_REAL || tok.type == TOK_INT) { + if (op_count < MAX_OPERANDS) { + ops[op_count].num = tok.num; + ops[op_count].is_name = false; + ops[op_count].is_string = false; + ops[op_count].str_len = 0; + op_count++; + } + continue; + } + if (tok.type == TOK_NAME) { + if (op_count < MAX_OPERANDS) { + ops[op_count].is_name = true; + ops[op_count].is_string = false; + str_cpy(ops[op_count].str, tok.str, MAX_TEXT_LEN); + ops[op_count].str_len = tok.str_len; + op_count++; + } + continue; + } + if (tok.type == TOK_STRING || tok.type == TOK_HEX_STRING) { + if (op_count < MAX_OPERANDS) { + ops[op_count].is_name = false; + ops[op_count].is_string = true; + montauk::memcpy(ops[op_count].str, tok.str, tok.str_len); + ops[op_count].str[tok.str_len] = '\0'; + ops[op_count].str_len = tok.str_len; + ops[op_count].num = 0; + op_count++; + } + continue; + } + if (tok.type == TOK_ARRAY_START) { + // Special handling for TJ arrays + // Collect items until ']' + // Build concatenated string from string elements + char tj_buf[MAX_TEXT_LEN]; + int tj_len = 0; + + while (pos < stream_len) { + Token atk; + int next_pos = next_token(stream_data, stream_len, pos, &atk); + if (atk.type == TOK_EOF) break; + if (atk.type == TOK_ARRAY_END) { pos = next_pos; break; } + + if ((atk.type == TOK_STRING || atk.type == TOK_HEX_STRING) && + tj_len + atk.str_len < MAX_TEXT_LEN - 1) { + montauk::memcpy(tj_buf + tj_len, atk.str, atk.str_len); + tj_len += atk.str_len; + } + // Number entries in TJ arrays are kerning values - skip them + pos = next_pos; + } + tj_buf[tj_len] = '\0'; + + if (tj_len > 0 && op_count < MAX_OPERANDS) { + ops[op_count].is_name = false; + ops[op_count].is_string = true; + montauk::memcpy(ops[op_count].str, tj_buf, tj_len + 1); + ops[op_count].str_len = tj_len; + ops[op_count].num = 0; + op_count++; + } + continue; + } + + // Process operator + if (tok.type != TOK_OPERATOR) { op_count = 0; continue; } + + const char* op = tok.str; + + // BT - begin text + if (op[0] == 'B' && op[1] == 'T' && op[2] == '\0') { + in_text = true; + ts.tm[0] = 1; ts.tm[1] = 0; ts.tm[2] = 0; ts.tm[3] = 1; + ts.tm[4] = 0; ts.tm[5] = 0; + ts.lm[0] = 1; ts.lm[1] = 0; ts.lm[2] = 0; ts.lm[3] = 1; + ts.lm[4] = 0; ts.lm[5] = 0; + } + // ET - end text + else if (op[0] == 'E' && op[1] == 'T' && op[2] == '\0') { + in_text = false; + } + // Tf - set font + else if (op[0] == 'T' && op[1] == 'f' && op[2] == '\0' && in_text) { + if (op_count >= 2 && ops[0].is_name) { + str_cpy(ts.font_name, ops[0].str, 32); + ts.font_size = ops[1].num; + FontInfo* fi = lookup_font(fonts, ts.font_name); + ts.font_flags = fi ? fi->flags : 0; + ts.tounicode = fi ? fi->tounicode : nullptr; + ts.embedded_font = fi ? fi->embedded_font : nullptr; + } + } + // TL - set text leading + else if (op[0] == 'T' && op[1] == 'L' && op[2] == '\0' && in_text) { + if (op_count >= 1) ts.tl = ops[0].num; + } + // Td - move text position + else if (op[0] == 'T' && op[1] == 'd' && op[2] == '\0' && in_text) { + if (op_count >= 2) { + float tx = ops[0].num; + float ty = ops[1].num; + float translate[6] = {1, 0, 0, 1, tx, ty}; + float result[6]; + matrix_multiply(result, translate, ts.lm); + for (int i = 0; i < 6; i++) { ts.tm[i] = result[i]; ts.lm[i] = result[i]; } + } + } + // TD - move text position and set leading + else if (op[0] == 'T' && op[1] == 'D' && op[2] == '\0' && in_text) { + if (op_count >= 2) { + ts.tl = -ops[1].num; + float tx = ops[0].num; + float ty = ops[1].num; + float translate[6] = {1, 0, 0, 1, tx, ty}; + float result[6]; + matrix_multiply(result, translate, ts.lm); + for (int i = 0; i < 6; i++) { ts.tm[i] = result[i]; ts.lm[i] = result[i]; } + } + } + // Tm - set text matrix + else if (op[0] == 'T' && op[1] == 'm' && op[2] == '\0' && in_text) { + if (op_count >= 6) { + for (int i = 0; i < 6; i++) { + ts.tm[i] = ops[i].num; + ts.lm[i] = ops[i].num; + } + } + } + // T* - move to start of next line + else if (op[0] == 'T' && op[1] == '*' && op[2] == '\0' && in_text) { + float translate[6] = {1, 0, 0, 1, 0, -ts.tl}; + float result[6]; + matrix_multiply(result, translate, ts.lm); + for (int i = 0; i < 6; i++) { ts.tm[i] = result[i]; ts.lm[i] = result[i]; } + } + // Tj - show string + else if (op[0] == 'T' && op[1] == 'j' && op[2] == '\0' && in_text) { + if (op_count >= 1 && ops[0].is_string && ops[0].str_len > 0) { + float eff_size = ts.font_size; + // Scale by text matrix + float sy = ts.tm[3]; + if (sy < 0) sy = -sy; + if (sy > 0.01f) eff_size *= sy; + + add_text_item(page, ts.tm[4], ts.tm[5], eff_size, + ops[0].str, ops[0].str_len, ts.font_flags, ts.tounicode, ts.embedded_font); + + // Advance text position using font metrics when available + TrueTypeFont* adv_font = ts.embedded_font; + if (!adv_font) { + adv_font = g_font; + if ((ts.font_flags & 1) && g_font_bold) adv_font = g_font_bold; + if ((ts.font_flags & 4) && g_font_mono) adv_font = g_font_mono; + } + float advance; + if (adv_font) { + int msz = (int)(ts.font_size + 0.5f); + if (msz < 4) msz = 4; + if (msz > 120) msz = 120; + advance = (float)adv_font->measure_text(ops[0].str, msz); + } else { + advance = ops[0].str_len * ts.font_size * 0.5f; + } + ts.tm[4] += advance * ts.tm[0]; + ts.tm[5] += advance * ts.tm[1]; + } + } + // TJ - show string (array already concatenated) + else if (op[0] == 'T' && op[1] == 'J' && op[2] == '\0' && in_text) { + if (op_count >= 1 && ops[op_count - 1].is_string && ops[op_count - 1].str_len > 0) { + Operand* sop = &ops[op_count - 1]; + float eff_size = ts.font_size; + float sy = ts.tm[3]; + if (sy < 0) sy = -sy; + if (sy > 0.01f) eff_size *= sy; + + add_text_item(page, ts.tm[4], ts.tm[5], eff_size, + sop->str, sop->str_len, ts.font_flags, ts.tounicode, ts.embedded_font); + + TrueTypeFont* adv_font = ts.embedded_font; + if (!adv_font) { + adv_font = g_font; + if ((ts.font_flags & 1) && g_font_bold) adv_font = g_font_bold; + if ((ts.font_flags & 4) && g_font_mono) adv_font = g_font_mono; + } + float advance; + if (adv_font) { + int msz = (int)(ts.font_size + 0.5f); + if (msz < 4) msz = 4; + if (msz > 120) msz = 120; + advance = (float)adv_font->measure_text(sop->str, msz); + } else { + advance = sop->str_len * ts.font_size * 0.5f; + } + ts.tm[4] += advance * ts.tm[0]; + ts.tm[5] += advance * ts.tm[1]; + } + } + // ' (single quote) - move to next line and show string + else if (op[0] == '\'' && op[1] == '\0' && in_text) { + // T* + float translate[6] = {1, 0, 0, 1, 0, -ts.tl}; + float result[6]; + matrix_multiply(result, translate, ts.lm); + for (int i = 0; i < 6; i++) { ts.tm[i] = result[i]; ts.lm[i] = result[i]; } + + // Tj + if (op_count >= 1 && ops[0].is_string && ops[0].str_len > 0) { + float eff_size = ts.font_size; + float sy = ts.tm[3]; + if (sy < 0) sy = -sy; + if (sy > 0.01f) eff_size *= sy; + + add_text_item(page, ts.tm[4], ts.tm[5], eff_size, + ops[0].str, ops[0].str_len, ts.font_flags, ts.tounicode, ts.embedded_font); + } + } + // " (double quote) - set aw, ac, move to next line, show string + else if (op[0] == '"' && op[1] == '\0' && in_text) { + // T* + float translate[6] = {1, 0, 0, 1, 0, -ts.tl}; + float result[6]; + matrix_multiply(result, translate, ts.lm); + for (int i = 0; i < 6; i++) { ts.tm[i] = result[i]; ts.lm[i] = result[i]; } + + // Show string (third operand) + if (op_count >= 3 && ops[2].is_string && ops[2].str_len > 0) { + float eff_size = ts.font_size; + float sy = ts.tm[3]; + if (sy < 0) sy = -sy; + if (sy > 0.01f) eff_size *= sy; + + add_text_item(page, ts.tm[4], ts.tm[5], eff_size, + ops[2].str, ops[2].str_len, ts.font_flags, ts.tounicode, ts.embedded_font); + } + } + + op_count = 0; // reset operand stack after operator + } + + montauk::mfree(ops); + montauk::mfree(stream_data); + } + + for (int fi = 0; fi < fonts->count; fi++) + if (fonts->fonts[fi].tounicode) montauk::mfree(fonts->fonts[fi].tounicode); + montauk::mfree(fonts); +} diff --git a/programs/src/pdfviewer/pdf_parser.cpp b/programs/src/pdfviewer/pdf_parser.cpp new file mode 100644 index 0000000..404b4d2 --- /dev/null +++ b/programs/src/pdfviewer/pdf_parser.cpp @@ -0,0 +1,1479 @@ +/* + * 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 + 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; + + // 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 (type == 1 && obj_idx < size && field2 > 0) { + g_doc.xref[obj_idx] = field2; + } + // Type 0 = free, type 2 = compressed (not supported) + + data_off += entry_size; + } + } + + montauk::mfree(stream_data); + return true; +} + +// ============================================================================ +// 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].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; + } + + // 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.pages) { + for (int i = 0; i < g_doc.page_count; i++) { + if (g_doc.pages[i].items) montauk::mfree(g_doc.pages[i].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; +} diff --git a/programs/src/pdfviewer/pdfviewer.h b/programs/src/pdfviewer/pdfviewer.h new file mode 100644 index 0000000..c3984b0 --- /dev/null +++ b/programs/src/pdfviewer/pdfviewer.h @@ -0,0 +1,175 @@ +/* + * pdfviewer.h + * Shared header for the MontaukOS PDF viewer app + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +using namespace gui; + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 800; +static constexpr int INIT_H = 700; +static constexpr int TOOLBAR_H = 36; +static constexpr int STATUS_BAR_H = 24; +static constexpr int TB_BTN_SIZE = 24; +static constexpr int TB_BTN_Y = 6; +static constexpr int TB_BTN_RAD = 3; +static constexpr int PATHBAR_H = 32; +static constexpr int PAGE_MARGIN = 20; +static constexpr int PAGE_PAD = 8; +static constexpr int FONT_SIZE = 18; +static constexpr int HEADER_FONT = 16; +static constexpr int SCROLL_STEP = 40; +static constexpr int MAX_TEXT_LEN = 128; +static constexpr int MAX_OPERANDS = 16; + +// Colors +static constexpr Color BG_COLOR = Color::from_rgb(0x80, 0x80, 0x80); +static constexpr Color PAGE_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color PAGE_SHADOW = Color::from_rgb(0x60, 0x60, 0x60); +static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22); +static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); +static constexpr Color HEADER_TEXT = Color::from_rgb(0x55, 0x55, 0x55); +static constexpr Color TB_BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); +static constexpr Color TB_BTN_ACTIVE = Color::from_rgb(0xC0, 0xD0, 0xE8); +static constexpr Color TB_SEP_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC); +static constexpr Color GRID_COLOR = Color::from_rgb(0xD0, 0xD0, 0xD0); +static constexpr Color STATUS_BG = Color::from_rgb(0x2B, 0x3E, 0x50); +static constexpr Color STATUS_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); + +// ============================================================================ +// Types +// ============================================================================ + +struct TextItem { + float x, y; // PDF user-space coordinates (origin bottom-left) + float font_size; // effective font size in points + char text[MAX_TEXT_LEN]; + uint8_t flags; // bit 0: bold, bit 1: italic, bit 2: monospace + TrueTypeFont* font; // embedded font, or nullptr for system font +}; + +struct PdfPage { + TextItem* items; + int item_count; + int item_cap; + float width, height; // page dimensions in points (from MediaBox) +}; + +struct EmbeddedFontEntry { + int stream_obj; // FontFile stream object number + TrueTypeFont* font; // loaded font + uint8_t* font_data; // heap-allocated font data (must persist for stb_truetype) +}; + +struct PdfDoc { + uint8_t* data; + int data_len; + + int* xref; // byte offset for each object number + int xref_count; + + PdfPage* pages; + int* page_objs; // object number for each page + int page_count; + int page_cap; + + EmbeddedFontEntry* emb_fonts; // cached embedded fonts + int emb_font_count; + + bool valid; +}; + +struct FontInfo { + char name[32]; // PDF font name (e.g., "F1") + 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 +}; + +struct FontMap { + FontInfo fonts[32]; + int count; +}; + +// ============================================================================ +// Global state (extern -- defined in main.cpp) +// ============================================================================ + +extern int g_win_w, g_win_h; +extern PdfDoc g_doc; +extern int g_current_page; +extern int g_scroll_y; +extern float g_zoom; + +extern char g_filepath[256]; + +extern bool g_pathbar_open; +extern char g_pathbar_text[256]; +extern int g_pathbar_len; +extern int g_pathbar_cursor; + +extern TrueTypeFont* g_font; +extern TrueTypeFont* g_font_bold; +extern TrueTypeFont* g_font_mono; + +extern char g_status_msg[128]; + +// ============================================================================ +// helpers.cpp +// ============================================================================ + +void px_fill(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c); +void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c); +void px_vline(uint32_t* px, int bw, int bh, int x, int y, int h, Color c); +void px_rect(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c); +void px_fill_rounded(uint32_t* px, int bw, int bh, int x, int y, int w, int h, int r, Color c); +int str_len(const char* s); +void str_cpy(char* dst, const char* src, int max); + +// ============================================================================ +// pdf_parser.cpp +// ============================================================================ + +bool load_pdf(const char* path); +void free_pdf(); + +// Internal utilities used by pdf_page.cpp +int skip_ws(const uint8_t* d, int len, int p); +int parse_int_at(const uint8_t* d, int len, int p, int* val); +int parse_real_at(const uint8_t* d, int len, int p, float* val); +bool starts_with(const uint8_t* d, int len, int p, const char* s); +int dict_lookup(const uint8_t* d, int len, int pos, const char* key); +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); + +// ============================================================================ +// pdf_page.cpp +// ============================================================================ + +void parse_page(int page_idx, int page_obj_num); + +// ============================================================================ +// render.cpp +// ============================================================================ + +void render(uint32_t* pixels); diff --git a/programs/src/pdfviewer/render.cpp b/programs/src/pdfviewer/render.cpp new file mode 100644 index 0000000..8983ef1 --- /dev/null +++ b/programs/src/pdfviewer/render.cpp @@ -0,0 +1,233 @@ +/* + * render.cpp + * UI rendering -- toolbar, page content, status bar + * Copyright (c) 2026 Daniel Hammer + */ + +#include "pdfviewer.h" + +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); + + // ---- Path bar (Open dialog) ---- + int pathbar_h = g_pathbar_open ? PATHBAR_H : 0; + + // ---- Page content area ---- + int content_y = TOOLBAR_H + pathbar_h; + int content_h = g_win_h - content_y - STATUS_BAR_H; + + if (g_doc.valid && g_current_page >= 0 && g_current_page < g_doc.page_count) { + PdfPage* page = &g_doc.pages[g_current_page]; + + int page_w = (int)(page->width * g_zoom); + int page_h = (int)(page->height * g_zoom); + + // Center page horizontally + int page_x = (g_win_w - page_w) / 2; + if (page_x < PAGE_MARGIN) page_x = PAGE_MARGIN; + + int page_y = content_y + PAGE_MARGIN - g_scroll_y; + + // Draw page shadow + px_fill(pixels, g_win_w, g_win_h, + page_x + 3, page_y + 3, page_w, page_h, PAGE_SHADOW); + + // Draw page background (clip to content area) + int clip_y0 = content_y; + int clip_y1 = content_y + content_h; + + // Draw white page + for (int row = page_y; row < page_y + page_h; row++) { + if (row < clip_y0 || row >= clip_y1) continue; + int x0 = page_x < 0 ? 0 : page_x; + int x1 = page_x + page_w; + if (x1 > g_win_w) x1 = g_win_w; + uint32_t white = PAGE_COLOR.to_pixel(); + for (int col = x0; col < x1; col++) + pixels[row * g_win_w + col] = white; + } + + // 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; + if ((item->flags & 1) && g_font_bold) font = g_font_bold; + if ((item->flags & 4) && g_font_mono) font = g_font_mono; + } + 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; + + 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, sy, item->text, TEXT_COLOR, px_size); + } else { + // System font: filter out non-printable characters + char render_text[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; + } else if (c == '\t') { + render_text[ri++] = ' '; + } + } + render_text[ri] = '\0'; + + if (ri > 0) { + font->draw_to_buffer(pixels, g_win_w, g_win_h, + sx, sy, render_text, TEXT_COLOR, px_size); + } + } + } + + // Page border + if (page_y < clip_y1 && page_y + page_h > clip_y0) { + px_rect(pixels, g_win_w, g_win_h, + page_x, page_y, page_w, page_h, + Color::from_rgb(0xAA, 0xAA, 0xAA)); + } + } else if (!g_doc.valid) { + // No document loaded - show help text + if (g_font) { + const char* msg = "Press Ctrl+O or click Open to load a PDF file"; + int tw = g_font->measure_text(msg, FONT_SIZE); + int tx = (g_win_w - tw) / 2; + int ty = content_y + content_h / 2 - FONT_SIZE / 2; + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, + tx, ty, msg, Color::from_rgb(0xCC, 0xCC, 0xCC), FONT_SIZE); + } + } + + // ---- Toolbar (drawn after content so it stays on top) ---- + px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG); + px_hline(pixels, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, GRID_COLOR); + + int bx = 4; + auto tb_btn = [&](int w, bool active, const char* label) { + Color bg = active ? TB_BTN_ACTIVE : TB_BTN_BG; + px_fill_rounded(pixels, g_win_w, g_win_h, bx, TB_BTN_Y, w, TB_BTN_SIZE, TB_BTN_RAD, bg); + if (g_font && label[0]) { + TrueTypeFont* f = g_font; + int tw = f->measure_text(label, HEADER_FONT); + f->draw_to_buffer(pixels, g_win_w, g_win_h, + bx + (w - tw) / 2, TB_BTN_Y + (TB_BTN_SIZE - HEADER_FONT) / 2, + label, HEADER_TEXT, HEADER_FONT); + } + bx += w + 4; + }; + auto tb_sep = [&]() { + px_vline(pixels, g_win_w, g_win_h, bx, 6, TOOLBAR_H - 12, TB_SEP_COLOR); + bx += 8; + }; + + // Open + tb_btn(36, false, "Open"); + tb_sep(); + + // Navigation + tb_btn(24, false, "<"); + tb_btn(24, false, ">"); + tb_sep(); + + // Zoom + tb_btn(24, false, "-"); + tb_btn(24, false, "+"); + + // Zoom percentage label + { + char zoom_label[16]; + int pct = (int)(g_zoom * 100 + 0.5f); + snprintf(zoom_label, 16, "%d%%", pct); + if (g_font) { + int tw = g_font->measure_text(zoom_label, HEADER_FONT); + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, + bx, TB_BTN_Y + (TB_BTN_SIZE - HEADER_FONT) / 2, + zoom_label, HEADER_TEXT, HEADER_FONT); + bx += tw + 8; + } + } + + // ---- Path bar (Open dialog, drawn after content) ---- + if (g_pathbar_open) { + int pby = TOOLBAR_H; + px_fill(pixels, g_win_w, g_win_h, 0, pby, g_win_w, PATHBAR_H, HEADER_BG); + px_hline(pixels, g_win_w, g_win_h, 0, pby + PATHBAR_H - 1, g_win_w, GRID_COLOR); + + const char* label = "Open:"; + if (g_font) { + int lw = g_font->measure_text(label, HEADER_FONT); + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, + 8, pby + (PATHBAR_H - HEADER_FONT) / 2, label, HEADER_TEXT, HEADER_FONT); + + int inp_x = 16 + lw; + int inp_w = g_win_w - inp_x - 8; + px_fill(pixels, g_win_w, g_win_h, inp_x, pby + 4, inp_w, PATHBAR_H - 8, PAGE_COLOR); + px_rect(pixels, g_win_w, g_win_h, inp_x, pby + 4, inp_w, PATHBAR_H - 8, GRID_COLOR); + + if (g_pathbar_text[0]) + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, + inp_x + 4, pby + (PATHBAR_H - FONT_SIZE) / 2, + g_pathbar_text, TEXT_COLOR, FONT_SIZE); + + // Cursor + char prefix[256]; + int plen = g_pathbar_cursor < 255 ? g_pathbar_cursor : 255; + for (int i = 0; i < plen; i++) prefix[i] = g_pathbar_text[i]; + prefix[plen] = '\0'; + int cx = inp_x + 4 + g_font->measure_text(prefix, FONT_SIZE); + Color cursor_color = Color::from_rgb(0x36, 0x7B, 0xF0); + px_fill(pixels, g_win_w, g_win_h, cx, pby + 6, 2, PATHBAR_H - 12, cursor_color); + } + } + + // ---- Status bar ---- + int sy = g_win_h - STATUS_BAR_H; + px_fill(pixels, g_win_w, g_win_h, 0, sy, g_win_w, STATUS_BAR_H, STATUS_BG); + + if (g_font) { + // Left: filename or status message + char left_text[128]; + if (g_filepath[0]) { + const char* fname = g_filepath; + for (int i = 0; g_filepath[i]; i++) + if (g_filepath[i] == '/') fname = g_filepath + i + 1; + snprintf(left_text, 128, " %s", fname); + } else if (g_status_msg[0]) { + snprintf(left_text, 128, " %s", g_status_msg); + } else { + str_cpy(left_text, " PDF Viewer", 128); + } + int sty = sy + (STATUS_BAR_H - HEADER_FONT) / 2; + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, 6, sty, left_text, STATUS_TEXT, HEADER_FONT); + + // Right: page number + if (g_doc.valid) { + char right_text[64]; + snprintf(right_text, 64, "Page %d of %d ", g_current_page + 1, g_doc.page_count); + int rw = g_font->measure_text(right_text, HEADER_FONT); + g_font->draw_to_buffer(pixels, g_win_w, g_win_h, + g_win_w - rw - 6, sty, right_text, STATUS_TEXT, HEADER_FONT); + } + } +} diff --git a/programs/src/pdfviewer/stb_truetype_impl.cpp b/programs/src/pdfviewer/stb_truetype_impl.cpp new file mode 100644 index 0000000..9ce2266 --- /dev/null +++ b/programs/src/pdfviewer/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// Override all stb_truetype dependencies before including the implementation + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include