1459 lines
54 KiB
C++
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;
|
|
}
|