From 990d184bd09d61e434d3964c29d497c3a8916938 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Mon, 10 Aug 2026 15:03:54 +0200 Subject: [PATCH] feat: pdfviewer - expand PDF rendering support --- kernel/src/Api/BuildNo.hpp | 2 +- programs/GNUmakefile | 2 +- programs/src/pdfviewer/Makefile | 5 +- programs/src/pdfviewer/main.cpp | 2 + programs/src/pdfviewer/pdf_page.cpp | 809 +++++++++++++++++++++----- programs/src/pdfviewer/pdf_parser.cpp | 663 ++++++++++++++++++--- programs/src/pdfviewer/pdfviewer.h | 60 +- programs/src/pdfviewer/render.cpp | 329 +++++++++-- 8 files changed, 1609 insertions(+), 263 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index aa327e8..5b504e9 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 96 +#define MONTAUK_BUILD_NUMBER 99 diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 5af443e..36d6990 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -173,7 +173,7 @@ wordprocessor: libc libloader dialogs $(MAKE) -C src/wordprocessor # Build PDF viewer standalone GUI client (depends on libc and libdialogs). -pdfviewer: libc libloader dialogs +pdfviewer: libc libjpeg libloader dialogs $(MAKE) -C src/pdfviewer # Build disks standalone GUI tool (depends on libc). diff --git a/programs/src/pdfviewer/Makefile b/programs/src/pdfviewer/Makefile index 1591b16..9f9bea4 100644 --- a/programs/src/pdfviewer/Makefile +++ b/programs/src/pdfviewer/Makefile @@ -20,6 +20,7 @@ LINK_LD := ../../link.ld BINDIR := ../../bin OBJDIR := obj LIBDIR := ../../lib +JPEG_LIB := $(LIBDIR)/libjpeg/libjpeg.a # ---- Compiler flags ---- @@ -63,9 +64,9 @@ LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a all: $(TARGET) -$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(LIBDIR)/libc/liblibc.a +$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(JPEG_LIB) $(LIBDIR)/libc/liblibc.a mkdir -p $(BINDIR)/apps/pdfviewer - $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(LIBDIR)/libc/liblibc.a -o $@ + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(JPEG_LIB) $(LIBDIR)/libc/liblibc.a -o $@ HEADERS := pdfviewer.h diff --git a/programs/src/pdfviewer/main.cpp b/programs/src/pdfviewer/main.cpp index 6b3f0c0..e5eb0ed 100644 --- a/programs/src/pdfviewer/main.cpp +++ b/programs/src/pdfviewer/main.cpp @@ -85,6 +85,8 @@ static void go_to_page(int page) { if (page >= g_doc.page_count) page = g_doc.page_count - 1; g_current_page = page; g_scroll_y = 0; + ensure_page_parsed(page); + trim_page_cache(page); } static void zoom_in() { diff --git a/programs/src/pdfviewer/pdf_page.cpp b/programs/src/pdfviewer/pdf_page.cpp index 4cc466b..c6a93df 100644 --- a/programs/src/pdfviewer/pdf_page.cpp +++ b/programs/src/pdfviewer/pdf_page.cpp @@ -198,54 +198,91 @@ static int next_token(const uint8_t* d, int len, int p, Token* tok) { // Text State and Item Output // ============================================================================ +struct ClipRect { float x0, y0, x1, y1; }; + +static void add_paint_command(PdfPage* page, PaintType type, int index, + const ClipRect& clip) { + if(page->paint_count>=page->paint_cap) { + int cap=page->paint_cap?page->paint_cap*2:128; + PaintCommand* commands=(PaintCommand*)montauk::malloc(cap*sizeof(PaintCommand)); + if(!commands)return; + if(page->paint_commands){montauk::memcpy(commands,page->paint_commands,page->paint_count*sizeof(PaintCommand));montauk::mfree(page->paint_commands);} + page->paint_commands=commands;page->paint_cap=cap; + } + PaintCommand* command=&page->paint_commands[page->paint_count++]; + command->type=type;command->index=index; + command->clip_x0=clip.x0;command->clip_y0=clip.y0; + command->clip_x1=clip.x1;command->clip_y1=clip.y1; +} + 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; + float char_space, word_space; + float hscale; // Tz as a multiplier (100% = 1) + float rise; 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 + const float* widths; + bool has_widths; + bool two_byte; }; // 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 uint8_t unicode_to_pdf_byte(uint16_t cp) { + if (cp <= 255) return (uint8_t)cp; + switch (cp) { + case 0x2013: return 0x96; + case 0x2014: return 0x97; + case 0x2018: return 0x91; + case 0x2019: return 0x92; + case 0x201C: return 0x93; + case 0x201D: return 0x94; + case 0x2022: return 0x95; + case 0x2026: return 0x85; + default: return '?'; + } +} + static int apply_tounicode(const char* raw, int raw_len, char* out, int out_max, - const uint16_t* tounicode) { + const uint16_t* tounicode, bool two_byte) { 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++] = '?'; - } + for (int i = 0; i < raw_len && oi < out_max - 1;) { + unsigned glyph; + if (two_byte && i + 1 < raw_len) { + glyph = ((uint8_t)raw[i] << 8) | (uint8_t)raw[i + 1]; + i += 2; } else { - out[oi++] = '?'; + glyph = (uint8_t)raw[i++]; } + uint16_t cp = (tounicode && glyph < 256) ? tounicode[glyph] : 0; + if (cp == 0) cp = glyph <= 255 ? (uint16_t)glyph : (uint16_t)'?'; + out[oi++] = (char)unicode_to_pdf_byte(cp); } out[oi] = '\0'; return oi; } static void add_text_item(PdfPage* page, float x, float y, float size, + const float* basis, float advance, + const float* glyph_advances, int advance_count, const char* text, int text_len, uint8_t flags, - const uint16_t* tounicode, TrueTypeFont* embedded_font) { + const uint16_t* tounicode, TrueTypeFont* embedded_font, + bool two_byte, uint8_t r, uint8_t g, uint8_t b, + const ClipRect& clip) { 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); + if (tounicode && (!embedded_font || two_byte)) { + text_len = apply_tounicode(text, text_len, mapped, MAX_TEXT_LEN, + tounicode, two_byte); text = mapped; } if (text_len <= 0) return; @@ -267,21 +304,29 @@ static void add_text_item(PdfPage* page, float x, float y, float size, item->x = x; item->y = y; item->font_size = size; + for (int i = 0; i < 4; i++) item->basis[i] = basis[i]; + item->advance = advance; item->flags = flags; + item->r = r; item->g = g; item->b = b; 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'; + item->text_len = copy; + float fallback_advance=copy>0?advance/copy:0; + for(int i=0;iglyph_advances[i]=iitem_count-1,clip); } // ============================================================================ // Graphics State // ============================================================================ -static constexpr int MAX_PATH_SEGS = 512; -static constexpr int MAX_PATH_RECTS = 128; -static constexpr int MAX_GFX_STACK = 16; +static constexpr int MAX_PATH_SEGS = 4096; +static constexpr int MAX_PATH_RECTS = 1024; +static constexpr int MAX_GFX_STACK = 64; struct PathSeg { float x1, y1, x2, y2; }; struct PathRect { float x, y, w, h; }; @@ -291,6 +336,7 @@ struct GfxState { float line_width; uint8_t stroke_r, stroke_g, stroke_b; uint8_t fill_r, fill_g, fill_b; + ClipRect clip; }; static void ctm_transform(const float* ctm, float x, float y, float* ox, float* oy) { @@ -298,23 +344,40 @@ static void ctm_transform(const float* ctm, float x, float y, float* ox, float* *oy = ctm[1] * x + ctm[3] * y + ctm[5]; } +static void intersect_clip_with_path(GfxState* gs, const PathSeg* segments, int count) { + if(count<=0)return; + float x0=segments[0].x1,x1=x0,y0=segments[0].y1,y1=y0; + for(int i=0;ix1)x1=xs[j];if(ys[j]y1)y1=ys[j];} + } + if(x0>gs->clip.x0)gs->clip.x0=x0; + if(y0>gs->clip.y0)gs->clip.y0=y0; + if(x1clip.x1)gs->clip.x1=x1; + if(y1clip.y1)gs->clip.y1=y1; + if(gs->clip.x1clip.x0)gs->clip.x1=gs->clip.x0; + if(gs->clip.y1clip.y0)gs->clip.y1=gs->clip.y0; +} + static float ctm_scale(const float* ctm) { // Approximate scale factor (geometric mean of axis scales) float sx = ctm[0] * ctm[0] + ctm[1] * ctm[1]; float sy = ctm[2] * ctm[2] + ctm[3] * ctm[3]; // sqrt approximation: just use max for simplicity if (sx < sy) sx = sy; - // Manual sqrt via Newton's method (2 iterations) + // Manual sqrt via Newton's method. Two iterations (the old behavior) are + // wildly inaccurate for common PDF scales such as 10 or 100. if (sx <= 0) return 1.0f; - float g = sx; - g = 0.5f * (g + sx / g); - g = 0.5f * (g + sx / g); + float g = sx >= 1.0f ? sx : 1.0f; + for(int i=0;i<16;i++) g=0.5f*(g+sx/g); return g; } static void add_gfx_item(PdfPage* page, GfxType type, float x1, float y1, float x2, float y2, - float lw, uint8_t r, uint8_t g, uint8_t b) { + float lw, uint8_t r, uint8_t g, uint8_t b, + const ClipRect& clip) { if (page->gfx_count >= page->gfx_cap) { int new_cap = page->gfx_cap ? page->gfx_cap * 2 : 64; GraphicsItem* ni = (GraphicsItem*)montauk::malloc(new_cap * sizeof(GraphicsItem)); @@ -332,6 +395,81 @@ static void add_gfx_item(PdfPage* page, GfxType type, item->x2 = x2; item->y2 = y2; item->line_width = lw; item->r = r; item->g = g; item->b = b; + add_paint_command(page,PAINT_GRAPHICS,page->gfx_count-1,clip); +} + +static void add_filled_path(PdfPage* page, const PathSeg* segments, int count, + bool even_odd, uint8_t r, uint8_t g, uint8_t b, + const ClipRect& clip) { + if (count < 2) return; + if (page->fill_count >= page->fill_cap) { + int cap=page->fill_cap?page->fill_cap*2:16; + FilledPathItem* items=(FilledPathItem*)montauk::malloc(cap*sizeof(FilledPathItem)); + if(!items)return; + if(page->fill_items){montauk::memcpy(items,page->fill_items,page->fill_count*sizeof(FilledPathItem));montauk::mfree(page->fill_items);} + page->fill_items=items;page->fill_cap=cap; + } + FilledPathEdge* edges=(FilledPathEdge*)montauk::malloc(count*2*sizeof(FilledPathEdge)); + if(!edges)return; + int edge_count=0; + float start_x=segments[0].x1,start_y=segments[0].y1; + for(int i=0;ifill_items[page->fill_count++]; + item->edges=edges;item->edge_count=edge_count;item->r=r;item->g=g;item->b=b;item->even_odd=even_odd; + add_paint_command(page,PAINT_FILL,page->fill_count-1,clip); +} + +static void paint_fill(PdfPage* page, const PathSeg* segments, int seg_count, + const PathRect* rects, int rect_count, bool even_odd, + uint8_t r, uint8_t g, uint8_t b, const ClipRect& clip) { + // Keep the fast rectangle representation for the overwhelmingly common + // page backgrounds and table cells. Mixed or curved paths use the full + // edge list and are scan-converted by the renderer. + bool simple_rects=rect_count>0 && seg_count==rect_count*4; + for(int i=0;simple_rects&&ix||edge[0].y1!=rect->y|| + edge[0].x2!=rect->x+rect->w||edge[0].y2!=rect->y|| + edge[1].x2!=rect->x+rect->w||edge[1].y2!=rect->y+rect->h|| + edge[2].x2!=rect->x||edge[2].y2!=rect->y+rect->h) simple_rects=false; + } + if (simple_rects) { + for(int i=0;iimage_count >= page->image_cap) { + int cap = page->image_cap ? page->image_cap * 2 : 8; + ImageItem* items = (ImageItem*)montauk::malloc(cap * sizeof(ImageItem)); + if (!items) return; + if (page->image_items) { + montauk::memcpy(items, page->image_items, page->image_count * sizeof(ImageItem)); + montauk::mfree(page->image_items); + } + page->image_items = items; + page->image_cap = cap; + } + ImageItem* item = &page->image_items[page->image_count++]; + item->pixels = pixels; item->width = width; item->height = height; + item->stencil=stencil;item->stencil_r=stencil_r;item->stencil_g=stencil_g;item->stencil_b=stencil_b; + for (int i = 0; i < 6; i++) item->matrix[i] = matrix[i]; + add_paint_command(page,PAINT_IMAGE,page->image_count-1,clip); } static void matrix_multiply(float* result, const float* a, const float* b) { @@ -347,6 +485,85 @@ static void matrix_multiply(float* result, const float* a, const float* b) { result[5] = a[4] * b[1] + a[5] * b[3] + b[5]; } +static float text_run_advances(const TextState& ts, const char* text, int len, + float* advances, int capacity, int* count) { + TrueTypeFont* font = ts.embedded_font; + if (!font) { + font = g_font; + if ((ts.font_flags & 1) && g_font_bold) font = g_font_bold; + if ((ts.font_flags & 4) && g_font_mono) font = g_font_mono; + } + float total=0;int output_count=0; + for(int i=0;ivalid) { + unsigned render_code=code<=255?code:'?'; + if(ts.tounicode&&code<256&&(!ts.embedded_font||ts.two_byte)) { + uint16_t cp=ts.tounicode[code]; + if(cp)render_code=unicode_to_pdf_byte(cp); + } + int metric_advance=0,lsb=0; + int codepoint=decode_single_byte_codepoint(render_code); + stbtt_GetCodepointHMetrics(&font->info,codepoint,&metric_advance,&lsb); + float scale=font->em_scaling + ?stbtt_ScaleForMappingEmToPixels(&font->info,ts.font_size) + :stbtt_ScaleForPixelHeight(&font->info,ts.font_size); + glyph_advance=metric_advance*scale; + } else { + glyph_advance=ts.font_size*0.5f; + } + glyph_advance+=ts.char_space; + if(!ts.two_byte&&code==32)glyph_advance+=ts.word_space; + advances[output_count++]=glyph_advance; + total+=glyph_advance; + } + *count=output_count; + return total; +} + +static void show_text_run(PdfPage* page, TextState& ts, const GfxState& gs, + const char* text, int len) { + if (len <= 0) return; + float tx = ts.tm[4] + ts.tm[2] * ts.rise; + float ty = ts.tm[5] + ts.tm[3] * ts.rise; + float gx, gy; + ctm_transform(gs.ctm, tx, ty, &gx, &gy); + + // Linear part of text matrix followed by CTM. Horizontal scaling is + // part of the text rendering matrix, but not the stored text matrix. + float basis[4]; + basis[0] = (gs.ctm[0] * ts.tm[0] + gs.ctm[2] * ts.tm[1]) * ts.hscale; + basis[1] = (gs.ctm[1] * ts.tm[0] + gs.ctm[3] * ts.tm[1]) * ts.hscale; + basis[2] = gs.ctm[0] * ts.tm[2] + gs.ctm[2] * ts.tm[3]; + basis[3] = gs.ctm[1] * ts.tm[2] + gs.ctm[3] * ts.tm[3]; + + float glyph_advances[MAX_TEXT_LEN];int advance_count=0; + float advance=text_run_advances(ts,text,len,glyph_advances,MAX_TEXT_LEN, + &advance_count); + add_text_item(page, gx, gy, ts.font_size, basis, advance, + glyph_advances,advance_count, + text, len, ts.font_flags, ts.tounicode, ts.embedded_font, + ts.two_byte, gs.fill_r, gs.fill_g, gs.fill_b, gs.clip); + float displaced = advance * ts.hscale; + ts.tm[4] += displaced * ts.tm[0]; + ts.tm[5] += displaced * ts.tm[1]; +} + +static void adjust_text_position(TextState& ts, float adjustment) { + float displaced = adjustment * ts.hscale; + ts.tm[4] += displaced * ts.tm[0]; + ts.tm[5] += displaced * ts.tm[1]; +} + // ============================================================================ // Operand Stack // ============================================================================ @@ -359,6 +576,102 @@ struct Operand { bool is_string; }; +struct TJElement { + bool is_string; + int offset, len; + float adjustment; +}; + +static constexpr int MAX_TJ_ELEMENTS = 1024; + +struct XObjectEntry { char name[32]; int obj_num; }; +struct XObjectMap { XObjectEntry entries[64]; int count; }; + +static void build_xobject_map_impl(int obj_num, bool inherit_page_resources, + const XObjectMap* fallback, XObjectMap* out) { + if (fallback) *out = *fallback; + else out->count = 0; + const uint8_t* d = g_doc.data; + int len = g_doc.data_len; + int start, end; + if (find_obj_content(obj_num, &start, &end) < 0) return; + int p = inherit_page_resources + ? find_inherited_page_value(obj_num, "Resources") + : dict_lookup(d, len, start, "Resources"); + int ref; + if (p < 0) return; + out->count = 0; + if (parse_ref_at(d, len, p, &ref) > 0) { + if (find_obj_content(ref, &start, &end) < 0) return; + p = start; + } + p = dict_lookup(d, len, p, "XObject"); + if (p < 0) return; + if (parse_ref_at(d, len, p, &ref) > 0) { + if (find_obj_content(ref, &start, &end) < 0) return; + p = start; + } + p = skip_ws(d, len, p); + if (p + 1 >= len || d[p] != '<' || d[p + 1] != '<') return; + p += 2; + while (p < len && out->count < 64) { + p = skip_ws(d, len, p); + if (p + 1 < len && d[p] == '>' && d[p + 1] == '>') break; + if (d[p] != '/') { p++; continue; } + p++; + XObjectEntry* e = &out->entries[out->count]; + int n = 0; + while (p < len && n < 31 && d[p] != ' ' && d[p] != '\t' && d[p] != '\r' && + d[p] != '\n' && d[p] != '/' && d[p] != '>') e->name[n++] = (char)d[p++]; + e->name[n] = '\0'; + int next = parse_ref_at(d, len, p, &e->obj_num); + if (next > 0) { out->count++; p = next; } else p++; + } +} + +static void build_xobject_map(int page_obj_num, XObjectMap* out) { + build_xobject_map_impl(page_obj_num, true, nullptr, out); +} + +static void build_form_xobject_map(int form_obj_num, const XObjectMap* fallback, + XObjectMap* out) { + build_xobject_map_impl(form_obj_num, false, fallback, out); +} + +static int lookup_xobject(const XObjectMap* map, const char* name) { + for (int i = 0; i < map->count; i++) { + int j = 0; + while (map->entries[i].name[j] && map->entries[i].name[j] == name[j]) j++; + if (!map->entries[i].name[j] && !name[j]) return map->entries[i].obj_num; + } + return -1; +} + +static void free_local_font_map(FontMap* fonts) { + if (!fonts) return; + for (int i = 0; i < fonts->count; i++) + if (fonts->fonts[i].tounicode) montauk::mfree(fonts->fonts[i].tounicode); + montauk::mfree(fonts); +} + +struct ContentFrame { + uint8_t* data; + int len, pos; + GfxState gs; + TextState ts; + bool in_text; + bool clip_pending; + FontMap* fonts; + bool owns_fonts; + XObjectMap xobjects; + PathSeg* path_segs; + PathRect* path_rects; + int seg_count, rect_count; + float path_cx, path_cy, path_sx, path_sy; +}; + +static constexpr int MAX_FORM_DEPTH = 16; + // ============================================================================ // Content Stream Parser // ============================================================================ @@ -378,11 +691,15 @@ static FontInfo* lookup_font(FontMap* fonts, const char* name) { void parse_page(int page_idx, int page_obj_num) { PdfPage* page = &g_doc.pages[page_idx]; + if (page->parsed) return; + page->parsed = true; // Build font map for this page FontMap* fonts = (FontMap*)montauk::malloc(sizeof(FontMap)); if (!fonts) return; build_font_map(page_obj_num, fonts); + XObjectMap xobjects; + build_xobject_map(page_obj_num, &xobjects); // Get content stream(s) int start, end; @@ -405,14 +722,14 @@ void parse_page(int page_idx, int page_obj_num) { } // Collect content stream object numbers - int content_objs[32]; + int content_objs[256]; 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) { + while (cp < len && d[cp] != ']' && content_count < 256) { cp = skip_ws(d, len, cp); if (cp >= len || d[cp] == ']') break; int ref_num; @@ -432,15 +749,50 @@ void parse_page(int page_idx, int page_obj_num) { } } - // Concatenate and parse all content streams + // A /Contents array is semantically one token stream. Producers may + // split it in the middle of a graphics or text state, so resetting the + // interpreter for each object is incorrect. + uint8_t* combined_data = nullptr; + int combined_len = 0, combined_cap = 0; 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; + int part_len = 0; + uint8_t* part = get_stream_data(content_objs[ci], &part_len); + if (!part) continue; + int needed = combined_len + part_len + 1; + if (needed > combined_cap) { + int cap = combined_cap ? combined_cap : 4096; + while (cap < needed && cap <= 256 * 1024 * 1024 / 2) cap *= 2; + if (cap < needed) cap = needed; + uint8_t* grown = (uint8_t*)montauk::malloc(cap); + if (!grown) { + montauk::mfree(part); + if (combined_data) montauk::mfree(combined_data); + combined_data=nullptr; combined_len=0; + break; + } + if (combined_data) { + montauk::memcpy(grown, combined_data, combined_len); + montauk::mfree(combined_data); + } + combined_data=grown; combined_cap=cap; + } + montauk::memcpy(combined_data+combined_len,part,part_len); + combined_len+=part_len; + combined_data[combined_len++]=' '; + montauk::mfree(part); + } + + if (combined_data) { + int stream_len=combined_len; + uint8_t* stream_data=combined_data; // Parse the content stream Operand* ops = (Operand*)montauk::malloc(MAX_OPERANDS * sizeof(Operand)); - if (!ops) { montauk::mfree(stream_data); continue; } + if (!ops) { montauk::mfree(stream_data); goto page_done; } + ContentFrame* frames = (ContentFrame*)montauk::malloc(MAX_FORM_DEPTH * sizeof(ContentFrame)); + if (!frames) { montauk::mfree(ops); montauk::mfree(stream_data); goto page_done; } + int frame_depth = 0; + bool owns_fonts = false; int op_count = 0; TextState ts; @@ -448,10 +800,13 @@ void parse_page(int page_idx, int page_obj_num) { 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.char_space = 0; ts.word_space = 0; ts.hscale = 1; ts.rise = 0; ts.font_name[0] = '\0'; ts.font_flags = 0; ts.tounicode = nullptr; ts.embedded_font = nullptr; + ts.widths = nullptr; ts.has_widths = false; + ts.two_byte = false; // Graphics state GfxState gs; @@ -460,6 +815,7 @@ void parse_page(int page_idx, int page_obj_num) { gs.line_width = 1.0f; gs.stroke_r = 0; gs.stroke_g = 0; gs.stroke_b = 0; gs.fill_r = 0; gs.fill_g = 0; gs.fill_b = 0; + gs.clip = {0,0,page->width,page->height}; GfxState gs_stack[MAX_GFX_STACK]; int gs_depth = 0; @@ -472,12 +828,32 @@ void parse_page(int page_idx, int page_obj_num) { float path_sx = 0, path_sy = 0; // subpath start bool in_text = false; + bool clip_pending = false; + TJElement tj_elements[MAX_TJ_ELEMENTS]; + int tj_count = 0; Token tok; int pos = 0; - while (pos < stream_len) { + while (true) { pos = next_token(stream_data, stream_len, pos, &tok); - if (tok.type == TOK_EOF) break; + if (tok.type == TOK_EOF) { + if (frame_depth == 0) break; + if (owns_fonts) free_local_font_map(fonts); + montauk::mfree(stream_data); + if (path_segs) montauk::mfree(path_segs); + if (path_rects) montauk::mfree(path_rects); + ContentFrame* frame = &frames[--frame_depth]; + stream_data=frame->data; stream_len=frame->len; pos=frame->pos; + gs=frame->gs; ts=frame->ts; in_text=frame->in_text;clip_pending=frame->clip_pending; + fonts=frame->fonts; owns_fonts=frame->owns_fonts; + xobjects=frame->xobjects; + path_segs=frame->path_segs; path_rects=frame->path_rects; + seg_count=frame->seg_count; rect_count=frame->rect_count; + path_cx=frame->path_cx; path_cy=frame->path_cy; + path_sx=frame->path_sx; path_sy=frame->path_sy; + op_count=0; + continue; + } // Accumulate operands if (tok.type == TOK_REAL || tok.type == TOK_INT) { @@ -514,10 +890,9 @@ void parse_page(int page_idx, int page_obj_num) { } 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; + tj_count = 0; while (pos < stream_len) { Token atk; @@ -526,11 +901,22 @@ void parse_page(int page_idx, int page_obj_num) { 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) { + tj_len + atk.str_len < MAX_TEXT_LEN - 1 && + tj_count < MAX_TJ_ELEMENTS) { + TJElement* element = &tj_elements[tj_count++]; + element->is_string = true; + element->offset = tj_len; + element->len = atk.str_len; + element->adjustment = 0; montauk::memcpy(tj_buf + tj_len, atk.str, atk.str_len); tj_len += atk.str_len; + } else if ((atk.type == TOK_REAL || atk.type == TOK_INT) && + tj_count < MAX_TJ_ELEMENTS) { + TJElement* element = &tj_elements[tj_count++]; + element->is_string = false; + element->offset = element->len = 0; + element->adjustment = atk.num; } - // Number entries in TJ arrays are kerning values - skip them pos = next_pos; } tj_buf[tj_len] = '\0'; @@ -572,8 +958,25 @@ void parse_page(int page_idx, int page_obj_num) { ts.font_flags = fi ? fi->flags : 0; ts.tounicode = fi ? fi->tounicode : nullptr; ts.embedded_font = fi ? fi->embedded_font : nullptr; + ts.widths = fi ? fi->widths : nullptr; + ts.has_widths = fi ? fi->has_widths : false; + ts.two_byte = fi ? fi->two_byte : false; } } + // Tc/Tw/Tz/Ts - character spacing, word spacing, horizontal + // scaling, and text rise. + else if (op[0] == 'T' && op[1] == 'c' && op[2] == '\0' && in_text) { + if (op_count >= 1) ts.char_space = ops[0].num; + } + else if (op[0] == 'T' && op[1] == 'w' && op[2] == '\0' && in_text) { + if (op_count >= 1) ts.word_space = ops[0].num; + } + else if (op[0] == 'T' && op[1] == 'z' && op[2] == '\0' && in_text) { + if (op_count >= 1) ts.hscale = ops[0].num / 100.0f; + } + else if (op[0] == 'T' && op[1] == 's' && op[2] == '\0' && in_text) { + if (op_count >= 1) ts.rise = ops[0].num; + } // 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; @@ -619,70 +1022,30 @@ void parse_page(int page_idx, int page_obj_num) { } // 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; - - float gx, gy; - ctm_transform(gs.ctm, ts.tm[4], ts.tm[5], &gx, &gy); - add_text_item(page, gx, gy, 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]; - } + if (op_count >= 1 && ops[0].is_string) + show_text_run(page, ts, gs, ops[0].str, ops[0].str_len); } - // TJ - show string (array already concatenated) + // TJ - show strings with interleaved thousandths-of-an-em + // positioning adjustments. 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; - - float gx, gy; - ctm_transform(gs.ctm, ts.tm[4], ts.tm[5], &gx, &gy); - add_text_item(page, gx, gy, 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); + if (tj_count > 0) { + for (int ti = 0; ti < tj_count; ti++) { + TJElement* element = &tj_elements[ti]; + if (element->is_string) { + show_text_run(page, ts, gs, sop->str + element->offset, + element->len); + } else { + adjust_text_position(ts, + -element->adjustment * ts.font_size / 1000.0f); + } + } } else { - advance = sop->str_len * ts.font_size * 0.5f; + show_text_run(page, ts, gs, sop->str, sop->str_len); } - ts.tm[4] += advance * ts.tm[0]; - ts.tm[5] += advance * ts.tm[1]; } + tj_count = 0; } // ' (single quote) - move to next line and show string else if (op[0] == '\'' && op[1] == '\0' && in_text) { @@ -693,20 +1056,15 @@ void parse_page(int page_idx, int page_obj_num) { 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; - - float gx, gy; - ctm_transform(gs.ctm, ts.tm[4], ts.tm[5], &gx, &gy); - add_text_item(page, gx, gy, eff_size, - ops[0].str, ops[0].str_len, ts.font_flags, ts.tounicode, ts.embedded_font); - } + if (op_count >= 1 && ops[0].is_string) + show_text_run(page, ts, gs, ops[0].str, ops[0].str_len); } // " (double quote) - set aw, ac, move to next line, show string else if (op[0] == '"' && op[1] == '\0' && in_text) { + if (op_count >= 2) { + ts.word_space = ops[0].num; + ts.char_space = ops[1].num; + } // T* float translate[6] = {1, 0, 0, 1, 0, -ts.tl}; float result[6]; @@ -714,20 +1072,116 @@ void parse_page(int page_idx, int page_obj_num) { 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; - - float gx, gy; - ctm_transform(gs.ctm, ts.tm[4], ts.tm[5], &gx, &gy); - add_text_item(page, gx, gy, eff_size, - ops[2].str, ops[2].str_len, ts.font_flags, ts.tounicode, ts.embedded_font); - } + if (op_count >= 3 && ops[2].is_string) + show_text_run(page, ts, gs, ops[2].str, ops[2].str_len); } // ---- Graphics state operators ---- + // Do - paint an image or recursively interpret a Form XObject. + else if (op[0] == 'D' && op[1] == 'o' && op[2] == '\0') { + if (op_count >= 1 && ops[0].is_name) { + int image_obj = lookup_xobject(&xobjects, ops[0].str); + int is, ie; + if (image_obj >= 0 && find_obj_content(image_obj, &is, &ie) == 0) { + int subtype = dict_lookup(g_doc.data, g_doc.data_len, is, "Subtype"); + if (subtype >= 0 && starts_with(g_doc.data, g_doc.data_len, + subtype + 1, "Image")) { + uint32_t* pixels; int iw, ih; + if (load_pdf_image(image_obj, &pixels, &iw, &ih)) { + int mask_pos=dict_lookup(g_doc.data,g_doc.data_len,is,"ImageMask"); + bool stencil=mask_pos>=0&&starts_with(g_doc.data,g_doc.data_len,mask_pos,"true"); + add_image_item(page,pixels,iw,ih,gs.ctm,gs.clip,stencil, + gs.fill_r,gs.fill_g,gs.fill_b); + } + } else if (subtype >= 0 && starts_with(g_doc.data, g_doc.data_len, + subtype + 1, "Form") && + frame_depth < MAX_FORM_DEPTH) { + int form_len = 0; + uint8_t* form_data = get_stream_data(image_obj, &form_len); + PathSeg* form_segs = (PathSeg*)montauk::malloc(MAX_PATH_SEGS * sizeof(PathSeg)); + PathRect* form_rects = (PathRect*)montauk::malloc(MAX_PATH_RECTS * sizeof(PathRect)); + if (form_data && form_segs && form_rects) { + FontMap* form_fonts = (FontMap*)montauk::malloc(sizeof(FontMap)); + bool form_owns_fonts = false; + if (form_fonts) { + build_object_font_map(image_obj, form_fonts); + if (form_fonts->count > 0) form_owns_fonts = true; + else { montauk::mfree(form_fonts); form_fonts = fonts; } + } else form_fonts = fonts; + + XObjectMap form_xobjects; + build_form_xobject_map(image_obj, &xobjects, &form_xobjects); + + ContentFrame* frame = &frames[frame_depth++]; + frame->data=stream_data; frame->len=stream_len; frame->pos=pos; + frame->gs=gs; frame->ts=ts; frame->in_text=in_text;frame->clip_pending=clip_pending; + frame->fonts=fonts; frame->owns_fonts=owns_fonts; + frame->xobjects=xobjects; + frame->path_segs=path_segs; frame->path_rects=path_rects; + frame->seg_count=seg_count; frame->rect_count=rect_count; + frame->path_cx=path_cx; frame->path_cy=path_cy; + frame->path_sx=path_sx; frame->path_sy=path_sy; + + // A form starts with the caller's graphics state and + // concatenates its optional /Matrix. + float m[6] = {1,0,0,1,0,0}; + int matrix_pos = dict_lookup(g_doc.data, g_doc.data_len, is, "Matrix"); + if (matrix_pos >= 0) { + int mp = skip_ws(g_doc.data, g_doc.data_len, matrix_pos); + if (mp < g_doc.data_len && g_doc.data[mp] == '[') { + mp++; + for (int mi=0;mi<6;mi++) { + int next = parse_real_at(g_doc.data, g_doc.data_len, mp, &m[mi]); + if (next < 0) { m[0]=1;m[1]=0;m[2]=0;m[3]=1;m[4]=0;m[5]=0; break; } + mp=next; + } + } + } + GfxState form_gs=gs; + form_gs.ctm[0]=m[0]*gs.ctm[0]+m[1]*gs.ctm[2]; + form_gs.ctm[1]=m[0]*gs.ctm[1]+m[1]*gs.ctm[3]; + form_gs.ctm[2]=m[2]*gs.ctm[0]+m[3]*gs.ctm[2]; + form_gs.ctm[3]=m[2]*gs.ctm[1]+m[3]*gs.ctm[3]; + form_gs.ctm[4]=m[4]*gs.ctm[0]+m[5]*gs.ctm[2]+gs.ctm[4]; + form_gs.ctm[5]=m[4]*gs.ctm[1]+m[5]*gs.ctm[3]+gs.ctm[5]; + + int bbox_pos=dict_lookup(g_doc.data,g_doc.data_len,is,"BBox"); + if(bbox_pos>=0) { + int bp=skip_ws(g_doc.data,g_doc.data_len,bbox_pos); + float box[4];bool valid=bp= needed) { + float x0=path_cx, y0=path_cy, x1, y1, x2, y2, x3, y3; + if (op[0] == 'c') { + ctm_transform(gs.ctm,ops[0].num,ops[1].num,&x1,&y1); + ctm_transform(gs.ctm,ops[2].num,ops[3].num,&x2,&y2); + ctm_transform(gs.ctm,ops[4].num,ops[5].num,&x3,&y3); + } else if (op[0] == 'v') { + x1=x0; y1=y0; + ctm_transform(gs.ctm,ops[0].num,ops[1].num,&x2,&y2); + ctm_transform(gs.ctm,ops[2].num,ops[3].num,&x3,&y3); + } else { + ctm_transform(gs.ctm,ops[0].num,ops[1].num,&x1,&y1); + ctm_transform(gs.ctm,ops[2].num,ops[3].num,&x3,&y3); + x2=x3; y2=y3; + } + float px=x0, py=y0; + for (int step=1; step<=12 && seg_count= 4 && rect_count < MAX_PATH_RECTS) { @@ -876,6 +1359,11 @@ void parse_page(int page_idx, int page_obj_num) { path_cy = path_sy; } } + // W/W* - clipping path. The new clip becomes active when the + // current path is ended by n or a painting operator. + else if (op[0]=='W'&&(op[1]=='\0'||(op[1]=='*'&&op[2]=='\0'))&&!in_text) { + clip_pending=true; + } // ---- Path painting operators ---- // S - stroke else if (op[0] == 'S' && op[1] == '\0' && !in_text) { @@ -884,7 +1372,8 @@ void parse_page(int page_idx, int page_obj_num) { add_gfx_item(page, GFX_LINE, path_segs[si].x1, path_segs[si].y1, path_segs[si].x2, path_segs[si].y2, - lw, gs.stroke_r, gs.stroke_g, gs.stroke_b); + lw, gs.stroke_r, gs.stroke_g, gs.stroke_b,gs.clip); + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } @@ -900,35 +1389,32 @@ void parse_page(int page_idx, int page_obj_num) { add_gfx_item(page, GFX_LINE, path_segs[si].x1, path_segs[si].y1, path_segs[si].x2, path_segs[si].y2, - lw, gs.stroke_r, gs.stroke_g, gs.stroke_b); + lw, gs.stroke_r, gs.stroke_g, gs.stroke_b,gs.clip); + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } // f or F - fill else if ((op[0] == 'f' || op[0] == 'F') && (op[1] == '\0' || (op[1] == '*' && op[2] == '\0')) && !in_text) { - for (int ri = 0; ri < rect_count; ri++) - add_gfx_item(page, GFX_RECT_FILL, - path_rects[ri].x, path_rects[ri].y, - path_rects[ri].w, path_rects[ri].h, - 0, gs.fill_r, gs.fill_g, gs.fill_b); + paint_fill(page,path_segs,seg_count,path_rects,rect_count, + op[1]=='*',gs.fill_r,gs.fill_g,gs.fill_b,gs.clip); + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } // B or B* - fill then stroke else if (op[0] == 'B' && (op[1] == '\0' || (op[1] == '*' && op[2] == '\0')) && !in_text) { - for (int ri = 0; ri < rect_count; ri++) - add_gfx_item(page, GFX_RECT_FILL, - path_rects[ri].x, path_rects[ri].y, - path_rects[ri].w, path_rects[ri].h, - 0, gs.fill_r, gs.fill_g, gs.fill_b); + paint_fill(page,path_segs,seg_count,path_rects,rect_count, + op[1]=='*',gs.fill_r,gs.fill_g,gs.fill_b,gs.clip); float lw = gs.line_width * ctm_scale(gs.ctm); for (int si = 0; si < seg_count; si++) add_gfx_item(page, GFX_LINE, path_segs[si].x1, path_segs[si].y1, path_segs[si].x2, path_segs[si].y2, - lw, gs.stroke_r, gs.stroke_g, gs.stroke_b); + lw, gs.stroke_r, gs.stroke_g, gs.stroke_b,gs.clip); + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } @@ -938,36 +1424,83 @@ void parse_page(int page_idx, int page_obj_num) { if (seg_count < MAX_PATH_SEGS && (path_cx != path_sx || path_cy != path_sy)) path_segs[seg_count++] = {path_cx, path_cy, path_sx, path_sy}; - for (int ri = 0; ri < rect_count; ri++) - add_gfx_item(page, GFX_RECT_FILL, - path_rects[ri].x, path_rects[ri].y, - path_rects[ri].w, path_rects[ri].h, - 0, gs.fill_r, gs.fill_g, gs.fill_b); + paint_fill(page,path_segs,seg_count,path_rects,rect_count, + op[1]=='*',gs.fill_r,gs.fill_g,gs.fill_b,gs.clip); float lw = gs.line_width * ctm_scale(gs.ctm); for (int si = 0; si < seg_count; si++) add_gfx_item(page, GFX_LINE, path_segs[si].x1, path_segs[si].y1, path_segs[si].x2, path_segs[si].y2, - lw, gs.stroke_r, gs.stroke_g, gs.stroke_b); + lw, gs.stroke_r, gs.stroke_g, gs.stroke_b,gs.clip); + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } // n - end path without painting (clipping only) else if (op[0] == 'n' && op[1] == '\0' && !in_text) { + if(clip_pending){intersect_clip_with_path(&gs,path_segs,seg_count);clip_pending=false;} seg_count = 0; rect_count = 0; } op_count = 0; // reset operand stack after operator + tj_count = 0; } montauk::mfree(ops); + montauk::mfree(frames); if (path_segs) montauk::mfree(path_segs); if (path_rects) montauk::mfree(path_rects); montauk::mfree(stream_data); } +page_done: for (int fi = 0; fi < fonts->count; fi++) if (fonts->fonts[fi].tounicode) montauk::mfree(fonts->fonts[fi].tounicode); montauk::mfree(fonts); } + +void ensure_page_parsed(int page_idx) { + if (!g_doc.valid || page_idx < 0 || page_idx >= g_doc.page_count) return; + if (!g_doc.pages[page_idx].parsed) + parse_page(page_idx, g_doc.page_objs[page_idx]); +} + +void trim_page_cache(int keep_page) { + // Retain only the visible page's display list. This keeps memory bounded + // when a user reads through thousands of pages; revisiting a page simply + // reparses its content stream. + for (int i = 0; i < g_doc.page_count; i++) { + if (i == keep_page || !g_doc.pages[i].parsed) continue; + PdfPage* page = &g_doc.pages[i]; + if (page->items) montauk::mfree(page->items); + if (page->gfx_items) montauk::mfree(page->gfx_items); + if (page->fill_items) { + for(int j=0;jfill_count;j++) + if(page->fill_items[j].edges)montauk::mfree(page->fill_items[j].edges); + montauk::mfree(page->fill_items); + } + if (page->image_items) montauk::mfree(page->image_items); + if (page->paint_commands) montauk::mfree(page->paint_commands); + page->items=nullptr; page->item_count=0; page->item_cap=0; + page->gfx_items=nullptr; page->gfx_count=0; page->gfx_cap=0; + page->fill_items=nullptr; page->fill_count=0; page->fill_cap=0; + page->image_items=nullptr; page->image_count=0; page->image_cap=0; + page->paint_commands=nullptr; page->paint_count=0; page->paint_cap=0; + page->parsed=false; + } + + // Drop decoded images no longer referenced by the retained display list. + int write = 0; + for (int i = 0; i < g_doc.image_count; i++) { + bool used = false; + if (keep_page >= 0 && keep_page < g_doc.page_count) { + PdfPage* page = &g_doc.pages[keep_page]; + for (int j = 0; j < page->image_count; j++) + if (page->image_items[j].pixels == g_doc.images[i].pixels) { used=true; break; } + } + if (used) g_doc.images[write++] = g_doc.images[i]; + else if (g_doc.images[i].pixels) montauk::mfree(g_doc.images[i].pixels); + } + g_doc.image_count = write; +} diff --git a/programs/src/pdfviewer/pdf_parser.cpp b/programs/src/pdfviewer/pdf_parser.cpp index 8b9f3cd..d9f79aa 100644 --- a/programs/src/pdfviewer/pdf_parser.cpp +++ b/programs/src/pdfviewer/pdf_parser.cpp @@ -6,6 +6,11 @@ #include "pdfviewer.h" +extern "C" { +#include +#include +} + // ============================================================================ // DEFLATE Decompression (RFC 1951) // ============================================================================ @@ -155,9 +160,10 @@ static int inflate_raw(BitStream* bs, uint8_t* out, int out_cap) { bs->buf = 0; bs->buf_bits = 0; if (bs->byte_pos + 4 > bs->len) goto fail; - int len = bs->data[bs->byte_pos] | (bs->data[bs->byte_pos + 1] << 8); + int len = bs->data[bs->byte_pos] | (bs->data[bs->byte_pos + 1] << 8); bs->byte_pos += 4; // skip len and nlen - for (int i = 0; i < len && out_pos < out_cap; i++) { + if (len > out_cap - out_pos) goto fail; + for (int i = 0; i < len; i++) { if (bs->byte_pos >= bs->len) goto fail; out[out_pos++] = bs->data[bs->byte_pos++]; } @@ -274,8 +280,10 @@ static uint8_t* inflate_zlib(const uint8_t* src, int src_len, int* out_len) { bs_init(&bs, src + deflate_start, src_len - deflate_start - 4); // Try progressively larger buffers - for (int mult = 8; mult <= 64; mult *= 2) { - int cap = src_len * mult; + for (int mult = 8; mult <= 512; mult *= 2) { + uint64_t wanted = (uint64_t)src_len * mult; + if (wanted > 256ULL * 1024 * 1024) wanted = 256ULL * 1024 * 1024; + int cap = (int)wanted; if (cap < 65536) cap = 65536; uint8_t* dst = (uint8_t*)montauk::malloc(cap); if (!dst) return nullptr; @@ -287,6 +295,7 @@ static uint8_t* inflate_zlib(const uint8_t* src, int src_len, int* out_len) { return dst; } montauk::mfree(dst); + if (cap == 256 * 1024 * 1024) break; } return nullptr; } @@ -582,12 +591,15 @@ uint8_t* get_stream_data(int obj_num, int* out_len) { int fp = skip_ws(d, len, filter_pos); // Could be /FlateDecode or [/FlateDecode] if (fp < len && d[fp] == '/') { - if (starts_with(d, len, fp + 1, "FlateDecode")) is_flate = true; + if (starts_with(d, len, fp + 1, "FlateDecode") || + starts_with(d, len, fp + 1, "Fl")) is_flate = true; } else if (fp < len && d[fp] == '[') { // Array of filters - check first one fp++; fp = skip_ws(d, len, fp); - if (fp < len && d[fp] == '/' && starts_with(d, len, fp + 1, "FlateDecode")) + if (fp < len && d[fp] == '/' && + (starts_with(d, len, fp + 1, "FlateDecode") || + starts_with(d, len, fp + 1, "Fl"))) is_flate = true; } } @@ -690,7 +702,9 @@ static bool parse_xref_table(int pos) { } char type = (char)d[p + 17]; - if (type == 'n' && offset > 0) { + // Sections are parsed newest-to-oldest, so never let an older + // revision overwrite the current location of an object. + if (type == 'n' && offset > 0 && g_doc.xref[start_num + i] == 0) { g_doc.xref[start_num + i] = offset; } @@ -743,14 +757,14 @@ static bool parse_xref_stream(int pos) { } // Get /Index array (optional, defaults to [0 Size]) - int index_pairs[32]; // start, count pairs + int index_pairs[256]; // start, count pairs int index_count = 0; int idx_pos = dict_lookup(d, len, dict_start, "Index"); if (idx_pos >= 0) { int ip = skip_ws(d, len, idx_pos); if (ip < len && d[ip] == '[') { ip++; - while (index_count < 30) { + while (index_count < 254) { ip = skip_ws(d, len, ip); if (ip >= len || d[ip] == ']') break; int v; @@ -766,19 +780,42 @@ static bool parse_xref_stream(int pos) { index_count = 2; } - // Allocate xref array and type-2 tracking arrays - g_doc.xref = (int*)montauk::malloc(size * sizeof(int)); - if (!g_doc.xref) return false; - montauk::memset(g_doc.xref, 0, size * sizeof(int)); - g_doc.xref_count = size; - g_doc.xref_stm = (int*)montauk::malloc(size * sizeof(int)); - g_doc.xref_idx = (int*)montauk::malloc(size * sizeof(int)); - if (g_doc.xref_stm) montauk::memset(g_doc.xref_stm, 0, size * sizeof(int)); - if (g_doc.xref_idx) montauk::memset(g_doc.xref_idx, 0, size * sizeof(int)); + // Grow rather than replace: incremental PDFs chain several xref sections. + if (size > g_doc.xref_count) { + int old_count = g_doc.xref_count; + int* nx = (int*)montauk::malloc(size * sizeof(int)); + int* ns = (int*)montauk::malloc(size * sizeof(int)); + int* ni = (int*)montauk::malloc(size * sizeof(int)); + if (!nx || !ns || !ni) { + if (nx) montauk::mfree(nx); + if (ns) montauk::mfree(ns); + if (ni) montauk::mfree(ni); + return false; + } + montauk::memset(nx, 0, size * sizeof(int)); + montauk::memset(ns, 0, size * sizeof(int)); + montauk::memset(ni, 0, size * sizeof(int)); + if (old_count) { + montauk::memcpy(nx, g_doc.xref, old_count * sizeof(int)); + if (g_doc.xref_stm) montauk::memcpy(ns, g_doc.xref_stm, old_count * sizeof(int)); + if (g_doc.xref_idx) montauk::memcpy(ni, g_doc.xref_idx, old_count * sizeof(int)); + } + if (g_doc.xref) montauk::mfree(g_doc.xref); + if (g_doc.xref_stm) montauk::mfree(g_doc.xref_stm); + if (g_doc.xref_idx) montauk::mfree(g_doc.xref_idx); + g_doc.xref = nx; g_doc.xref_stm = ns; g_doc.xref_idx = ni; + g_doc.xref_count = size; + } else if (!g_doc.xref_stm || !g_doc.xref_idx) { + g_doc.xref_stm = (int*)montauk::malloc(size * sizeof(int)); + g_doc.xref_idx = (int*)montauk::malloc(size * sizeof(int)); + if (!g_doc.xref_stm || !g_doc.xref_idx) return false; + montauk::memset(g_doc.xref_stm, 0, size * sizeof(int)); + montauk::memset(g_doc.xref_idx, 0, size * sizeof(int)); + } // Temporarily add this xref stream object to the xref table // so get_stream_data can find it - if (obj_num < size) g_doc.xref[obj_num] = pos; + if (obj_num < size && g_doc.xref[obj_num] == 0) g_doc.xref[obj_num] = pos; // Get /Filter and /Length, then extract stream data int filter_pos = dict_lookup(d, len, dict_start, "Filter"); @@ -852,9 +889,11 @@ static bool parse_xref_stream(int pos) { int obj_idx = start_obj + i; if (obj_idx < size) { - if (type == 1 && field2 > 0) { + if (type == 1 && field2 > 0 && g_doc.xref[obj_idx] == 0 && + g_doc.xref_stm[obj_idx] == 0) { g_doc.xref[obj_idx] = field2; - } else if (type == 2 && g_doc.xref_stm && g_doc.xref_idx) { + } else if (type == 2 && g_doc.xref[obj_idx] == 0 && + g_doc.xref_stm[obj_idx] == 0) { g_doc.xref_stm[obj_idx] = field2; // containing ObjStm number g_doc.xref_idx[obj_idx] = field3; // index within stream } @@ -868,6 +907,33 @@ static bool parse_xref_stream(int pos) { return true; } +static int xref_dict_value(int pos, const char* key) { + const uint8_t* d = g_doc.data; + int len = g_doc.data_len; + int dict_start = -1; + if (starts_with(d, len, pos, "xref")) { + int p = pos + 4; + while (p + 7 < len) { + if (starts_with(d, len, p, "trailer")) { dict_start = p + 7; break; } + if (starts_with(d, len, p, "startxref")) break; + p++; + } + } else { + int obj, gen; + int p = parse_int_at(d, len, pos, &obj); + if (p > 0) p = parse_int_at(d, len, p, &gen); + if (p > 0) { + p = skip_ws(d, len, p); + if (starts_with(d, len, p, "obj")) dict_start = skip_ws(d, len, p + 3); + } + } + if (dict_start < 0) return 0; + int value_pos = dict_lookup(d, len, dict_start, key); + int value = 0; + if (value_pos >= 0) parse_int_at(d, len, value_pos, &value); + return value; +} + // ============================================================================ // Object Stream Decompression (xref type 2) // ============================================================================ @@ -876,7 +942,8 @@ static void decompress_object_streams() { if (!g_doc.xref_stm) return; // Collect unique object stream numbers - int stm_objs[256]; + int* stm_objs = (int*)montauk::malloc(g_doc.xref_count * sizeof(int)); + if (!stm_objs) return; int stm_count = 0; for (int i = 0; i < g_doc.xref_count; i++) { @@ -886,17 +953,17 @@ static void decompress_object_streams() { for (int j = 0; j < stm_count; j++) { if (stm_objs[j] == sobj) { found = true; break; } } - if (!found && stm_count < 256) + if (!found) stm_objs[stm_count++] = sobj; } - if (stm_count == 0) return; + if (stm_count == 0) { montauk::mfree(stm_objs); return; } // Build synthetic "N 0 obj\n\nendobj\n" wrappers for each // compressed object and append to g_doc.data int synth_cap = 64 * 1024; uint8_t* synth = (uint8_t*)montauk::malloc(synth_cap); - if (!synth) return; + if (!synth) { montauk::mfree(stm_objs); return; } int synth_len = 0; for (int si = 0; si < stm_count; si++) { @@ -1014,6 +1081,7 @@ static void decompress_object_streams() { } montauk::mfree(synth); + montauk::mfree(stm_objs); } // ============================================================================ @@ -1046,11 +1114,38 @@ static void add_page(int obj_num) { g_doc.pages[g_doc.page_count].gfx_items = nullptr; g_doc.pages[g_doc.page_count].gfx_count = 0; g_doc.pages[g_doc.page_count].gfx_cap = 0; + g_doc.pages[g_doc.page_count].fill_items = nullptr; + g_doc.pages[g_doc.page_count].fill_count = 0; + g_doc.pages[g_doc.page_count].fill_cap = 0; + g_doc.pages[g_doc.page_count].image_items = nullptr; + g_doc.pages[g_doc.page_count].image_count = 0; + g_doc.pages[g_doc.page_count].image_cap = 0; + g_doc.pages[g_doc.page_count].paint_commands = nullptr; + g_doc.pages[g_doc.page_count].paint_count = 0; + g_doc.pages[g_doc.page_count].paint_cap = 0; g_doc.pages[g_doc.page_count].width = 612; g_doc.pages[g_doc.page_count].height = 792; + g_doc.pages[g_doc.page_count].parsed = false; g_doc.page_count++; } +// Return the position of a page attribute, following the /Parent chain. PDF +// page resources and boxes are inheritable and are commonly stored only once +// at the root of a large page tree. +int find_inherited_page_value(int page_obj_num, const char* key) { + int obj = page_obj_num; + for (int depth = 0; depth < 64; depth++) { + int start, end; + if (find_obj_content(obj, &start, &end) < 0) return -1; + int value = dict_lookup(g_doc.data, g_doc.data_len, start, key); + if (value >= 0) return value; + int parent = dict_lookup(g_doc.data, g_doc.data_len, start, "Parent"); + if (parent < 0 || parse_ref_at(g_doc.data, g_doc.data_len, parent, &obj) < 0) + return -1; + } + return -1; +} + static void collect_pages(int obj_num, int depth) { if (depth > 20) return; // prevent infinite recursion int start, end; @@ -1069,7 +1164,7 @@ static void collect_pages(int obj_num, int depth) { add_page(obj_num); // Get MediaBox - int mb_pos = dict_lookup(d, len, start, "MediaBox"); + int mb_pos = find_inherited_page_value(obj_num, "MediaBox"); if (mb_pos >= 0) { int mp = skip_ws(d, len, mb_pos); // Could be a reference @@ -1139,6 +1234,23 @@ static bool str_contains(const char* haystack, const char* needle) { // Parse a ToUnicode CMap stream and build a glyph->Unicode mapping table. // Returns heap-allocated array of 256 uint16_t entries, or nullptr on failure. +static bool parse_cmap_hex(const uint8_t* data, int len, int* position, int* value) { + int p=*position; + while(p=len||data[p]!='<')return false; + p++;int result=0; + while(p') { + int digit=-1; + if(data[p]>='0'&&data[p]<='9')digit=data[p]-'0'; + else if(data[p]>='a'&&data[p]<='f')digit=data[p]-'a'+10; + else if(data[p]>='A'&&data[p]<='F')digit=data[p]-'A'+10; + if(digit>=0)result=(result<<4)|digit; + p++; + } + if(p>=len)return false; + *position=p+1;*value=result;return true; +} + static uint16_t* parse_tounicode(int cmap_obj_num) { int stream_len; uint8_t* cmap_data = get_stream_data(cmap_obj_num, &stream_len); @@ -1209,28 +1321,29 @@ static uint16_t* parse_tounicode(int cmap_obj_num) { p++; if (starts_with(cmap_data, stream_len, p, "endbfrange")) break; - // Parse - int vals[3] = {}; - for (int vi = 0; vi < 3; vi++) { - if (p >= stream_len || cmap_data[p] != '<') goto done_range; + int first,end; + if(!parse_cmap_hex(cmap_data,stream_len,&p,&first)|| + !parse_cmap_hex(cmap_data,stream_len,&p,&end))break; + while(p=0)table[code]=(uint16_t)(destination+code-first); + } else if(p= '0' && cmap_data[p] <= '9') v = cmap_data[p] - '0'; - else if (cmap_data[p] >= 'a' && cmap_data[p] <= 'f') v = cmap_data[p] - 'a' + 10; - else if (cmap_data[p] >= 'A' && cmap_data[p] <= 'F') v = cmap_data[p] - 'A' + 10; - if (v >= 0) vals[vi] = (vals[vi] << 4) | v; - p++; + for(int code=first;code<=end;code++) { + int destination; + if(!parse_cmap_hex(cmap_data,stream_len,&p,&destination))break; + if(code>=0&&code<256)table[code]=(uint16_t)destination; } - if (p < stream_len) p++; - while (p < stream_len && (cmap_data[p] == ' ' || cmap_data[p] == '\n' || - cmap_data[p] == '\r' || cmap_data[p] == '\t')) - p++; + while(p= 0 && parse_ref_at(d, len, file_pos, &file_obj) > 0) + return load_embedded_font(file_obj); + file_pos = dict_lookup(d, len, ds, "FontFile3"); + if (file_pos >= 0 && parse_ref_at(d, len, file_pos, &file_obj) > 0) + return load_embedded_font(file_obj); + return nullptr; +} + +static void parse_cid_widths(int font_start, FontInfo* font) { + const uint8_t* d = g_doc.data; + int len = g_doc.data_len; + int default_width = 1000; + int default_pos = dict_lookup(d, len, font_start, "DW"); + if (default_pos >= 0) parse_int_at(d, len, default_pos, &default_width); + for (int i = 0; i < 256; i++) font->widths[i] = (float)default_width; + font->has_widths = true; + + int widths_pos = dict_lookup(d, len, font_start, "W"); + if (widths_pos < 0) return; + int p = skip_ws(d, len, widths_pos); + if (p >= len || d[p] != '[') return; + p++; + while (p < len) { + p = skip_ws(d, len, p); + if (p >= len || d[p] == ']') break; + int first; + int next = parse_int_at(d, len, p, &first); + if (next < 0) break; + p = skip_ws(d, len, next); + if (p < len && d[p] == '[') { + p++; + int code = first; + while (p < len) { + p = skip_ws(d, len, p); + if (p >= len || d[p] == ']') { if (p < len) p++; break; } + float width; + next = parse_real_at(d, len, p, &width); + if (next < 0) return; + if (code >= 0 && code < 256) font->widths[code] = width; + code++; + p = next; + } + } else { + int last; + next = parse_int_at(d, len, p, &last); + if (next < 0) break; + float width; + next = parse_real_at(d, len, next, &width); + if (next < 0) break; + if (first < 0) first = 0; + if (last > 255) last = 255; + for (int code = first; code <= last; code++) font->widths[code] = width; + p = next; + } + } +} + +static void build_font_map_impl(int page_obj_num, bool inherit_page_resources, FontMap* out) { out->count = 0; int start, end; @@ -1305,20 +1487,9 @@ void build_font_map(int page_obj_num, FontMap* out) { int len = g_doc.data_len; // Find /Resources - could be inline or a reference - int res_pos = dict_lookup(d, len, start, "Resources"); - if (res_pos < 0) { - // Try parent /Pages for inherited resources - int parent_pos = dict_lookup(d, len, start, "Parent"); - if (parent_pos >= 0) { - int parent_num; - if (parse_ref_at(d, len, parent_pos, &parent_num) > 0) { - int ps, pe; - if (find_obj_content(parent_num, &ps, &pe) == 0) { - res_pos = dict_lookup(d, len, ps, "Resources"); - } - } - } - } + int res_pos = inherit_page_resources + ? find_inherited_page_value(page_obj_num, "Resources") + : dict_lookup(d, len, start, "Resources"); if (res_pos < 0) return; // Resolve if it's a reference @@ -1356,7 +1527,7 @@ void build_font_map(int page_obj_num, FontMap* out) { if (fdp >= len || d[fdp] != '<') return; fdp += 2; // skip << - while (fdp < len && out->count < 32) { + while (fdp < len && out->count < 64) { fdp = skip_ws(d, len, fdp); if (fdp >= len) break; if (d[fdp] == '>' && fdp + 1 < len && d[fdp + 1] == '>') break; @@ -1375,6 +1546,10 @@ void build_font_map(int page_obj_num, FontMap* out) { fi->name[ni] = '\0'; fi->flags = 0; fi->tounicode = nullptr; + fi->embedded_font = nullptr; + fi->has_widths = false; + fi->two_byte = false; + for (int wi = 0; wi < 256; wi++) fi->widths[wi] = 0; // Read font reference fdp = skip_ws(d, len, fdp); @@ -1417,8 +1592,43 @@ void build_font_map(int page_obj_num, FontMap* out) { } } + // Simple fonts describe text advances independently of the + // embedded TrueType program. Keeping these values avoids + // cumulative rounding gaps when a producer splits a word + // across several positioned text runs. + int first_char = 0; + int first_pos = dict_lookup(d, len, fs2, "FirstChar"); + if (first_pos >= 0) parse_int_at(d, len, first_pos, &first_char); + int widths_pos = dict_lookup(d, len, fs2, "Widths"); + if (widths_pos >= 0) { + int wp = skip_ws(d, len, widths_pos); + int widths_obj; + if (parse_ref_at(d, len, wp, &widths_obj) > 0) { + int ws, we; + if (find_obj_content(widths_obj, &ws, &we) == 0) + wp = skip_ws(d, len, ws); + } + if (wp < len && d[wp] == '[') { + wp++; + for (int code = first_char; code < 256; code++) { + wp = skip_ws(d, len, wp); + if (wp >= len || d[wp] == ']') break; + float width; + int next = parse_real_at(d, len, wp, &width); + if (next < 0) break; + fi->widths[code] = width; + fi->has_widths = true; + wp = next; + } + } + } + + int encoding_pos = dict_lookup(d, len, fs2, "Encoding"); + if (encoding_pos >= 0 && d[encoding_pos] == '/' && + starts_with(d, len, encoding_pos + 1, "Identity-H")) + fi->two_byte = true; + // Try to load embedded font from FontDescriptor - fi->embedded_font = nullptr; int fdesc_pos = dict_lookup(d, len, fs2, "FontDescriptor"); if (fdesc_pos >= 0) { int fdesc_num; @@ -1444,6 +1654,23 @@ void build_font_map(int page_obj_num, FontMap* out) { } } } + + // Type0 fonts keep metrics and the embedded program in their + // CIDFont descendant rather than in the outer font object. + int descendants_pos = dict_lookup(d, len, fs2, "DescendantFonts"); + if (descendants_pos >= 0) { + int dp = skip_ws(d, len, descendants_pos); + if (dp < len && d[dp] == '[') dp = skip_ws(d, len, dp + 1); + int descendant_obj; + if (parse_ref_at(d, len, dp, &descendant_obj) > 0) { + int cds, cde; + if (find_obj_content(descendant_obj, &cds, &cde) == 0) { + parse_cid_widths(cds, fi); + if (!fi->embedded_font) + fi->embedded_font = load_font_from_descriptor(cds); + } + } + } } } else { // Inline font dict or unknown - skip value @@ -1454,6 +1681,259 @@ void build_font_map(int page_obj_num, FontMap* out) { } } +void build_font_map(int page_obj_num, FontMap* out) { + build_font_map_impl(page_obj_num, true, out); +} + +void build_object_font_map(int obj_num, FontMap* out) { + build_font_map_impl(obj_num, false, out); +} + +static int image_int_value(int start, const char* key, int fallback) { + int p = dict_lookup(g_doc.data, g_doc.data_len, start, key); + int value = fallback; + if (p >= 0) parse_int_at(g_doc.data, g_doc.data_len, p, &value); + return value; +} + +static void undo_png_predictor(uint8_t* data, int len, int row_bytes, int bpp) { + if (row_bytes <= 0 || bpp <= 0) return; + int stride = row_bytes + 1; + int rows = len / stride; + for (int y = 0; y < rows; y++) { + uint8_t filter = data[y * stride]; + uint8_t* row = data + y * stride + 1; + uint8_t* prev = y ? data + (y - 1) * stride + 1 : nullptr; + for (int x = 0; x < row_bytes; x++) { + int a = x >= bpp ? row[x - bpp] : 0; + int b = prev ? prev[x] : 0; + int c = (prev && x >= bpp) ? prev[x - bpp] : 0; + if (filter == 1) row[x] = (uint8_t)(row[x] + a); + else if (filter == 2) row[x] = (uint8_t)(row[x] + b); + else if (filter == 3) row[x] = (uint8_t)(row[x] + ((a + b) >> 1)); + else if (filter == 4) { + int p = a + b - c, pa = p - a, pb = p - b, pc = p - c; + if (pa < 0) pa = -pa; + if (pb < 0) pb = -pb; + if (pc < 0) pc = -pc; + row[x] = (uint8_t)(row[x] + (pa <= pb && pa <= pc ? a : (pb <= pc ? b : c))); + } + } + } + // Remove the per-row filter bytes in place. + for (int y = 0; y < rows; y++) { + uint8_t* dst = data + y * row_bytes; + uint8_t* src = data + y * stride + 1; + for (int x = 0; x < row_bytes; x++) dst[x] = src[x]; + } +} + +static int image_mask_stack[16]; +static int image_mask_depth = 0; + +static int skip_name_token(const uint8_t* d,int len,int p) { + p=skip_ws(d,len,p);if(p0) { + int n=0;uint8_t* stream=get_stream_data(obj,&n); + if(!stream)return 0; + if(n>cap)n=cap; + montauk::memcpy(out,stream,n); + montauk::mfree(stream); + return n; + } + int count=0; + if(p='0'&&d[p]<='9')v=d[p]-'0';else if(d[p]>='A'&&d[p]<='F')v=d[p]-'A'+10;else if(d[p]>='a'&&d[p]<='f')v=d[p]-'a'+10;p++;if(v<0)continue;if(high<0)high=v;else{out[count++]=(uint8_t)((high<<4)|v);high=-1;}} + if(high>=0&&count='0'&&c<='7'){int v=c-'0';for(int k=0;k<2&&p='0'&&d[p]<='7';k++)v=v*8+d[p++]-'0';c=(uint8_t)v;}else if(c=='n')c='\n';else if(c=='r')c='\r';else if(c=='t')c='\t';out[count++]=c;}else if(c=='('){depth++;out[count++]=c;}else if(c==')'){depth--;if(depth)out[count++]=c;}else out[count++]=c;} + } + return count; +} + +static bool read_indexed_palette(int cs,uint32_t* palette,int* palette_count) { + const uint8_t* d=g_doc.data;int len=g_doc.data_len;int ref,s,e; + cs=skip_ws(d,len,cs); + if(parse_ref_at(d,len,cs,&ref)>0){if(find_obj_content(ref,&s,&e)<0)return false;cs=skip_ws(d,len,s);} + if(cs>=len||d[cs]!='[')return false; + int p=skip_ws(d,len,cs+1); + if(p>=len||d[p]!='/'|| + !(starts_with(d,len,p+1,"Indexed")|| + (p+2=len||d[p]!='/')return false; + int components=3; + if(starts_with(d,len,p+1,"DeviceGray"))components=1; + else if(starts_with(d,len,p+1,"DeviceCMYK"))components=4; + p=skip_name_token(d,len,p); + int high=0;p=parse_int_at(d,len,p,&high);if(p<0||high<0)return false;if(high>255)high=255; + uint8_t bytes[1024];int got=read_lookup_bytes(p,bytes,sizeof(bytes)); + if(got<(high+1)*components)return false; + for(int i=0;i<=high;i++) { + uint8_t r,g,b; + if(components==1)r=g=b=bytes[i]; + else if(components==3){r=bytes[i*3];g=bytes[i*3+1];b=bytes[i*3+2];} + else{int c=bytes[i*4],m=bytes[i*4+1],y=bytes[i*4+2],k=bytes[i*4+3];r=(uint8_t)(255-((c+k)>255?255:c+k));g=(uint8_t)(255-((m+k)>255?255:m+k));b=(uint8_t)(255-((y+k)>255?255:y+k));} + palette[i]=0xFF000000u|((uint32_t)r<<16)|((uint32_t)g<<8)|b; + } + *palette_count=high+1;return true; +} + +static int packed_sample(const uint8_t* row,int sample,int bits) { + int bit=sample*bits,shift=8-bits-(bit&7); + return (row[bit>>3]>>shift)&((1<=0&&starts_with(g_doc.data,g_doc.data_len,image_mask_pos,"true"); + int bits = image_int_value(start, "BitsPerComponent", image_mask?1:8); + if (width <= 0 || height <= 0 || width > 16384 || height > 16384 || + (bits != 1 && bits != 2 && bits != 4 && bits != 8)) + return false; + + int filter = dict_lookup(g_doc.data, g_doc.data_len, start, "Filter"); + bool jpeg = filter >= 0 && (starts_with(g_doc.data, g_doc.data_len, filter + 1, "DCTDecode") || + starts_with(g_doc.data, g_doc.data_len, filter + 1, "DCT") || + (g_doc.data[filter] == '[' && starts_with(g_doc.data, g_doc.data_len, + skip_ws(g_doc.data, g_doc.data_len, filter + 1) + 1, "DCTDecode"))); + + int data_len = 0; + uint8_t* data = get_stream_data(obj_num, &data_len); + if (!data) return false; + uint32_t* pixels = nullptr; + + if (jpeg) { + int jw = 0, jh = 0, channels = 0; + unsigned char* rgba = stbi_load_from_memory(data, data_len, &jw, &jh, &channels, 4); + if (rgba && jw > 0 && jh > 0 && jw <= 16384 && jh <= 16384) { + width = jw; height = jh; + pixels = (uint32_t*)montauk::malloc((uint64_t)width * height * sizeof(uint32_t)); + if (pixels) for (int i = 0; i < width * height; i++) + pixels[i] = 0xFF000000u | ((uint32_t)rgba[i*4] << 16) | + ((uint32_t)rgba[i*4+1] << 8) | rgba[i*4+2]; + } + if (rgba) free(rgba); + } else { + int cs = dict_lookup(g_doc.data, g_doc.data_len, start, "ColorSpace"); + int resolved_cs=cs,cs_ref,cs_start,cs_end; + if(cs>=0&&parse_ref_at(g_doc.data,g_doc.data_len,cs,&cs_ref)>0&& + find_obj_content(cs_ref,&cs_start,&cs_end)==0)resolved_cs=cs_start; + int components = 3; + uint32_t palette[256];int palette_count=0; + bool indexed=cs>=0&&read_indexed_palette(cs,palette,&palette_count); + if(indexed||image_mask)components=1; + if (resolved_cs >= 0 && starts_with(g_doc.data, g_doc.data_len, resolved_cs + 1, "DeviceGray")) components = 1; + else if (resolved_cs >= 0 && starts_with(g_doc.data, g_doc.data_len, resolved_cs + 1, "DeviceCMYK")) components = 4; + int row_bytes = (width * components * bits + 7) / 8; + int predictor_pos = dict_lookup(g_doc.data, g_doc.data_len, start, "DecodeParms"); + if (predictor_pos >= 0) { + int params_ref, ps, pe; + if (parse_ref_at(g_doc.data, g_doc.data_len, predictor_pos, ¶ms_ref) > 0 && + find_obj_content(params_ref, &ps, &pe) == 0) predictor_pos = ps; + int predictor = image_int_value(predictor_pos, "Predictor", 1); + int bpp=(components*bits+7)/8;if(bpp<1)bpp=1; + if (predictor >= 10) undo_png_predictor(data, data_len, row_bytes, bpp); + } + if ((uint64_t)row_bytes * height <= (uint64_t)data_len) { + pixels = (uint32_t*)montauk::malloc((uint64_t)width * height * sizeof(uint32_t)); + int sample_max=(1<=0){int dp=skip_ws(g_doc.data,g_doc.data_len,decode_pos);if(dp0&&parse_real_at(g_doc.data,g_doc.data_len,next,&last)>0)invert=first>last;}} + if (pixels) for (int y_pos=0;y_pos255?255:c+k)); + g=(uint8_t)(255 - ((m+k)>255?255:m+k)); + b=(uint8_t)(255 - ((y+k)>255?255:y+k)); + } + pixels[y_pos*width+x_pos] = 0xFF000000u | ((uint32_t)r<<16) | ((uint32_t)g<<8) | b; + } + } + } + montauk::mfree(data); + if (!pixels) return false; + + // Apply a soft mask when present. Masks may have a different resolution; + // sample them proportionally into the base image. + int smask_pos = dict_lookup(g_doc.data, g_doc.data_len, start, "SMask"); + int smask_obj = -1; + if (smask_pos >= 0 && parse_ref_at(g_doc.data, g_doc.data_len, smask_pos, &smask_obj) > 0 && + smask_obj != obj_num) { + bool cycle = image_mask_depth >= 16; + for (int i=0;i>16)&255; + pixels[y*width+x]=(pixels[y*width+x]&0x00FFFFFFu)|(alpha<<24); + } + } + } + + if (g_doc.image_count >= g_doc.image_cap) { + int cap = g_doc.image_cap ? g_doc.image_cap * 2 : 16; + PdfImageCacheEntry* entries = (PdfImageCacheEntry*)montauk::malloc(cap * sizeof(PdfImageCacheEntry)); + if (!entries) { montauk::mfree(pixels); return false; } + if (g_doc.images) { + montauk::memcpy(entries, g_doc.images, g_doc.image_count * sizeof(PdfImageCacheEntry)); + montauk::mfree(g_doc.images); + } + g_doc.images = entries; g_doc.image_cap = cap; + } + g_doc.images[g_doc.image_count++] = {obj_num, pixels, width, height}; + *out_pixels = pixels; *out_width = width; *out_height = height; + return true; +} + // ============================================================================ // Top-Level Load / Free // ============================================================================ @@ -1468,7 +1948,7 @@ bool load_pdf(const char* path) { } uint64_t fsize = montauk::getsize(fd); - if (fsize < 32 || fsize > 64 * 1024 * 1024) { + if (fsize < 32 || fsize > 512ULL * 1024 * 1024) { montauk::close(fd); str_cpy(g_status_msg, "File too small or too large", 128); return false; @@ -1480,8 +1960,20 @@ bool load_pdf(const char* path) { str_cpy(g_status_msg, "Out of memory", 128); return false; } - montauk::read(fd, g_doc.data, 0, fsize); + uint64_t read_off = 0; + while (read_off < fsize) { + uint64_t chunk = fsize - read_off; + if (chunk > 1024 * 1024) chunk = 1024 * 1024; + int got = montauk::read(fd, g_doc.data + read_off, read_off, chunk); + if (got <= 0) break; + read_off += (uint64_t)got; + } montauk::close(fd); + if (read_off != fsize) { + str_cpy(g_status_msg, "Could not read complete PDF", 128); + free_pdf(); + return false; + } g_doc.data_len = (int)fsize; // Check PDF header @@ -1517,12 +2009,22 @@ bool load_pdf(const char* path) { return false; } - // Parse xref (traditional table or cross-reference stream) - bool xref_ok = false; - if (starts_with(g_doc.data, g_doc.data_len, xref_off, "xref")) { - xref_ok = parse_xref_table(xref_off); - } else { - xref_ok = parse_xref_stream(xref_off); + // Parse newest-to-oldest so incremental updates retain all unchanged + // objects. Also honor /XRefStm in hybrid-reference files. + bool xref_ok = true; + int section = xref_off; + for (int revision = 0; section > 0 && revision < 64; revision++) { + bool table = starts_with(g_doc.data, g_doc.data_len, section, "xref"); + bool ok = table ? parse_xref_table(section) : parse_xref_stream(section); + if (!ok) { xref_ok = false; break; } + if (table) { + int hybrid = xref_dict_value(section, "XRefStm"); + if (hybrid > 0 && hybrid < g_doc.data_len && + !parse_xref_stream(hybrid)) { xref_ok = false; break; } + } + int prev = xref_dict_value(section, "Prev"); + if (prev <= 0 || prev >= g_doc.data_len || prev == section) break; + section = prev; } if (!xref_ok || g_doc.xref_count == 0) { @@ -1602,12 +2104,9 @@ bool load_pdf(const char* path) { return false; } - // Parse content for each page - for (int i = 0; i < g_doc.page_count; i++) { - parse_page(i, g_doc.page_objs[i]); - } - g_doc.valid = true; + ensure_page_parsed(0); + trim_page_cache(0); snprintf(g_status_msg, 128, "%d page%s loaded", g_doc.page_count, g_doc.page_count == 1 ? "" : "s"); return true; @@ -1622,6 +2121,14 @@ void free_pdf() { for (int i = 0; i < g_doc.page_count; i++) { if (g_doc.pages[i].items) montauk::mfree(g_doc.pages[i].items); if (g_doc.pages[i].gfx_items) montauk::mfree(g_doc.pages[i].gfx_items); + if (g_doc.pages[i].fill_items) { + for (int j=0;j PDF page coordinates + bool stencil; + uint8_t stencil_r, stencil_g, stencil_b; +}; + struct PdfPage { TextItem* items; int item_count; @@ -84,7 +114,23 @@ struct PdfPage { GraphicsItem* gfx_items; int gfx_count; int gfx_cap; + FilledPathItem* fill_items; + int fill_count; + int fill_cap; + ImageItem* image_items; + int image_count; + int image_cap; + PaintCommand* paint_commands; + int paint_count; + int paint_cap; float width, height; // page dimensions in points (from MediaBox) + bool parsed; // content is decoded on demand for large documents +}; + +struct PdfImageCacheEntry { + int obj_num; + uint32_t* pixels; + int width, height; }; struct EmbeddedFontEntry { @@ -110,6 +156,10 @@ struct PdfDoc { EmbeddedFontEntry* emb_fonts; // cached embedded fonts int emb_font_count; + PdfImageCacheEntry* images; // decoded image XObjects, shared across pages + int image_count; + int image_cap; + bool valid; }; @@ -118,10 +168,13 @@ struct FontInfo { uint8_t flags; // bit 0: bold, bit 1: italic, bit 2: mono uint16_t* tounicode; // glyph ID -> Unicode codepoint (256 entries), heap-allocated TrueTypeFont* embedded_font; // loaded from PDF font stream, or nullptr + float widths[256]; // glyph widths in thousandths of a text-space unit + bool has_widths; + bool two_byte; // composite Identity-H character codes }; struct FontMap { - FontInfo fonts[32]; + FontInfo fonts[64]; int count; }; @@ -181,12 +234,17 @@ int parse_ref_at(const uint8_t* d, int len, int p, int* obj_num); int find_obj_content(int obj_num, int* start, int* end); uint8_t* get_stream_data(int obj_num, int* out_len); void build_font_map(int page_obj_num, FontMap* out); +void build_object_font_map(int obj_num, FontMap* out); +int find_inherited_page_value(int page_obj_num, const char* key); +bool load_pdf_image(int obj_num, uint32_t** pixels, int* width, int* height); // ============================================================================ // pdf_page.cpp // ============================================================================ void parse_page(int page_idx, int page_obj_num); +void ensure_page_parsed(int page_idx); +void trim_page_cache(int keep_page); // ============================================================================ // render.cpp diff --git a/programs/src/pdfviewer/render.cpp b/programs/src/pdfviewer/render.cpp index 9d1fd19..9b373d1 100644 --- a/programs/src/pdfviewer/render.cpp +++ b/programs/src/pdfviewer/render.cpp @@ -6,6 +6,143 @@ #include "pdfviewer.h" +static int line_outcode(float x,float y,int x0,int y0,int x1,int y1) { + int code=0;if(x=x1)code|=2; + if(y=y1)code|=8;return code; +} + +static void draw_clipped_line(uint32_t* pixels,int x0,int y0,int x1,int y1, + int thick,Color color,int cx0,int cy0,int cx1,int cy1) { + float ax=x0,ay=y0,bx=x1,by=y1; + for(int guard=0;guard<12;guard++) { + int a=line_outcode(ax,ay,cx0,cy0,cx1,cy1),b=line_outcode(bx,by,cx0,cy0,cx1,cy1); + if(!(a|b)){px_line(pixels,g_win_w,g_win_h,(int)ax,(int)ay,(int)bx,(int)by,thick,color);return;} + if(a&b)return; + int out=a?a:b;float x=0,y=0; + if(out&4){y=(float)cy0;x=ax+(bx-ax)*(y-ay)/(by-ay);} + else if(out&8){y=(float)(cy1-1);x=ax+(bx-ax)*(y-ay)/(by-ay);} + else if(out&2){x=(float)(cx1-1);y=ay+(by-ay)*(x-ax)/(bx-ax);} + else {x=(float)cx0;y=ay+(by-ay)*(x-ax)/(bx-ax);} + if(out==a){ax=x;ay=y;}else{bx=x;by=y;} + } +} + +static int floor_pixel(float value) { + int result = (int)value; + if ((float)result > value) result--; + return result; +} + +static int ceil_pixel(float value) { + int result = (int)value; + if ((float)result < value) result++; + return result; +} + +static void blend_glyph_pixel(uint32_t* dst, uint8_t alpha, const Color& color) { + if (!alpha) return; + if (alpha == 255) { + *dst = 0xFF000000u | ((uint32_t)color.r << 16) | + ((uint32_t)color.g << 8) | color.b; + return; + } + uint32_t old = *dst, inv = 255 - alpha; + uint32_t r = (alpha * color.r + inv * ((old >> 16) & 255) + 127) / 255; + uint32_t g = (alpha * color.g + inv * ((old >> 8) & 255) + 127) / 255; + uint32_t b = (alpha * color.b + inv * (old & 255) + 127) / 255; + *dst = 0xFF000000u | (r << 16) | (g << 8) | b; +} + +static void draw_affine_text(uint32_t* pixels, const TextItem& item, + TrueTypeFont* font, const char* text, + const float* advances, int text_len, + int base_x, int base_y, int pixel_size, + int clip_x0, int clip_y0, int clip_x1, int clip_y1) { + GlyphCache* cache = font->get_cache(pixel_size); + if (!cache) return; + Color color = Color::from_rgb(item.r, item.g, item.b); + + // The common untransformed case should use the cached glyph bitmap + // verbatim. Resampling it merely to reconcile PDF advances with rounded + // cache advances softens stems, especially at fractional zoom levels. + float bx0=item.basis[0]-1,bx1=item.basis[1]; + float by0=item.basis[2],by1=item.basis[3]-1; + if(bx0>-0.0001f&&bx0<0.0001f&&bx1>-0.0001f&&bx1<0.0001f&& + by0>-0.0001f&&by0<0.0001f&&by1>-0.0001f&&by1<0.0001f) { + float pen=0; + for(int i=0;iget_glyph(cache,(uint8_t)text[i]); + if(!glyph)continue; + if(glyph->bitmap) { + int gx=base_x+floor_pixel(pen*g_zoom+0.5f)+glyph->xoff; + int gy=base_y+glyph->yoff; + for(int row=0;rowheight;row++) { + int y=gy+row; + if(y=clip_y1)continue; + for(int col=0;colwidth;col++) { + int x=gx+col; + if(x=clip_x1)continue; + uint8_t alpha=glyph->bitmap[row*glyph->width+col]; + if(alpha)blend_glyph_pixel(&pixels[y*g_win_w+x],alpha,color); + } + } + } + pen+=advances?advances[i]:glyph->advance/g_zoom; + } + return; + } + + float a = item.basis[0]; + float b = -item.basis[2]; + float c = -item.basis[1]; + float d = item.basis[3]; + float determinant = a * d - b * c; + if (determinant > -0.000001f && determinant < 0.000001f) return; + + float pen = 0; + for (int i = 0; i < text_len; i++) { + CachedGlyph* glyph = font->get_glyph(cache, (uint8_t)text[i]); + if (!glyph) continue; + if (glyph->bitmap) { + float gx0 = (float)(pen + glyph->xoff); + float gy0 = (float)glyph->yoff; + float gx1 = gx0 + glyph->width; + float gy1 = gy0 + glyph->height; + float xs[4] = {a*gx0+b*gy0, a*gx1+b*gy0, + a*gx0+b*gy1, a*gx1+b*gy1}; + float ys[4] = {c*gx0+d*gy0, c*gx1+d*gy0, + c*gx0+d*gy1, c*gx1+d*gy1}; + float min_x=xs[0],max_x=xs[0],min_y=ys[0],max_y=ys[0]; + for(int corner=1;corner<4;corner++) { + if(xs[corner]max_x)max_x=xs[corner]; + if(ys[corner]max_y)max_y=ys[corner]; + } + int x0=base_x+floor_pixel(min_x),x1=base_x+ceil_pixel(max_x); + int y0=base_y+floor_pixel(min_y),y1=base_y+ceil_pixel(max_y); + if(x0clip_x1)x1=clip_x1; + if(y0clip_y1)y1=clip_y1; + for(int y=y0;y=0&&colwidth&&row>=0&&rowheight) { + uint8_t alpha=glyph->bitmap[row*glyph->width+col]; + if(alpha)blend_glyph_pixel(&pixels[y*g_win_w+x],alpha,color); + } + } + } + } + pen += (advances?advances[i]:glyph->advance/g_zoom)*g_zoom; + } +} + void render(uint32_t* pixels) { // ---- Background ---- px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR); @@ -48,22 +185,137 @@ void render(uint32_t* pixels) { pixels[row * g_win_w + col] = white; } - // Draw graphics items (lines, filled rectangles) - for (int i = 0; i < page->gfx_count; i++) { - GraphicsItem* gi = &page->gfx_items[i]; + // Replay the page in original PDF painter order. Each command also + // snapshots the active clip at the point where it was emitted. + for(int ci=0;cipaint_count;ci++) { + PaintCommand* command=&page->paint_commands[ci]; + int cmd_x0=page_x+(int)(command->clip_x0*g_zoom); + int cmd_x1=page_x+(int)(command->clip_x1*g_zoom+1); + int cmd_y0=page_y+(int)((page->height-command->clip_y1)*g_zoom); + int cmd_y1=page_y+(int)((page->height-command->clip_y0)*g_zoom+1); + if(cmd_x0<0)cmd_x0=0; + if(cmd_x1>g_win_w)cmd_x1=g_win_w; + if(cmd_y0clip_y1)cmd_y1=clip_y1; + if(cmd_x0>=cmd_x1||cmd_y0>=cmd_y1)continue; + + if(command->type==PAINT_IMAGE) { + ImageItem* image = &page->image_items[command->index]; + const float* m = image->matrix; + float xs[4] = {m[4], m[0]+m[4], m[2]+m[4], m[0]+m[2]+m[4]}; + float ys[4] = {m[5], m[1]+m[5], m[3]+m[5], m[1]+m[3]+m[5]}; + float minx=xs[0], maxx=xs[0], miny=ys[0], maxy=ys[0]; + for (int c=1;c<4;c++) { if(xs[c]maxx)maxx=xs[c]; if(ys[c]maxy)maxy=ys[c]; } + int x0=page_x+(int)(minx*g_zoom), x1=page_x+(int)(maxx*g_zoom+1); + int y0=page_y+(int)((page->height-maxy)*g_zoom), y1=page_y+(int)((page->height-miny)*g_zoom+1); + if(x0cmd_x1)x1=cmd_x1; + if(y0cmd_y1)y1=cmd_y1; + float det=m[0]*m[3]-m[1]*m[2]; + if (det > -0.000001f && det < 0.000001f) continue; + for(int sy=y0;syheight-(sy-page_y+0.5f)/g_zoom; + float dx=px-m[4],dy=py-m[5]; + float u=(m[3]*dx-m[2]*dy)/det; + float v=(-m[1]*dx+m[0]*dy)/det; + if(u>=0 && u<1 && v>=0 && v<1) { + int ix=(int)(u*image->width); + int iy=(int)((1.0f-v)*image->height); + if(iy>=image->height)iy=image->height-1; + uint32_t src=image->pixels[iy*image->width+ix]; + if(image->stencil)src=(src&0xFF000000u)|((uint32_t)image->stencil_r<<16)| + ((uint32_t)image->stencil_g<<8)|image->stencil_b; + uint32_t alpha=src>>24; + if(alpha==255) pixels[sy*g_win_w+sx]=src; + else if(alpha) { + uint32_t dst=pixels[sy*g_win_w+sx], inv=255-alpha; + uint32_t r=(((src>>16)&255)*alpha+((dst>>16)&255)*inv+127)/255; + uint32_t g=(((src>>8)&255)*alpha+((dst>>8)&255)*inv+127)/255; + uint32_t b=((src&255)*alpha+(dst&255)*inv+127)/255; + pixels[sy*g_win_w+sx]=0xFF000000u|(r<<16)|(g<<8)|b; + } + } + } + } else if(command->type==PAINT_FILL) { + FilledPathItem* path=&page->fill_items[command->index]; + float* hits=(float*)montauk::malloc(path->edge_count*sizeof(float)); + int* winds=(int*)montauk::malloc(path->edge_count*sizeof(int)); + if(!hits||!winds){if(hits)montauk::mfree(hits);if(winds)montauk::mfree(winds);continue;} + float min_y=path->edges[0].y1,max_y=min_y; + for(int e=0;eedge_count;e++) { + float a=path->edges[e].y1,b=path->edges[e].y2; + if(amax_y)max_y=a; + if(b>max_y)max_y=b; + } + int row0=page_y+(int)((page->height-max_y)*g_zoom); + int row1=page_y+(int)((page->height-min_y)*g_zoom+1); + if(row0cmd_y1)row1=cmd_y1; + Color color=Color::from_rgb(path->r,path->g,path->b); + for(int row=row0;rowheight-(row-page_y+0.5f)/g_zoom; + int count=0; + for(int e=0;eedge_count;e++) { + FilledPathEdge* edge=&path->edges[e]; + if((edge->y1<=scan&&edge->y2>scan)||(edge->y2<=scan&&edge->y1>scan)) { + float t=(scan-edge->y1)/(edge->y2-edge->y1); + hits[count]=page_x+(edge->x1+t*(edge->x2-edge->x1))*g_zoom; + winds[count]=edge->y2>edge->y1?1:-1; + count++; + } + } + for(int i=1;i=0&&hits[j]>hx){hits[j+1]=hits[j];winds[j+1]=winds[j];j--;} + hits[j+1]=hx;winds[j+1]=hw; + } + if(path->even_odd) { + for(int i=0;i+1cmd_x1)x1=cmd_x1; + if(x1>x0)px_fill(pixels,g_win_w,g_win_h,x0,row,x1-x0,1,color); + } + } else { + int winding=0,start_x=0; + for(int i=0;icmd_x1)end_x=cmd_x1; + if(end_x>start_x)px_fill(pixels,g_win_w,g_win_h,start_x,row,end_x-start_x,1,color); + } + } + } + } + montauk::mfree(hits);montauk::mfree(winds); + } else if(command->type==PAINT_GRAPHICS) { + GraphicsItem* gi = &page->gfx_items[command->index]; Color gfx_color = Color::from_rgb(gi->r, gi->g, gi->b); if (gi->type == GFX_RECT_FILL) { // gi->x1,y1 = rect origin (PDF coords), gi->x2,y2 = width,height - int rx = page_x + (int)(gi->x1 * g_zoom); - int ry = page_y + (int)((page->height - gi->y1 - gi->y2) * g_zoom); - int rw = (int)(gi->x2 * g_zoom); - int rh = (int)(gi->y2 * g_zoom); - if (rw < 0) { rx += rw; rw = -rw; } - if (rh < 0) { ry += rh; rh = -rh; } + float pdf_x0=gi->x1,pdf_x1=gi->x1+gi->x2; + float pdf_y0=gi->y1,pdf_y1=gi->y1+gi->y2; + if(pdf_x1height-pdf_y1)*g_zoom+0.5f); + int right=page_x+floor_pixel(pdf_x1*g_zoom+0.5f); + int bottom=page_y+floor_pixel((page->height-pdf_y0)*g_zoom+0.5f); + int rw=right-rx,rh=bottom-ry; + if(gi->x2!=0&&rw<1)rw=1; + if(gi->y2!=0&&rh<1)rh=1; // Clip to content area - if (ry + rh > clip_y0 && ry < clip_y1) - px_fill(pixels, g_win_w, g_win_h, rx, ry, rw, rh, gfx_color); + int x0=rxcmd_x1?cmd_x1:rx+rw,y1=ry+rh>cmd_y1?cmd_y1:ry+rh; + if(x1>x0&&y1>y0)px_fill(pixels,g_win_w,g_win_h,x0,y0,x1-x0,y1-y0,gfx_color); } else { // GFX_LINE or GFX_RECT_STROKE int lx0 = page_x + (int)(gi->x1 * g_zoom); @@ -75,25 +327,16 @@ void render(uint32_t* pixels) { // Basic clip check int min_y = ly0 < ly1 ? ly0 : ly1; int max_y = ly0 > ly1 ? ly0 : ly1; - if (max_y + lw >= clip_y0 && min_y - lw < clip_y1) - px_line(pixels, g_win_w, g_win_h, lx0, ly0, lx1, ly1, lw, gfx_color); + if (max_y + lw >= cmd_y0 && min_y - lw < cmd_y1) + draw_clipped_line(pixels,lx0,ly0,lx1,ly1,lw,gfx_color, + cmd_x0,cmd_y0,cmd_x1,cmd_y1); } - } + } else if(command->type==PAINT_TEXT) { + TextItem* item = &page->items[command->index]; - // Draw text items - for (int i = 0; i < page->item_count; i++) { - TextItem* item = &page->items[i]; - - // Convert PDF coords to screen coords - // PDF: origin bottom-left, y up - // Screen: origin top-left, y down int sx = page_x + (int)(item->x * g_zoom); int sy = page_y + (int)((page->height - item->y) * g_zoom); - // Skip if outside visible area - if (sy < clip_y0 - 30 || sy > clip_y1) continue; - - // Choose font: prefer embedded, fall back to system fonts TrueTypeFont* font = item->font; if (!font) { font = g_font; @@ -102,41 +345,35 @@ void render(uint32_t* pixels) { } if (!font) continue; - // Scale font size int px_size = (int)(item->font_size * g_zoom + 0.5f); if (px_size < 4) px_size = 4; if (px_size > 120) px_size = 120; - // draw_to_buffer treats y as top of text box, but PDF - // specifies the baseline. Subtract ascent so the rendered - // baseline lands at the correct position. - GlyphCache* gc = font->get_cache(px_size); - int baseline_adj = gc ? gc->ascent : (int)(px_size * 0.8f); - int ty = sy - baseline_adj; - if (item->font) { - // Embedded font: pass raw character codes directly - // (subset fonts use codes 0-N that map through the font's cmap) - font->draw_to_buffer(pixels, g_win_w, g_win_h, - sx, ty, item->text, TEXT_COLOR, px_size); + draw_affine_text(pixels, *item, font, item->text, + item->glyph_advances,item->text_len, + sx, sy, px_size, cmd_x0, cmd_y0, cmd_x1, cmd_y1); } else { - // System font: filter out non-printable characters char render_text[MAX_TEXT_LEN]; + float render_advances[MAX_TEXT_LEN]; int ri = 0; - for (int j = 0; item->text[j] && ri < MAX_TEXT_LEN - 1; j++) { - char c = item->text[j]; - if (c >= 32 && c < 127) { - render_text[ri++] = c; + for (int j = 0; j < item->text_len && ri < MAX_TEXT_LEN - 1; j++) { + uint8_t c = (uint8_t)item->text[j]; + if (c >= 32 && c != 127) { + render_text[ri++] = (char)c; + render_advances[ri-1]=item->glyph_advances[j]; } else if (c == '\t') { render_text[ri++] = ' '; + render_advances[ri-1]=item->glyph_advances[j]; } } render_text[ri] = '\0'; - if (ri > 0) { - font->draw_to_buffer(pixels, g_win_w, g_win_h, - sx, ty, render_text, TEXT_COLOR, px_size); - } + if (ri > 0) + draw_affine_text(pixels, *item, font, render_text, + render_advances,ri, + sx, sy, px_size, cmd_x0, cmd_y0, cmd_x1, cmd_y1); + } } }