feat: PDF viewer
This commit is contained in:
@@ -59,7 +59,7 @@ BINDIR := bin
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, games are handled separately.
|
||||
@@ -84,9 +84,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
||||
# Home directory placeholder.
|
||||
HOMEKEEP := $(BINDIR)/home/.keep
|
||||
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet desktop icons fonts bearssl libc tls libjpeg
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer desktop icons fonts bearssl libc tls libjpeg
|
||||
|
||||
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
|
||||
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
@@ -139,6 +139,10 @@ fontpreview:
|
||||
spreadsheet: libc
|
||||
$(MAKE) -C src/spreadsheet
|
||||
|
||||
# Build PDF viewer standalone GUI client (depends on libc).
|
||||
pdfviewer: libc
|
||||
$(MAKE) -C src/pdfviewer
|
||||
|
||||
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
|
||||
desktop: libc libjpeg
|
||||
$(MAKE) -C src/desktop
|
||||
@@ -200,3 +204,4 @@ clean:
|
||||
$(MAKE) -C src/imageviewer clean
|
||||
$(MAKE) -C src/fontpreview clean
|
||||
$(MAKE) -C src/spreadsheet clean
|
||||
$(MAKE) -C src/pdfviewer clean
|
||||
|
||||
@@ -1484,6 +1484,11 @@ static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, in
|
||||
// all the encodingIDs are unicode, so we don't bother to check it
|
||||
info->index_map = cmap + ttULONG(data+encoding_record+4);
|
||||
break;
|
||||
case STBTT_PLATFORM_ID_MAC:
|
||||
// Accept Mac Roman as fallback (common in PDF subset fonts)
|
||||
if (info->index_map == 0)
|
||||
info->index_map = cmap + ttULONG(data+encoding_record+4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (info->index_map == 0)
|
||||
|
||||
@@ -63,9 +63,11 @@ struct TrueTypeFont {
|
||||
GlyphCache caches[4];
|
||||
int cache_count;
|
||||
bool valid;
|
||||
bool em_scaling; // true for PDF embedded fonts: scale by em square, not ascent-descent
|
||||
|
||||
bool init(const char* vfs_path) {
|
||||
valid = false;
|
||||
em_scaling = false;
|
||||
data = nullptr;
|
||||
cache_count = 0;
|
||||
|
||||
@@ -97,19 +99,20 @@ struct TrueTypeFont {
|
||||
return true;
|
||||
}
|
||||
|
||||
GlyphCache* get_cache(int pixel_size) {
|
||||
// Search existing caches
|
||||
for (int i = 0; i < cache_count; i++) {
|
||||
if (caches[i].pixel_size == pixel_size)
|
||||
return &caches[i];
|
||||
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||
// Free any existing glyph bitmaps
|
||||
for (int i = 0; i < 256; i++) {
|
||||
if (gc->glyphs[i].bitmap) {
|
||||
montauk::mfree(gc->glyphs[i].bitmap);
|
||||
}
|
||||
gc->glyphs[i].bitmap = nullptr;
|
||||
gc->glyphs[i].loaded = false;
|
||||
}
|
||||
|
||||
// Create new cache
|
||||
if (cache_count >= 4) return &caches[0]; // fallback to first
|
||||
|
||||
GlyphCache* gc = &caches[cache_count++];
|
||||
gc->pixel_size = pixel_size;
|
||||
gc->scale = stbtt_ScaleForPixelHeight(&info, (float)pixel_size);
|
||||
gc->scale = em_scaling
|
||||
? stbtt_ScaleForMappingEmToPixels(&info, (float)pixel_size)
|
||||
: stbtt_ScaleForPixelHeight(&info, (float)pixel_size);
|
||||
|
||||
int asc, desc, lg;
|
||||
stbtt_GetFontVMetrics(&info, &asc, &desc, &lg);
|
||||
@@ -117,13 +120,29 @@ struct TrueTypeFont {
|
||||
gc->descent = (int)(desc * gc->scale);
|
||||
gc->line_gap = (int)(lg * gc->scale);
|
||||
gc->line_height = gc->ascent - gc->descent + gc->line_gap;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 128; i++) {
|
||||
gc->glyphs[i].bitmap = nullptr;
|
||||
gc->glyphs[i].loaded = false;
|
||||
GlyphCache* get_cache(int pixel_size) {
|
||||
// Search existing caches
|
||||
for (int i = 0; i < cache_count; i++) {
|
||||
if (caches[i].pixel_size == pixel_size)
|
||||
return &caches[i];
|
||||
}
|
||||
|
||||
return gc;
|
||||
// Use a free slot if available
|
||||
if (cache_count < 4) {
|
||||
GlyphCache* gc = &caches[cache_count++];
|
||||
init_cache(gc, pixel_size);
|
||||
return gc;
|
||||
}
|
||||
|
||||
// Evict oldest cache (slot 0) and shift others down
|
||||
GlyphCache evicted = caches[0];
|
||||
for (int i = 0; i < 3; i++)
|
||||
caches[i] = caches[i + 1];
|
||||
caches[3] = evicted;
|
||||
init_cache(&caches[3], pixel_size);
|
||||
return &caches[3];
|
||||
}
|
||||
|
||||
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||
|
||||
@@ -65,6 +65,14 @@ static bool is_font_file(const char* name) {
|
||||
return str_ends_with(name, ".ttf");
|
||||
}
|
||||
|
||||
static bool is_pdf_file(const char* name) {
|
||||
return str_ends_with(name, ".pdf");
|
||||
}
|
||||
|
||||
static bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
static int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
@@ -567,6 +575,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
|
||||
} else if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
@@ -608,6 +620,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
montauk::spawn("0:/os/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
|
||||
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
|
||||
} else if (fm->desktop) {
|
||||
open_texteditor_with_file(fm->desktop, fullpath);
|
||||
}
|
||||
|
||||
@@ -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