feat: PDF viewer
This commit is contained in:
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# 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))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, games are handled separately.
|
# 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.
|
# Home directory placeholder.
|
||||||
HOMEKEEP := $(BINDIR)/home/.keep
|
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).
|
# 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)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -139,6 +139,10 @@ fontpreview:
|
|||||||
spreadsheet: libc
|
spreadsheet: libc
|
||||||
$(MAKE) -C src/spreadsheet
|
$(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).
|
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
|
||||||
desktop: libc libjpeg
|
desktop: libc libjpeg
|
||||||
$(MAKE) -C src/desktop
|
$(MAKE) -C src/desktop
|
||||||
@@ -200,3 +204,4 @@ clean:
|
|||||||
$(MAKE) -C src/imageviewer clean
|
$(MAKE) -C src/imageviewer clean
|
||||||
$(MAKE) -C src/fontpreview clean
|
$(MAKE) -C src/fontpreview clean
|
||||||
$(MAKE) -C src/spreadsheet clean
|
$(MAKE) -C src/spreadsheet clean
|
||||||
|
$(MAKE) -C src/pdfviewer clean
|
||||||
|
|||||||
@@ -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
|
// all the encodingIDs are unicode, so we don't bother to check it
|
||||||
info->index_map = cmap + ttULONG(data+encoding_record+4);
|
info->index_map = cmap + ttULONG(data+encoding_record+4);
|
||||||
break;
|
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)
|
if (info->index_map == 0)
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ struct TrueTypeFont {
|
|||||||
GlyphCache caches[4];
|
GlyphCache caches[4];
|
||||||
int cache_count;
|
int cache_count;
|
||||||
bool valid;
|
bool valid;
|
||||||
|
bool em_scaling; // true for PDF embedded fonts: scale by em square, not ascent-descent
|
||||||
|
|
||||||
bool init(const char* vfs_path) {
|
bool init(const char* vfs_path) {
|
||||||
valid = false;
|
valid = false;
|
||||||
|
em_scaling = false;
|
||||||
data = nullptr;
|
data = nullptr;
|
||||||
cache_count = 0;
|
cache_count = 0;
|
||||||
|
|
||||||
@@ -97,19 +99,20 @@ struct TrueTypeFont {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
GlyphCache* get_cache(int pixel_size) {
|
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||||
// Search existing caches
|
// Free any existing glyph bitmaps
|
||||||
for (int i = 0; i < cache_count; i++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
if (caches[i].pixel_size == pixel_size)
|
if (gc->glyphs[i].bitmap) {
|
||||||
return &caches[i];
|
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->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;
|
int asc, desc, lg;
|
||||||
stbtt_GetFontVMetrics(&info, &asc, &desc, &lg);
|
stbtt_GetFontVMetrics(&info, &asc, &desc, &lg);
|
||||||
@@ -117,15 +120,31 @@ struct TrueTypeFont {
|
|||||||
gc->descent = (int)(desc * gc->scale);
|
gc->descent = (int)(desc * gc->scale);
|
||||||
gc->line_gap = (int)(lg * gc->scale);
|
gc->line_gap = (int)(lg * gc->scale);
|
||||||
gc->line_height = gc->ascent - gc->descent + gc->line_gap;
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a free slot if available
|
||||||
|
if (cache_count < 4) {
|
||||||
|
GlyphCache* gc = &caches[cache_count++];
|
||||||
|
init_cache(gc, pixel_size);
|
||||||
return gc;
|
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) {
|
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||||
if (codepoint < 0 || codepoint >= 256) return nullptr;
|
if (codepoint < 0 || codepoint >= 256) return nullptr;
|
||||||
CachedGlyph* g = &gc->glyphs[codepoint];
|
CachedGlyph* g = &gc->glyphs[codepoint];
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ static bool is_font_file(const char* name) {
|
|||||||
return str_ends_with(name, ".ttf");
|
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) {
|
static int detect_file_type(const char* name, bool is_dir) {
|
||||||
if (is_dir) return 1;
|
if (is_dir) return 1;
|
||||||
if (str_ends_with(name, ".elf")) return 2;
|
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);
|
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
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) {
|
} else if (fm->desktop) {
|
||||||
open_texteditor_with_file(fm->desktop, fullpath);
|
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);
|
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
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) {
|
} else if (fm->desktop) {
|
||||||
open_texteditor_with_file(fm->desktop, fullpath);
|
open_texteditor_with_file(fm->desktop, fullpath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* pdfviewer.h
|
||||||
|
* Shared header for the MontaukOS PDF viewer app
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <gui/gui.hpp>
|
||||||
|
#include <gui/truetype.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* stb_truetype_impl.cpp
|
||||||
|
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <gui/stb_math.h>
|
||||||
|
|
||||||
|
// 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 <gui/stb_truetype.h>
|
||||||
Reference in New Issue
Block a user