/* * print_export.cpp * Word Processor print export and job submission * Copyright (c) 2026 Daniel Hammer */ #include "wordprocessor.hpp" #include #include extern "C" { #include #include #include #define STBI_WRITE_NO_STDIO #include } 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; }