feat: PDF viewer

This commit is contained in:
2026-03-06 08:00:57 +01:00
parent ead7d296f7
commit b148f0605a
12 changed files with 3127 additions and 17 deletions
+16
View File
@@ -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);
}
+88
View File
@@ -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)
+97
View File
@@ -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';
}
+329
View File
@@ -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);
}
+629
View File
@@ -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
+175
View File
@@ -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);
+233
View File
@@ -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>