Files
MontaukOS/programs/src/wordprocessor/print_export.cpp
T

1459 lines
54 KiB
C++

/*
* print_export.cpp
* Word Processor print export and job submission
* Copyright (c) 2026 Daniel Hammer
*/
#include "wordprocessor.hpp"
#include <gui/dialogs.hpp>
#include <print/print.hpp>
extern "C" {
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#define STBI_WRITE_NO_STDIO
#include <gui/stb_image_write.h>
}
namespace dialogs = gui::dialogs;
static constexpr int WP_PRINT_PAGE_W = 1275; // 8.5in at 150 DPI
static constexpr int WP_PRINT_PAGE_H = 1650; // 11in at 150 DPI
static constexpr int WP_PRINT_MARGIN_X = 96;
static constexpr int WP_PRINT_MARGIN_Y = 96;
static constexpr int WP_PRINT_JPEG_QUALITY = 92;
struct WpJpegBuffer {
uint8_t* data;
int size;
int capacity;
bool failed;
};
static void wp_status_copy(char* out, int out_len, const char* text) {
if (!out || out_len <= 0) return;
if (!text) text = "";
montauk::strncpy(out, text, out_len - 1);
out[out_len - 1] = '\0';
}
static void wp_jpeg_write_callback(void* context, void* data, int size) {
WpJpegBuffer* buf = (WpJpegBuffer*)context;
if (!buf || !data || size <= 0 || buf->failed) return;
int needed = buf->size + size;
if (needed > buf->capacity) {
int new_cap = buf->capacity > 0 ? buf->capacity : 256 * 1024;
while (new_cap < needed)
new_cap *= 2;
uint8_t* grown = (uint8_t*)montauk::realloc(buf->data, new_cap);
if (!grown) {
buf->failed = true;
return;
}
buf->data = grown;
buf->capacity = new_cap;
}
montauk::memcpy(buf->data + buf->size, data, size);
buf->size += size;
}
static int wp_print_layout_width() {
return (WP_PRINT_PAGE_W - WP_PRINT_MARGIN_X * 2) + WP_MARGIN * 2 + WP_SCROLLBAR_W;
}
static void wp_draw_print_divider(Canvas& c, int x, int y, int w, int h,
uint8_t divider_type, Color color) {
if (w <= 0 || h <= 0 || divider_type == PARA_DIVIDER_NONE) return;
int cy = y + h / 2;
switch (divider_type) {
case PARA_DIVIDER_SINGLE:
c.fill_rect(x, cy, w, 1, color);
break;
case PARA_DIVIDER_DOUBLE:
c.fill_rect(x, cy - 2, w, 1, color);
c.fill_rect(x, cy + 2, w, 1, color);
break;
case PARA_DIVIDER_DOTTED:
for (int dx = x; dx < x + w; dx += 5)
c.fill_rect(dx, cy, 2, 1, color);
break;
case PARA_DIVIDER_DASHED:
for (int dx = x; dx < x + w; dx += 11) {
int dash_w = (x + w - dx) < 7 ? (x + w - dx) : 7;
if (dash_w > 0) c.fill_rect(dx, cy, dash_w, 1, color);
}
break;
case PARA_DIVIDER_HEAVY:
c.fill_rect(x, cy - 1, w, 3, color);
break;
case PARA_DIVIDER_THIN_THICK:
c.fill_rect(x, cy - 3, w, 1, color);
c.fill_rect(x, cy, w, 3, color);
break;
case PARA_DIVIDER_THICK_THIN:
c.fill_rect(x, cy - 3, w, 3, color);
c.fill_rect(x, cy + 2, w, 1, color);
break;
}
}
static void wp_make_base_print_name(WordProcessorState* wp, char* out, int out_len) {
const char* src = wp->filename[0] ? wp->filename : "document";
int last_dot = -1;
for (int i = 0; src[i]; i++) {
if (src[i] == '.') last_dot = i;
}
if (last_dot <= 0) {
wp_status_copy(out, out_len, src);
return;
}
int copy_len = last_dot;
if (copy_len >= out_len) copy_len = out_len - 1;
montauk::memcpy(out, src, (uint64_t)copy_len);
out[copy_len] = '\0';
}
static bool wp_printer_accepts_jpeg(const char* printer_uri, char* err, int err_len) {
print::ProbeState probe = {};
char probe_err[160] = {};
if (!print::probe_printer_uri(printer_uri, &probe, probe_err, sizeof(probe_err)))
return true;
if (probe.caps.supports_jpeg)
return true;
if (probe.caps.supported_formats[0]) {
snprintf(err, (size_t)err_len,
"Selected printer does not advertise JPEG support (supports: %s)",
probe.caps.supported_formats);
} else {
wp_status_copy(err, err_len, "Selected printer does not advertise JPEG support");
}
return false;
}
static bool wp_build_print_page_map(WordProcessorState* wp,
int** out_line_pages,
int** out_line_y,
int* out_page_count,
char* err, int err_len) {
if (out_line_pages) *out_line_pages = nullptr;
if (out_line_y) *out_line_y = nullptr;
if (out_page_count) *out_page_count = 0;
wp_recompute_wrap(wp, wp_print_layout_width(), WP_PRINT_DPI);
if (wp->wrap_line_count <= 0) {
wp_status_copy(err, err_len, "failed to paginate document");
return false;
}
int* line_pages = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
int* line_y = (int*)montauk::malloc((uint64_t)wp->wrap_line_count * sizeof(int));
if (!line_pages || !line_y) {
if (line_pages) montauk::mfree(line_pages);
if (line_y) montauk::mfree(line_y);
wp_status_copy(err, err_len, "out of memory while preparing print pages");
return false;
}
int base_y = wp->wrap_lines[0].y;
int printable_bottom = WP_PRINT_PAGE_H - WP_PRINT_MARGIN_Y;
int current_page = 0;
int current_y = WP_PRINT_MARGIN_Y;
int prev_rel_bottom = 0;
for (int i = 0; i < wp->wrap_line_count; i++) {
WrapLine* wl = &wp->wrap_lines[i];
int rel_y = wl->y - base_y;
if (rel_y < 0) rel_y = 0;
int gap_before = (i == 0) ? 0 : (rel_y - prev_rel_bottom);
if (gap_before < 0) gap_before = 0;
int line_top = current_y + gap_before;
if (line_top + wl->height > printable_bottom && current_y > WP_PRINT_MARGIN_Y) {
current_page++;
current_y = WP_PRINT_MARGIN_Y;
line_top = current_y;
}
line_pages[i] = current_page;
line_y[i] = line_top;
current_y = line_top + wl->height;
prev_rel_bottom = rel_y + wl->height;
}
if (out_line_pages) *out_line_pages = line_pages;
else montauk::mfree(line_pages);
if (out_line_y) *out_line_y = line_y;
else montauk::mfree(line_y);
if (out_page_count) *out_page_count = current_page + 1;
return true;
}
static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, int py) {
if (!wl->first_in_paragraph || wl->paragraph_idx < 0 || wl->paragraph_idx >= wp->paragraph_count)
return;
ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx];
if (para->divider_type != PARA_DIVIDER_NONE || para->list_type == PARA_LIST_NONE) return;
int marker_x = WP_PRINT_MARGIN_X + wp_scale_layout_units(para->left_indent + para->first_line_indent, WP_PRINT_DPI);
int min_x = WP_PRINT_MARGIN_X - WP_MARGIN + 4;
if (marker_x < min_x) marker_x = min_x;
if (para->list_type == PARA_LIST_BULLET) {
int bullet_x = marker_x + wp_scale_layout_units(7, WP_PRINT_DPI);
StyledRun* run = (wl->run_idx >= 0 && wl->run_idx < wp->run_count) ? &wp->runs[wl->run_idx] : nullptr;
int run_size = run ? run->size : WP_DEFAULT_SIZE;
int font_px = wp_print_font_pixels(run_size);
TrueTypeFont* font = run ? wp_get_font(run->font_id, 0) : g_ui_font;
if (!font) font = g_ui_font;
int th = text_height(font, font_px);
int top_y = py + (wl->height - th) / 2;
char bullet[2] = { '\x95', '\0' };
draw_text(c, font, bullet_x, top_y, bullet, colors::BLACK, font_px);
return;
}
if (wp->run_count <= 0) return;
StyledRun* run = &wp->runs[wl->run_idx < wp->run_count ? wl->run_idx : 0];
TrueTypeFont* font = wp_get_font(run->font_id, run->flags);
char label[16];
snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1);
int font_px = wp_print_font_pixels(run->size);
int top_y = py + (wl->height - font_px) / 2;
draw_text(c, font ? font : g_ui_font, marker_x, top_y, label, colors::BLACK, font_px);
}
static bool wp_encode_print_page(WordProcessorState* wp,
const int* line_pages,
const int* line_y,
int page_index,
uint8_t** out_data,
int* out_len,
char* err, int err_len) {
if (out_data) *out_data = nullptr;
if (out_len) *out_len = 0;
uint64_t pixel_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * sizeof(uint32_t);
uint32_t* pixels = (uint32_t*)montauk::malloc(pixel_bytes);
if (!pixels) {
wp_status_copy(err, err_len, "out of memory while rendering print page");
return false;
}
Canvas c(pixels, WP_PRINT_PAGE_W, WP_PRINT_PAGE_H);
c.fill(colors::WHITE);
for (int li = 0; li < wp->wrap_line_count; li++) {
if (line_pages[li] != page_index) continue;
WrapLine* wl = &wp->wrap_lines[li];
int py = line_y[li];
wp_draw_print_list_marker(c, wp, wl, py);
if (wl->divider_type != PARA_DIVIDER_NONE) {
int divider_x = WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN);
wp_draw_print_divider(c, divider_x, py, wl->width, wl->height, wl->divider_type, colors::BLACK);
}
int chars_left = wl->char_count;
int ri = wl->run_idx;
int ro = wl->run_offset;
int x = WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN);
int line_abs_start = wp_wrap_line_start(wp, li);
int char_idx = 0;
while (chars_left > 0 && ri < wp->run_count) {
StyledRun* r = &wp->runs[ri];
TrueTypeFont* font = wp_get_font(r->font_id, r->flags);
if (!font || !font->valid) {
ri++;
ro = 0;
continue;
}
GlyphCache* gc = font->get_cache(wp_print_font_pixels(r->size));
int baseline = py + wl->baseline;
int avail = r->len - ro;
int to_draw = avail < chars_left ? avail : chars_left;
for (int ci = 0; ci < to_draw; ci++) {
char ch = r->text[ro + ci];
(void)line_abs_start;
(void)char_idx;
if (ch != '\n' && (ch >= 32 || ch < 0)) {
int adv = font->draw_char_to_buffer(
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, colors::BLACK, gc);
x += adv;
}
char_idx++;
}
chars_left -= to_draw;
ro += to_draw;
if (ro >= r->len) {
ri++;
ro = 0;
}
}
}
uint64_t rgb_bytes = (uint64_t)WP_PRINT_PAGE_W * WP_PRINT_PAGE_H * 3;
uint8_t* rgb = (uint8_t*)montauk::malloc(rgb_bytes);
if (!rgb) {
montauk::mfree(pixels);
wp_status_copy(err, err_len, "out of memory while encoding print page");
return false;
}
for (int y = 0; y < WP_PRINT_PAGE_H; y++) {
uint8_t* dst = rgb + (uint64_t)y * WP_PRINT_PAGE_W * 3;
for (int x = 0; x < WP_PRINT_PAGE_W; x++) {
uint32_t px = pixels[(uint64_t)y * WP_PRINT_PAGE_W + x];
dst[x * 3 + 0] = (uint8_t)((px >> 16) & 0xFF);
dst[x * 3 + 1] = (uint8_t)((px >> 8) & 0xFF);
dst[x * 3 + 2] = (uint8_t)(px & 0xFF);
}
}
montauk::mfree(pixels);
WpJpegBuffer jpeg = {};
jpeg.capacity = 256 * 1024;
jpeg.data = (uint8_t*)montauk::malloc(jpeg.capacity);
if (!jpeg.data) {
montauk::mfree(rgb);
wp_status_copy(err, err_len, "out of memory for JPEG output");
return false;
}
int ok = stbi_write_jpg_to_func(wp_jpeg_write_callback, &jpeg,
WP_PRINT_PAGE_W, WP_PRINT_PAGE_H, 3, rgb,
WP_PRINT_JPEG_QUALITY);
montauk::mfree(rgb);
if (!ok || jpeg.failed || jpeg.size <= 0) {
montauk::mfree(jpeg.data);
wp_status_copy(err, err_len, "JPEG encoding failed");
return false;
}
if (out_data) *out_data = jpeg.data;
else montauk::mfree(jpeg.data);
if (out_len) *out_len = jpeg.size;
return true;
}
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len) {
if (out_status && out_status_len > 0) out_status[0] = '\0';
if (!wp) {
wp_status_copy(out_status, out_status_len, "No document loaded");
return false;
}
char printer_uri[256] = {};
char printer_name[128] = {};
char msg[160] = {};
montauk::strncpy(printer_uri, wp->preferred_printer_uri, (int)sizeof(printer_uri) - 1);
printer_uri[sizeof(printer_uri) - 1] = '\0';
montauk::strncpy(printer_name, wp->preferred_printer_name, (int)sizeof(printer_name) - 1);
printer_name[sizeof(printer_name) - 1] = '\0';
uint32_t copies = wp->preferred_print_copies > 0 ? wp->preferred_print_copies : 1;
const char* job_name = wp->filename[0] ? wp->filename : "Untitled document";
if (!dialogs::configure_print("Print Document",
printer_uri, job_name,
printer_uri, sizeof(printer_uri),
printer_name, sizeof(printer_name),
&copies, msg, sizeof(msg))) {
if (msg[0]) wp_status_copy(out_status, out_status_len, msg);
return false;
}
montauk::strncpy(wp->preferred_printer_uri, printer_uri, (int)sizeof(wp->preferred_printer_uri) - 1);
wp->preferred_printer_uri[sizeof(wp->preferred_printer_uri) - 1] = '\0';
montauk::strncpy(wp->preferred_printer_name, printer_name, (int)sizeof(wp->preferred_printer_name) - 1);
wp->preferred_printer_name[sizeof(wp->preferred_printer_name) - 1] = '\0';
wp->preferred_print_copies = copies > 0 ? copies : 1;
char err[192] = {};
if (!wp_printer_accepts_jpeg(printer_uri, err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
return false;
}
int* line_pages = nullptr;
int* line_y = nullptr;
int page_count = 0;
if (!wp_build_print_page_map(wp, &line_pages, &line_y, &page_count, err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
return false;
}
char base_name[96] = {};
wp_make_base_print_name(wp, base_name, sizeof(base_name));
if (!base_name[0]) wp_status_copy(base_name, sizeof(base_name), "document");
int submitted_pages = 0;
char first_job_id[64] = {};
for (int page = 0; page < page_count; page++) {
uint8_t* jpeg = nullptr;
int jpeg_len = 0;
if (!wp_encode_print_page(wp, line_pages, line_y, page, &jpeg, &jpeg_len, err, sizeof(err))) {
break;
}
char page_job_name[128];
char source_name[128];
if (page_count > 1) {
snprintf(page_job_name, sizeof(page_job_name), "%s (Page %d/%d)", job_name, page + 1, page_count);
snprintf(source_name, sizeof(source_name), "%s-page-%03d.jpg", base_name, page + 1);
} else {
wp_status_copy(page_job_name, sizeof(page_job_name), job_name);
snprintf(source_name, sizeof(source_name), "%s.jpg", base_name);
}
char job_id[64] = {};
bool ok = print::submit_document_buffer_job(
jpeg, jpeg_len,
source_name,
"image/jpeg",
printer_uri,
page_job_name,
(int)(copies > 0 ? copies : 1),
job_id, sizeof(job_id),
err, sizeof(err));
montauk::mfree(jpeg);
if (!ok) break;
if (submitted_pages == 0)
wp_status_copy(first_job_id, sizeof(first_job_id), job_id);
submitted_pages++;
}
montauk::mfree(line_pages);
montauk::mfree(line_y);
if (submitted_pages != page_count) {
if (submitted_pages > 0) {
char partial[192];
snprintf(partial, sizeof(partial),
"Queued %d of %d pages before failing: %s",
submitted_pages, page_count,
err[0] ? err : "unknown error");
wp_status_copy(out_status, out_status_len, partial);
} else {
wp_status_copy(out_status, out_status_len, err[0] ? err : "Failed to queue print job");
}
return false;
}
char summary[192];
const char* label = printer_name[0] ? printer_name : printer_uri;
if (page_count == 1) {
if (copies > 1) {
snprintf(summary, sizeof(summary),
"Queued 1 page to %s (%u copies), job %s",
label, (unsigned)copies, first_job_id);
} else {
snprintf(summary, sizeof(summary),
"Queued print job %s to %s",
first_job_id, label);
}
} else if (copies > 1) {
snprintf(summary, sizeof(summary),
"Queued %d pages to %s (%u copies each)",
page_count, label, (unsigned)copies);
} else {
snprintf(summary, sizeof(summary),
"Queued %d pages to %s",
page_count, label);
}
wp_status_copy(out_status, out_status_len, summary);
return true;
}
struct WpPdfBuffer {
uint8_t* data;
int len;
int cap;
};
struct WpPdfPageStream {
uint8_t* data;
int len;
};
struct WpPdfFontDef {
uint8_t font_id;
uint8_t style_index;
char resource_name[8];
char base_font[48];
const char* file_path;
TrueTypeFont* font;
uint8_t* file_data;
int file_len;
int descriptor_flags;
int italic_angle;
int bbox_x0;
int bbox_y0;
int bbox_x1;
int bbox_y1;
int ascent;
int descent;
int cap_height;
int units_per_em;
int font_obj_num;
int descriptor_obj_num;
int file_obj_num;
};
static void wp_pdf_buffer_free(WpPdfBuffer* buf) {
if (buf && buf->data) {
montauk::mfree(buf->data);
buf->data = nullptr;
}
if (buf) {
buf->len = 0;
buf->cap = 0;
}
}
static bool wp_pdf_buffer_init(WpPdfBuffer* buf, int initial_cap) {
if (!buf) return false;
buf->len = 0;
buf->cap = initial_cap > 0 ? initial_cap : 1024;
buf->data = (uint8_t*)montauk::malloc((uint64_t)buf->cap);
return buf->data != nullptr;
}
static bool wp_pdf_buffer_ensure(WpPdfBuffer* buf, int extra) {
if (!buf || extra < 0) return false;
if (buf->len + extra <= buf->cap) return true;
int new_cap = buf->cap > 0 ? buf->cap : 1024;
while (new_cap < buf->len + extra)
new_cap *= 2;
uint8_t* grown = (uint8_t*)montauk::realloc(buf->data, new_cap);
if (!grown) return false;
buf->data = grown;
buf->cap = new_cap;
return true;
}
static bool wp_pdf_buffer_append_bytes(WpPdfBuffer* buf, const void* data, int len) {
if (!buf || !data || len < 0) return false;
if (len == 0) return true;
if (!wp_pdf_buffer_ensure(buf, len)) return false;
montauk::memcpy(buf->data + buf->len, data, (uint64_t)len);
buf->len += len;
return true;
}
static bool wp_pdf_buffer_appendf(WpPdfBuffer* buf, const char* fmt, ...) {
if (!buf || !fmt) return false;
char stack_buf[256];
va_list ap;
va_start(ap, fmt);
va_list ap_copy;
va_copy(ap_copy, ap);
int needed = vsnprintf(stack_buf, sizeof(stack_buf), fmt, ap);
va_end(ap);
if (needed < 0) {
va_end(ap_copy);
return false;
}
bool ok = false;
if (needed < (int)sizeof(stack_buf)) {
ok = wp_pdf_buffer_append_bytes(buf, stack_buf, needed);
} else {
char* tmp = (char*)montauk::malloc((uint64_t)needed + 1);
if (tmp) {
vsnprintf(tmp, (size_t)needed + 1, fmt, ap_copy);
ok = wp_pdf_buffer_append_bytes(buf, tmp, needed);
montauk::mfree(tmp);
}
}
va_end(ap_copy);
return ok;
}
static bool wp_pdf_buffer_append_hex_string(WpPdfBuffer* buf, const char* text, int len) {
static constexpr char HEX[] = "0123456789ABCDEF";
if (!buf || !text || len < 0) return false;
if (!wp_pdf_buffer_ensure(buf, len * 2 + 2)) return false;
buf->data[buf->len++] = '<';
for (int i = 0; i < len; i++) {
unsigned char ch = (unsigned char)text[i];
buf->data[buf->len++] = (uint8_t)HEX[(ch >> 4) & 0x0F];
buf->data[buf->len++] = (uint8_t)HEX[ch & 0x0F];
}
buf->data[buf->len++] = '>';
return true;
}
static constexpr int WP_PDF_POINT_SCALE = 100;
static int wp_pdf_div_round_nearest(int64_t numer, int denom) {
if (denom <= 0) return 0;
if (numer >= 0) return (int)((numer + denom / 2) / denom);
return (int)((numer - denom / 2) / denom);
}
static int wp_pdf_points_hundredths_from_print_px(int px) {
return wp_pdf_div_round_nearest((int64_t)px * 72 * WP_PDF_POINT_SCALE, WP_PRINT_DPI);
}
static int wp_pdf_page_width_points_hundredths() {
return wp_pdf_points_hundredths_from_print_px(WP_PRINT_PAGE_W);
}
static int wp_pdf_page_height_points_hundredths() {
return wp_pdf_points_hundredths_from_print_px(WP_PRINT_PAGE_H);
}
static int wp_pdf_y_points_hundredths_from_print_top(int y_px) {
return wp_pdf_page_height_points_hundredths() - wp_pdf_points_hundredths_from_print_px(y_px);
}
static bool wp_pdf_buffer_append_number_hundredths(WpPdfBuffer* buf, int value_hundredths) {
// The freestanding printf implementation used here does not substitute %f reliably.
if (!buf) return false;
bool negative = value_hundredths < 0;
int magnitude = negative ? -value_hundredths : value_hundredths;
int whole = magnitude / WP_PDF_POINT_SCALE;
int frac = magnitude % WP_PDF_POINT_SCALE;
char tmp[32];
int wrote = 0;
if (frac == 0) {
wrote = snprintf(tmp, sizeof(tmp), negative ? "-%d" : "%d", whole);
} else if ((frac % 10) == 0) {
wrote = snprintf(tmp, sizeof(tmp), negative ? "-%d.%d" : "%d.%d", whole, frac / 10);
} else {
wrote = snprintf(tmp, sizeof(tmp), negative ? "-%d.%02d" : "%d.%02d", whole, frac);
}
if (wrote <= 0 || wrote >= (int)sizeof(tmp)) return false;
return wp_pdf_buffer_append_bytes(buf, tmp, wrote);
}
static int wp_pdf_glyph_advance_hundredths(const WpPdfFontDef* def, int font_points, unsigned char ch) {
if (!def || !def->font || !def->font->valid || def->units_per_em <= 0 || font_points <= 0)
return 0;
int advance = 0;
int lsb = 0;
int cp = decode_single_byte_codepoint(ch);
stbtt_GetCodepointHMetrics(&def->font->info, cp, &advance, &lsb);
return wp_pdf_div_round_nearest((int64_t)advance * font_points * WP_PDF_POINT_SCALE,
def->units_per_em);
}
static int wp_pdf_style_index(uint8_t flags) {
int idx = 0;
if (flags & STYLE_BOLD) idx |= 1;
if (flags & STYLE_ITALIC) idx |= 2;
return idx;
}
// Path and base-font lookups fall back to the regular variant when the
// requested style is missing, mirroring wp_get_font so a document with, say,
// bold-italic text in a regular-only family still embeds (in the regular face).
static const char* wp_pdf_font_path(uint8_t font_id, uint8_t style_index) {
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
int v = wp_resolve_variant(font_id, style_index);
if (v < 0) return nullptr;
const char* p = g_wp_fonts.families[font_id].paths[v];
return p[0] ? p : nullptr;
}
static const char* wp_pdf_base_font_name(uint8_t font_id, uint8_t style_index) {
if (font_id >= g_wp_fonts.count || style_index > 3) return nullptr;
int v = wp_resolve_variant(font_id, style_index);
if (v < 0) return nullptr;
const char* n = g_wp_fonts.families[font_id].basenames[v];
return n[0] ? n : nullptr;
}
static int wp_pdf_descriptor_flags(uint8_t font_id, uint8_t style_index) {
int flags = 32; // Nonsymbolic
if (font_id < g_wp_fonts.count && g_wp_fonts.families[font_id].serif)
flags |= 2; // Serif
if (style_index == 2 || style_index == 3)
flags |= 64; // Italic
return flags;
}
static int wp_pdf_units_per_em(TrueTypeFont* font) {
if (!font || !font->valid || font->info.head <= 0 || !font->info.data)
return 1000;
uint8_t* upem_ptr = font->info.data + font->info.head + 18;
int upem = ((int)upem_ptr[0] << 8) | (int)upem_ptr[1];
return upem > 0 ? upem : 1000;
}
static bool wp_pdf_path_has_suffix(const char* path, const char* suffix) {
if (!path || !suffix) return false;
int plen = montauk::slen(path);
int slen = montauk::slen(suffix);
if (slen > plen) return false;
for (int i = 0; i < slen; i++) {
char a = path[plen - slen + i];
char b = suffix[i];
if (a >= 'A' && a <= 'Z') a = (char)(a + ('a' - 'A'));
if (b >= 'A' && b <= 'Z') b = (char)(b + ('a' - 'A'));
if (a != b) return false;
}
return true;
}
static bool wp_pdf_normalize_path(const char* path, char* out, int out_len,
char* err, int err_len) {
if (!out || out_len <= 0) return false;
out[0] = '\0';
if (!path || !path[0]) {
wp_status_copy(err, err_len, "Choose a PDF filename");
return false;
}
if (wp_pdf_path_has_suffix(path, ".pdf")) {
wp_status_copy(out, out_len, path);
return true;
}
int plen = montauk::slen(path);
if (plen + 4 >= out_len) {
wp_status_copy(err, err_len, "PDF filename is too long");
return false;
}
wp_status_copy(out, out_len, path);
montauk::strncpy(out + plen, ".pdf", out_len - plen - 1);
out[out_len - 1] = '\0';
return true;
}
static bool wp_pdf_load_font_data(WpPdfFontDef* def, char* err, int err_len) {
if (!def || !def->file_path) return false;
int fd = montauk::open(def->file_path);
if (fd < 0) {
snprintf(err, (size_t)err_len, "Cannot open font %s", def->file_path);
return false;
}
uint64_t size = montauk::getsize(fd);
if (size == 0 || size > 4 * 1024 * 1024) {
montauk::close(fd);
snprintf(err, (size_t)err_len, "Font file is invalid: %s", def->file_path);
return false;
}
def->file_data = (uint8_t*)montauk::malloc(size);
if (!def->file_data) {
montauk::close(fd);
wp_status_copy(err, err_len, "Out of memory while reading a font");
return false;
}
int got = montauk::read(fd, def->file_data, 0, size);
montauk::close(fd);
if (got < 0 || (uint64_t)got != size) {
montauk::mfree(def->file_data);
def->file_data = nullptr;
snprintf(err, (size_t)err_len, "Failed to read font %s", def->file_path);
return false;
}
def->file_len = got;
return true;
}
static bool wp_pdf_prepare_font_def(WpPdfFontDef* def,
uint8_t font_id,
uint8_t style_index,
int resource_index,
char* err, int err_len) {
if (!def) return false;
montauk::memset(def, 0, sizeof(*def));
def->font_id = font_id;
def->style_index = style_index;
snprintf(def->resource_name, sizeof(def->resource_name), "F%d", resource_index);
const char* base_name = wp_pdf_base_font_name(font_id, style_index);
const char* file_path = wp_pdf_font_path(font_id, style_index);
if (!base_name || !file_path) {
wp_status_copy(err, err_len, "Unsupported font selection in document");
return false;
}
def->file_path = file_path;
wp_status_copy(def->base_font, sizeof(def->base_font), base_name);
uint8_t style_flags = 0;
if (style_index & 1) style_flags |= STYLE_BOLD;
if (style_index & 2) style_flags |= STYLE_ITALIC;
def->font = wp_get_font(font_id, style_flags);
if (!def->font || !def->font->valid) {
snprintf(err, (size_t)err_len, "Failed to load font %s", base_name);
return false;
}
def->descriptor_flags = wp_pdf_descriptor_flags(font_id, style_index);
def->italic_angle = (style_index == 2 || style_index == 3) ? -12 : 0;
def->units_per_em = wp_pdf_units_per_em(def->font);
stbtt_GetFontBoundingBox(&def->font->info,
&def->bbox_x0, &def->bbox_y0,
&def->bbox_x1, &def->bbox_y1);
int asc = 0;
int desc = 0;
int line_gap = 0;
stbtt_GetFontVMetrics(&def->font->info, &asc, &desc, &line_gap);
(void)line_gap;
def->ascent = (asc * 1000 + def->units_per_em / 2) / def->units_per_em;
def->descent = (desc * 1000 - def->units_per_em / 2) / def->units_per_em;
def->cap_height = def->ascent > 0 ? def->ascent : 700;
return wp_pdf_load_font_data(def, err, err_len);
}
static void wp_pdf_free_fonts(WpPdfFontDef* defs, int count) {
if (!defs) return;
for (int i = 0; i < count; i++) {
if (defs[i].file_data) {
montauk::mfree(defs[i].file_data);
defs[i].file_data = nullptr;
defs[i].file_len = 0;
}
}
}
static bool wp_pdf_collect_fonts(WordProcessorState* wp,
WpPdfFontDef* defs,
int* out_count,
char* err, int err_len) {
if (out_count) *out_count = 0;
if (!wp || !defs) return false;
bool used[WP_MAX_FONTS][4] = {};
for (int i = 0; i < wp->run_count; i++) {
StyledRun* run = &wp->runs[i];
if (run->len <= 0) continue;
int font_id = run->font_id < wp_font_count() ? run->font_id : wp_default_font_id();
if (font_id < 0 || font_id >= WP_MAX_FONTS) font_id = 0;
int style_index = wp_pdf_style_index(run->flags);
used[font_id][style_index] = true;
}
int count = 0;
for (int font_id = 0; font_id < wp_font_count(); font_id++) {
for (int style_index = 0; style_index < 4; style_index++) {
if (!used[font_id][style_index]) continue;
if (!wp_pdf_prepare_font_def(&defs[count],
(uint8_t)font_id,
(uint8_t)style_index,
count + 1,
err, err_len)) {
wp_pdf_free_fonts(defs, count + 1);
return false;
}
count++;
}
}
if (out_count) *out_count = count;
return true;
}
static const WpPdfFontDef* wp_pdf_find_font_def(const WpPdfFontDef* defs,
int count,
uint8_t font_id,
uint8_t flags) {
int want_style = wp_pdf_style_index(flags);
int want_font = font_id < wp_font_count() ? font_id : wp_default_font_id();
for (int i = 0; i < count; i++) {
if (defs[i].font_id == want_font && defs[i].style_index == want_style)
return &defs[i];
}
return nullptr;
}
static bool wp_pdf_append_text_op(WpPdfBuffer* buf,
const WpPdfFontDef* font,
int font_points,
int x_pt_hundredths,
int baseline_pt_hundredths,
const char* text,
int text_len) {
if (!buf || !font || !text || text_len <= 0) return true;
return wp_pdf_buffer_appendf(buf, "BT\n/%s %d Tf\n1 0 0 1 ",
font->resource_name, font_points)
&& wp_pdf_buffer_append_number_hundredths(buf, x_pt_hundredths)
&& wp_pdf_buffer_appendf(buf, " ")
&& wp_pdf_buffer_append_number_hundredths(buf, baseline_pt_hundredths)
&& wp_pdf_buffer_appendf(buf, " Tm\n")
&& wp_pdf_buffer_append_hex_string(buf, text, text_len)
&& wp_pdf_buffer_appendf(buf, " Tj\nET\n");
}
static bool wp_pdf_append_stroke_line(WpPdfBuffer* buf,
int x0_px, int y0_px,
int x1_px, int y1_px,
int thickness_px) {
if (!buf) return false;
int width_pt_hundredths = wp_pdf_points_hundredths_from_print_px(thickness_px);
if (width_pt_hundredths < 25) width_pt_hundredths = 25;
return wp_pdf_buffer_append_number_hundredths(buf, width_pt_hundredths)
&& wp_pdf_buffer_appendf(buf, " w\n")
&& wp_pdf_buffer_append_number_hundredths(buf, wp_pdf_points_hundredths_from_print_px(x0_px))
&& wp_pdf_buffer_appendf(buf, " ")
&& wp_pdf_buffer_append_number_hundredths(buf, wp_pdf_y_points_hundredths_from_print_top(y0_px))
&& wp_pdf_buffer_appendf(buf, " m\n")
&& wp_pdf_buffer_append_number_hundredths(buf, wp_pdf_points_hundredths_from_print_px(x1_px))
&& wp_pdf_buffer_appendf(buf, " ")
&& wp_pdf_buffer_append_number_hundredths(buf, wp_pdf_y_points_hundredths_from_print_top(y1_px))
&& wp_pdf_buffer_appendf(buf, " l\nS\n");
}
static bool wp_pdf_append_divider_ops(WpPdfBuffer* buf,
int x_px, int y_px, int w_px, int h_px,
uint8_t divider_type) {
if (!buf || w_px <= 0 || h_px <= 0 || divider_type == PARA_DIVIDER_NONE) return true;
int cy = y_px + h_px / 2;
switch (divider_type) {
case PARA_DIVIDER_SINGLE:
return wp_pdf_append_stroke_line(buf, x_px, cy, x_px + w_px, cy, 1);
case PARA_DIVIDER_DOUBLE:
return wp_pdf_append_stroke_line(buf, x_px, cy - 2, x_px + w_px, cy - 2, 1)
&& wp_pdf_append_stroke_line(buf, x_px, cy + 2, x_px + w_px, cy + 2, 1);
case PARA_DIVIDER_DOTTED:
for (int dx = x_px; dx < x_px + w_px; dx += 5) {
int dot_w = (x_px + w_px - dx) < 2 ? (x_px + w_px - dx) : 2;
if (dot_w > 0 &&
!wp_pdf_append_stroke_line(buf, dx, cy, dx + dot_w, cy, 1))
return false;
}
return true;
case PARA_DIVIDER_DASHED:
for (int dx = x_px; dx < x_px + w_px; dx += 11) {
int dash_w = (x_px + w_px - dx) < 7 ? (x_px + w_px - dx) : 7;
if (dash_w > 0 &&
!wp_pdf_append_stroke_line(buf, dx, cy, dx + dash_w, cy, 1))
return false;
}
return true;
case PARA_DIVIDER_HEAVY:
return wp_pdf_append_stroke_line(buf, x_px, cy, x_px + w_px, cy, 3);
case PARA_DIVIDER_THIN_THICK:
return wp_pdf_append_stroke_line(buf, x_px, cy - 3, x_px + w_px, cy - 3, 1)
&& wp_pdf_append_stroke_line(buf, x_px, cy, x_px + w_px, cy, 3);
case PARA_DIVIDER_THICK_THIN:
return wp_pdf_append_stroke_line(buf, x_px, cy - 3, x_px + w_px, cy - 3, 3)
&& wp_pdf_append_stroke_line(buf, x_px, cy + 2, x_px + w_px, cy + 2, 1);
default:
return true;
}
}
static int wp_pdf_measure_line_width_hundredths(WordProcessorState* wp,
const WrapLine* wl,
const WpPdfFontDef* defs,
int def_count) {
if (!wp || !wl) return 0;
int width = 0;
int chars_left = wl->char_count;
int ri = wl->run_idx;
int ro = wl->run_offset;
while (chars_left > 0 && ri < wp->run_count) {
StyledRun* run = &wp->runs[ri];
const WpPdfFontDef* font_def = wp_pdf_find_font_def(defs, def_count, run->font_id, run->flags);
int avail = run->len - ro;
int to_measure = avail < chars_left ? avail : chars_left;
for (int ci = 0; ci < to_measure; ci++) {
unsigned char ch = (unsigned char)run->text[ro + ci];
if (ch != '\n' && (ch >= 32 || (char)ch < 0))
width += wp_pdf_glyph_advance_hundredths(font_def, run->size, ch);
}
chars_left -= to_measure;
ro += to_measure;
if (ro >= run->len) {
ri++;
ro = 0;
}
}
return width;
}
static bool wp_pdf_append_list_marker(WpPdfBuffer* buf,
WordProcessorState* wp,
WrapLine* wl,
int py,
const WpPdfFontDef* defs,
int def_count) {
if (!buf || !wp || !wl || !wl->first_in_paragraph ||
wl->paragraph_idx < 0 || wl->paragraph_idx >= wp->paragraph_count)
return true;
ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx];
if (para->divider_type != PARA_DIVIDER_NONE || para->list_type == PARA_LIST_NONE)
return true;
if (wp->run_count <= 0) return true;
StyledRun* run = &wp->runs[wl->run_idx < wp->run_count ? wl->run_idx : 0];
const WpPdfFontDef* font_def = wp_pdf_find_font_def(defs, def_count, run->font_id, run->flags);
if (!font_def) return true;
int marker_x = WP_PRINT_MARGIN_X + wp_scale_layout_units(para->left_indent + para->first_line_indent, WP_PRINT_DPI);
int min_x = WP_PRINT_MARGIN_X - WP_MARGIN + 4;
if (marker_x < min_x) marker_x = min_x;
int baseline_pt_hundredths = wp_pdf_y_points_hundredths_from_print_top(py + wl->baseline);
int marker_x_pt_hundredths = wp_pdf_points_hundredths_from_print_px(marker_x);
if (para->list_type == PARA_LIST_BULLET) {
char bullet[1] = { (char)0x95 };
return wp_pdf_append_text_op(buf, font_def, run->size,
marker_x_pt_hundredths, baseline_pt_hundredths,
bullet, 1);
}
char label[16];
int label_len = snprintf(label, sizeof(label), "%d.", wl->list_number > 0 ? wl->list_number : 1);
if (label_len <= 0) return true;
return wp_pdf_append_text_op(buf, font_def, run->size,
marker_x_pt_hundredths, baseline_pt_hundredths,
label, label_len);
}
static bool wp_pdf_build_page_stream(WordProcessorState* wp,
const int* line_pages,
const int* line_y,
int page_index,
const WpPdfFontDef* defs,
int def_count,
WpPdfPageStream* out_stream,
char* err, int err_len) {
if (!out_stream) return false;
out_stream->data = nullptr;
out_stream->len = 0;
WpPdfBuffer buf = {};
if (!wp_pdf_buffer_init(&buf, 4096)) {
wp_status_copy(err, err_len, "Out of memory while building PDF");
return false;
}
bool ok = wp_pdf_buffer_appendf(&buf, "0 g\n0 G\n");
for (int li = 0; ok && li < wp->wrap_line_count; li++) {
if (line_pages[li] != page_index) continue;
WrapLine* wl = &wp->wrap_lines[li];
int py = line_y[li];
ok = wp_pdf_append_list_marker(&buf, wp, wl, py, defs, def_count);
if (!ok) break;
if (wl->divider_type != PARA_DIVIDER_NONE) {
int divider_x = WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN);
ok = wp_pdf_append_divider_ops(&buf, divider_x, py, wl->width, wl->height, wl->divider_type);
if (!ok) break;
}
int chars_left = wl->char_count;
int ri = wl->run_idx;
int ro = wl->run_offset;
int x_pt_hundredths = wp_pdf_points_hundredths_from_print_px(WP_PRINT_MARGIN_X + (wl->x - WP_MARGIN));
if (wl->paragraph_idx >= 0 && wl->paragraph_idx < wp->paragraph_count) {
ParagraphStyle* para = &wp->paragraphs[wl->paragraph_idx];
if (para->align == PARA_ALIGN_CENTER || para->align == PARA_ALIGN_RIGHT) {
int line_width_pt_hundredths = wp_pdf_measure_line_width_hundredths(wp, wl, defs, def_count);
int width_delta = wp_pdf_points_hundredths_from_print_px(wl->width) - line_width_pt_hundredths;
if (para->align == PARA_ALIGN_CENTER) x_pt_hundredths += width_delta / 2;
else x_pt_hundredths += width_delta;
}
}
while (ok && chars_left > 0 && ri < wp->run_count) {
StyledRun* run = &wp->runs[ri];
const WpPdfFontDef* font_def = wp_pdf_find_font_def(defs, def_count, run->font_id, run->flags);
if (!font_def) {
ri++;
ro = 0;
continue;
}
int avail = run->len - ro;
int to_draw = avail < chars_left ? avail : chars_left;
char segment[512];
int seg_len = 0;
int seg_x_pt_hundredths = x_pt_hundredths;
int baseline_px = py + wl->baseline;
for (int ci = 0; ci < to_draw; ci++) {
char ch = run->text[ro + ci];
int advance_hundredths = 0;
if (ch != '\n' && (ch >= 32 || ch < 0))
advance_hundredths = wp_pdf_glyph_advance_hundredths(font_def, run->size, (unsigned char)ch);
if (ch != '\n' && (ch >= 32 || ch < 0)) {
if (seg_len >= (int)sizeof(segment)) {
ok = wp_pdf_append_text_op(&buf, font_def, run->size,
seg_x_pt_hundredths,
wp_pdf_y_points_hundredths_from_print_top(baseline_px),
segment, seg_len);
if (!ok) break;
seg_len = 0;
seg_x_pt_hundredths = x_pt_hundredths;
}
segment[seg_len++] = ch;
}
x_pt_hundredths += advance_hundredths;
}
if (seg_len > 0) {
ok = wp_pdf_append_text_op(&buf, font_def, run->size,
seg_x_pt_hundredths,
wp_pdf_y_points_hundredths_from_print_top(baseline_px),
segment, seg_len);
}
chars_left -= to_draw;
ro += to_draw;
if (ro >= run->len) {
ri++;
ro = 0;
}
}
}
if (!ok) {
wp_status_copy(err, err_len, "Failed while writing PDF page data");
wp_pdf_buffer_free(&buf);
return false;
}
out_stream->data = buf.data;
out_stream->len = buf.len;
return true;
}
static bool wp_pdf_append_font_widths(WpPdfBuffer* pdf, const WpPdfFontDef* def) {
if (!pdf || !def || !def->font || !def->font->valid) return false;
if (!wp_pdf_buffer_appendf(pdf, "/Widths [")) return false;
for (int ch = 32; ch <= 255; ch++) {
int advance = 0;
int lsb = 0;
int cp = decode_single_byte_codepoint(ch);
stbtt_GetCodepointHMetrics(&def->font->info, cp, &advance, &lsb);
int width = (advance * 1000 + def->units_per_em / 2) / def->units_per_em;
if (width < 0) width = 0;
if (!wp_pdf_buffer_appendf(pdf, "%d%s", width, ch == 255 ? "" : " "))
return false;
}
return wp_pdf_buffer_appendf(pdf, "] ");
}
bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_status, int out_status_len) {
if (out_status && out_status_len > 0) out_status[0] = '\0';
if (!wp) {
wp_status_copy(out_status, out_status_len, "No document loaded");
return false;
}
char final_path[256] = {};
char err[192] = {};
if (!wp_pdf_normalize_path(path, final_path, sizeof(final_path), err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
return false;
}
int* line_pages = nullptr;
int* line_y = nullptr;
int page_count = 0;
if (!wp_build_print_page_map(wp, &line_pages, &line_y, &page_count, err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
return false;
}
bool success = false;
// Sized for every discovered family in all four styles; heap-allocated to
// keep the deep PDF call chain off an already-tight user stack.
WpPdfFontDef* fonts = nullptr;
int font_cap = wp_font_count() > 0 ? wp_font_count() * 4 : 4;
int font_count = 0;
WpPdfPageStream* page_streams = nullptr;
int* page_obj_nums = nullptr;
int* content_obj_nums = nullptr;
int* xref = nullptr;
WpPdfBuffer pdf = {};
int next_obj = 0;
int total_objects = 0;
fonts = (WpPdfFontDef*)montauk::malloc((uint64_t)font_cap * sizeof(WpPdfFontDef));
if (!fonts) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
montauk::memset(fonts, 0, (uint64_t)font_cap * sizeof(WpPdfFontDef));
if (!wp_pdf_collect_fonts(wp, fonts, &font_count, err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
goto cleanup;
}
page_streams = (WpPdfPageStream*)montauk::malloc((uint64_t)page_count * sizeof(WpPdfPageStream));
if (!page_streams) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
montauk::memset(page_streams, 0, (uint64_t)page_count * sizeof(WpPdfPageStream));
for (int page = 0; page < page_count; page++) {
if (!wp_pdf_build_page_stream(wp, line_pages, line_y, page, fonts, font_count,
&page_streams[page], err, sizeof(err))) {
wp_status_copy(out_status, out_status_len, err);
goto cleanup;
}
}
next_obj = 3;
for (int i = 0; i < font_count; i++) {
fonts[i].font_obj_num = next_obj++;
fonts[i].descriptor_obj_num = next_obj++;
fonts[i].file_obj_num = next_obj++;
}
page_obj_nums = (int*)montauk::malloc((uint64_t)page_count * sizeof(int));
content_obj_nums = (int*)montauk::malloc((uint64_t)page_count * sizeof(int));
if (!page_obj_nums || !content_obj_nums) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
for (int page = 0; page < page_count; page++) {
page_obj_nums[page] = next_obj++;
content_obj_nums[page] = next_obj++;
}
total_objects = next_obj - 1;
xref = (int*)montauk::malloc((uint64_t)(total_objects + 1) * sizeof(int));
if (!xref) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
montauk::memset(xref, 0, (uint64_t)(total_objects + 1) * sizeof(int));
if (!wp_pdf_buffer_init(&pdf, 16384)) {
wp_status_copy(out_status, out_status_len, "Out of memory while exporting PDF");
goto cleanup;
}
if (!wp_pdf_buffer_appendf(&pdf, "%%PDF-1.4\n%%MontaukOS\n")) {
wp_status_copy(out_status, out_status_len, "Failed to build PDF output");
goto cleanup;
}
xref[1] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")) {
wp_status_copy(out_status, out_status_len, "Failed to build PDF output");
goto cleanup;
}
xref[2] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf, "2 0 obj\n<< /Type /Pages /Kids [")) {
wp_status_copy(out_status, out_status_len, "Failed to build PDF output");
goto cleanup;
}
for (int page = 0; page < page_count; page++) {
if (!wp_pdf_buffer_appendf(&pdf, "%d 0 R%s", page_obj_nums[page], page == page_count - 1 ? "" : " ")) {
wp_status_copy(out_status, out_status_len, "Failed to build PDF output");
goto cleanup;
}
}
if (!wp_pdf_buffer_appendf(&pdf, "] /Count %d >>\nendobj\n", page_count)) {
wp_status_copy(out_status, out_status_len, "Failed to build PDF output");
goto cleanup;
}
for (int i = 0; i < font_count; i++) {
WpPdfFontDef* def = &fonts[i];
xref[def->font_obj_num] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf,
"%d 0 obj\n<< /Type /Font /Subtype /TrueType /Name /%s "
"/BaseFont /%s /FirstChar 32 /LastChar 255 ",
def->font_obj_num, def->resource_name, def->base_font)
|| !wp_pdf_append_font_widths(&pdf, def)
|| !wp_pdf_buffer_appendf(&pdf,
"/Encoding /WinAnsiEncoding /FontDescriptor %d 0 R >>\nendobj\n",
def->descriptor_obj_num)) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF font data");
goto cleanup;
}
xref[def->descriptor_obj_num] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf,
"%d 0 obj\n<< /Type /FontDescriptor /FontName /%s "
"/Flags %d /FontBBox [%d %d %d %d] /ItalicAngle %d "
"/Ascent %d /Descent %d /CapHeight %d /StemV 80 "
"/FontFile2 %d 0 R >>\nendobj\n",
def->descriptor_obj_num, def->base_font,
def->descriptor_flags,
def->bbox_x0, def->bbox_y0, def->bbox_x1, def->bbox_y1,
def->italic_angle, def->ascent, def->descent,
def->cap_height, def->file_obj_num)) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF font data");
goto cleanup;
}
xref[def->file_obj_num] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf, "%d 0 obj\n<< /Length %d >>\nstream\n",
def->file_obj_num, def->file_len)
|| !wp_pdf_buffer_append_bytes(&pdf, def->file_data, def->file_len)
|| !wp_pdf_buffer_appendf(&pdf, "\nendstream\nendobj\n")) {
wp_status_copy(out_status, out_status_len, "Failed to encode embedded PDF fonts");
goto cleanup;
}
}
for (int page = 0; page < page_count; page++) {
xref[page_obj_nums[page]] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf,
"%d 0 obj\n<< /Type /Page /Parent 2 0 R "
"/MediaBox [0 0 ",
page_obj_nums[page])
|| !wp_pdf_buffer_append_number_hundredths(&pdf, wp_pdf_page_width_points_hundredths())
|| !wp_pdf_buffer_appendf(&pdf, " ")
|| !wp_pdf_buffer_append_number_hundredths(&pdf, wp_pdf_page_height_points_hundredths())
|| !wp_pdf_buffer_appendf(&pdf, "] /Resources <<")) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
if (font_count > 0) {
if (!wp_pdf_buffer_appendf(&pdf, " /Font <<")) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
for (int i = 0; i < font_count; i++) {
if (!wp_pdf_buffer_appendf(&pdf, " /%s %d 0 R",
fonts[i].resource_name, fonts[i].font_obj_num)) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
}
if (!wp_pdf_buffer_appendf(&pdf, " >>")) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
}
if (!wp_pdf_buffer_appendf(&pdf, " >> /Contents %d 0 R >>\nendobj\n", content_obj_nums[page])) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
xref[content_obj_nums[page]] = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf, "%d 0 obj\n<< /Length %d >>\nstream\n",
content_obj_nums[page], page_streams[page].len)
|| !wp_pdf_buffer_append_bytes(&pdf, page_streams[page].data, page_streams[page].len)
|| !wp_pdf_buffer_appendf(&pdf, "\nendstream\nendobj\n")) {
wp_status_copy(out_status, out_status_len, "Failed to encode PDF pages");
goto cleanup;
}
}
{
int xref_pos = pdf.len;
if (!wp_pdf_buffer_appendf(&pdf, "xref\n0 %d\n", total_objects + 1)
|| !wp_pdf_buffer_appendf(&pdf, "%010d 65535 f \n", 0)) {
wp_status_copy(out_status, out_status_len, "Failed to finalize PDF output");
goto cleanup;
}
for (int i = 1; i <= total_objects; i++) {
if (!wp_pdf_buffer_appendf(&pdf, "%010d 00000 n \n", xref[i])) {
wp_status_copy(out_status, out_status_len, "Failed to finalize PDF output");
goto cleanup;
}
}
if (!wp_pdf_buffer_appendf(&pdf,
"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n",
total_objects + 1, xref_pos)) {
wp_status_copy(out_status, out_status_len, "Failed to finalize PDF output");
goto cleanup;
}
}
{
int fd = montauk::fcreate(final_path);
if (fd < 0) {
snprintf(err, sizeof(err), "Cannot create %s", final_path);
wp_status_copy(out_status, out_status_len, err);
goto cleanup;
}
int wrote = montauk::fwrite(fd, pdf.data, 0, (uint64_t)pdf.len);
montauk::close(fd);
if (wrote < 0 || wrote != pdf.len) {
snprintf(err, sizeof(err), "Failed to write %s", final_path);
wp_status_copy(out_status, out_status_len, err);
goto cleanup;
}
}
{
char summary[192];
snprintf(summary, sizeof(summary),
"Exported PDF to %s (%d page%s)",
final_path, page_count, page_count == 1 ? "" : "s");
wp_status_copy(out_status, out_status_len, summary);
}
success = true;
cleanup:
if (page_streams) {
for (int i = 0; i < page_count; i++) {
if (page_streams[i].data)
montauk::mfree(page_streams[i].data);
}
montauk::mfree(page_streams);
}
if (page_obj_nums) montauk::mfree(page_obj_nums);
if (content_obj_nums) montauk::mfree(content_obj_nums);
if (xref) montauk::mfree(xref);
if (line_pages) montauk::mfree(line_pages);
if (line_y) montauk::mfree(line_y);
wp_pdf_free_fonts(fonts, font_count);
if (fonts) montauk::mfree(fonts);
wp_pdf_buffer_free(&pdf);
return success;
}