diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index c30ba94..f4cf435 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -29,6 +29,42 @@ static void wp_open_print_dialog(WordProcessorState* wp) { wp_set_status(wp, msg); } +static void wp_make_pdf_suggested_name(WordProcessorState* wp, char* out, int out_len) { + if (!out || out_len <= 0) return; + + const char* src = (wp && wp->filename[0]) ? wp->filename : "document"; + int last_dot = -1; + for (int i = 0; src[i]; i++) { + if (src[i] == '.') last_dot = i; + } + + int copy_len = (last_dot > 0) ? last_dot : montauk::slen(src); + if (copy_len > out_len - 5) copy_len = out_len - 5; + if (copy_len < 0) copy_len = 0; + montauk::memcpy(out, src, (uint64_t)copy_len); + out[copy_len] = '\0'; + montauk::strncpy(out + copy_len, ".pdf", out_len - copy_len - 1); + out[out_len - 1] = '\0'; +} + +static void wp_open_export_dialog(WordProcessorState* wp) { + char path[256] = {}; + char suggested_name[128] = {}; + char msg[160] = {}; + + wp_make_pdf_suggested_name(wp, suggested_name, sizeof(suggested_name)); + if (dialogs::save_file("Export PDF", + wp->filepath, + suggested_name, + path, sizeof(path), + msg, sizeof(msg))) { + if (wp_export_pdf_document(wp, path, msg, sizeof(msg)) || msg[0]) + wp_set_status(wp, msg); + } else if (msg[0]) { + wp_set_status(wp, msg); + } +} + static void wp_commit_pathbar(WordProcessorState* wp) { if (!wp->pathbar_text[0]) return; if (wp->pathbar_save_mode) { @@ -227,6 +263,10 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) { wp_open_print_dialog(wp); return; } + if (local_x >= WP_BTN_EXPORT_X && local_x < WP_BTN_EXPORT_X + 24 && local_y >= 6 && local_y < 30) { + wp_open_export_dialog(wp); + return; + } if (local_x >= WP_BTN_UNDO_X && local_x < WP_BTN_UNDO_X + 24 && local_y >= 6 && local_y < 30) { wp_close_dropdowns(wp); wp_undo(wp); @@ -425,7 +465,11 @@ void wp_handle_key(const Montauk::KeyEvent& key) { wp_open_pathbar_for_open(wp); return; } - if (key.ctrl && (key.ascii == 'p' || key.ascii == 'P')) { + if (key.ctrl && key.alt && (key.ascii == 'p' || key.ascii == 'P')) { + wp_open_export_dialog(wp); + return; + } + if (key.ctrl && !key.alt && (key.ascii == 'p' || key.ascii == 'P')) { wp_open_print_dialog(wp); return; } diff --git a/programs/src/wordprocessor/main.cpp b/programs/src/wordprocessor/main.cpp index b8250c6..aceaca6 100644 --- a/programs/src/wordprocessor/main.cpp +++ b/programs/src/wordprocessor/main.cpp @@ -14,6 +14,7 @@ WPFontTable g_wp_fonts = { {{nullptr}}, false }; SvgIcon g_icon_folder = {}; SvgIcon g_icon_save = {}; SvgIcon g_icon_print = {}; +SvgIcon g_icon_export_pdf = {}; SvgIcon g_icon_undo = {}; SvgIcon g_icon_redo = {}; SvgIcon g_icon_align_left = {}; @@ -34,6 +35,11 @@ void wp_load_icons() { g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, def_color); if (!g_icon_print.pixels) g_icon_print = svg_load("0:/icons/printer-symbolic.svg", 16, 16, def_color); + if (!g_icon_export_pdf.pixels) { + g_icon_export_pdf = svg_load("0:/icons/document-export-symbolic.svg", 16, 16, def_color); + if (!g_icon_export_pdf.pixels) + g_icon_export_pdf = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, def_color); + } if (!g_icon_undo.pixels) g_icon_undo = svg_load("0:/icons/edit-undo-symbolic.svg", 16, 16, def_color); if (!g_icon_redo.pixels) @@ -59,6 +65,7 @@ void wp_cleanup_state() { if (g_icon_folder.pixels) svg_free(g_icon_folder); if (g_icon_save.pixels) svg_free(g_icon_save); if (g_icon_print.pixels) svg_free(g_icon_print); + if (g_icon_export_pdf.pixels) svg_free(g_icon_export_pdf); if (g_icon_undo.pixels) svg_free(g_icon_undo); if (g_icon_redo.pixels) svg_free(g_icon_redo); if (g_icon_align_left.pixels) svg_free(g_icon_align_left); diff --git a/programs/src/wordprocessor/print_export.cpp b/programs/src/wordprocessor/print_export.cpp index 996fd55..9dbe01a 100644 --- a/programs/src/wordprocessor/print_export.cpp +++ b/programs/src/wordprocessor/print_export.cpp @@ -10,6 +10,7 @@ extern "C" { #include +#include #include #define STBI_WRITE_NO_STDIO @@ -476,3 +477,997 @@ bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_ 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; +} + +static const char* wp_pdf_font_path(uint8_t font_id, uint8_t style_index) { + static const char* PATHS[FONT_COUNT][4] = { + { + "0:/fonts/Roboto-Medium.ttf", + "0:/fonts/Roboto-Bold.ttf", + "0:/fonts/Roboto-Italic.ttf", + "0:/fonts/Roboto-BoldItalic.ttf", + }, + { + "0:/fonts/NotoSerif-Regular.ttf", + "0:/fonts/NotoSerif-SemiBold.ttf", + "0:/fonts/NotoSerif-Italic.ttf", + "0:/fonts/NotoSerif-BoldItalic.ttf", + }, + { + "0:/fonts/C059-Roman.ttf", + "0:/fonts/C059-Bold.ttf", + "0:/fonts/C059-Italic.ttf", + "0:/fonts/C059-Bold.ttf", + }, + }; + + if (font_id >= FONT_COUNT || style_index > 3) return nullptr; + return PATHS[font_id][style_index]; +} + +static const char* wp_pdf_base_font_name(uint8_t font_id, uint8_t style_index) { + static const char* NAMES[FONT_COUNT][4] = { + { + "Roboto-Medium", + "Roboto-Bold", + "Roboto-Italic", + "Roboto-BoldItalic", + }, + { + "NotoSerif-Regular", + "NotoSerif-SemiBold", + "NotoSerif-Italic", + "NotoSerif-BoldItalic", + }, + { + "C059-Roman", + "C059-Bold", + "C059-Italic", + "C059-Bold", + }, + }; + + if (font_id >= FONT_COUNT || style_index > 3) return nullptr; + return NAMES[font_id][style_index]; +} + +static int wp_pdf_descriptor_flags(uint8_t font_id, uint8_t style_index) { + int flags = 32; // Nonsymbolic + if (font_id == FONT_NOTOSERIF || font_id == FONT_C059) + 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[FONT_COUNT][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 < FONT_COUNT ? run->font_id : FONT_ROBOTO; + 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 < 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 < FONT_COUNT ? font_id : FONT_ROBOTO; + 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; + WpPdfFontDef fonts[FONT_COUNT * 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; + + 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); + wp_pdf_buffer_free(&pdf); + + return success; +} diff --git a/programs/src/wordprocessor/render.cpp b/programs/src/wordprocessor/render.cpp index ff42ffd..bf5affd 100644 --- a/programs/src/wordprocessor/render.cpp +++ b/programs/src/wordprocessor/render.cpp @@ -208,7 +208,10 @@ void wp_render() { if (g_icon_print.pixels) c.icon(WP_BTN_PRINT_X + 4, 10, g_icon_print); - c.vline(WP_BTN_PRINT_X + 28, 4, 28, colors::BORDER); + wp_draw_ui_icon_button(c, WP_BTN_EXPORT_X, 6, 24, 24, g_icon_export_pdf, "P", + btn_bg, colors::TEXT_COLOR); + + c.vline(WP_BTN_EXPORT_X + 28, 4, 28, colors::BORDER); wp_draw_ui_icon_button(c, WP_BTN_UNDO_X, 6, 24, 24, g_icon_undo, "U", btn_bg, colors::TEXT_COLOR); wp_draw_ui_icon_button(c, WP_BTN_REDO_X, 6, 24, 24, g_icon_redo, "R", btn_bg, colors::TEXT_COLOR); diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index 4292e9b..c4cfcb6 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -44,25 +44,26 @@ static constexpr int WP_LIST_MARKER_W = 20; static constexpr int WP_BTN_OPEN_X = 4; static constexpr int WP_BTN_SAVE_X = 32; static constexpr int WP_BTN_PRINT_X = 60; -static constexpr int WP_BTN_UNDO_X = 94; -static constexpr int WP_BTN_REDO_X = 122; -static constexpr int WP_BTN_BOLD_X = 156; -static constexpr int WP_BTN_ITALIC_X = 184; -static constexpr int WP_FONT_DD_X = 216; +static constexpr int WP_BTN_EXPORT_X = 88; +static constexpr int WP_BTN_UNDO_X = 122; +static constexpr int WP_BTN_REDO_X = 150; +static constexpr int WP_BTN_BOLD_X = 184; +static constexpr int WP_BTN_ITALIC_X = 212; +static constexpr int WP_FONT_DD_X = 244; static constexpr int WP_FONT_DD_W = 74; -static constexpr int WP_SIZE_DD_X = 296; +static constexpr int WP_SIZE_DD_X = 324; static constexpr int WP_SIZE_DD_W = 36; -static constexpr int WP_BTN_ALIGN_L_X = 344; -static constexpr int WP_BTN_ALIGN_C_X = 372; -static constexpr int WP_BTN_ALIGN_R_X = 400; -static constexpr int WP_BTN_BULLET_X = 432; -static constexpr int WP_BTN_NUMBER_X = 460; -static constexpr int WP_BTN_OUTDENT_X = 496; -static constexpr int WP_BTN_INDENT_X = 524; -static constexpr int WP_LINE_DD_X = 558; +static constexpr int WP_BTN_ALIGN_L_X = 372; +static constexpr int WP_BTN_ALIGN_C_X = 400; +static constexpr int WP_BTN_ALIGN_R_X = 428; +static constexpr int WP_BTN_BULLET_X = 460; +static constexpr int WP_BTN_NUMBER_X = 488; +static constexpr int WP_BTN_OUTDENT_X = 524; +static constexpr int WP_BTN_INDENT_X = 552; +static constexpr int WP_LINE_DD_X = 586; static constexpr int WP_LINE_DD_W = 48; -static constexpr int WP_BTN_DIVIDER_X = 614; -static constexpr int WP_BTN_SECTION_X = 646; +static constexpr int WP_BTN_DIVIDER_X = 642; +static constexpr int WP_BTN_SECTION_X = 674; static constexpr int WP_DIVIDER_ROW_H = 24; static constexpr int WP_DIVIDER_FLYOUT_W = 216; static constexpr int WP_SPECIAL_CHAR_ROW_H = 24; @@ -371,6 +372,7 @@ extern WPFontTable g_wp_fonts; extern SvgIcon g_icon_folder; extern SvgIcon g_icon_save; extern SvgIcon g_icon_print; +extern SvgIcon g_icon_export_pdf; extern SvgIcon g_icon_undo; extern SvgIcon g_icon_redo; extern SvgIcon g_icon_align_left; @@ -441,6 +443,7 @@ bool wp_undo(WordProcessorState* wp); bool wp_redo(WordProcessorState* wp); bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len); +bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_status, int out_status_len); void wp_render(); void wp_handle_mouse(const Montauk::WinEvent& ev); diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index b49cb7b..e41f01c 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -24,6 +24,7 @@ ICONS=( "actions/symbolic/go-next-symbolic.svg" "actions/symbolic/go-home-symbolic.svg" "actions/symbolic/document-save-symbolic.svg" + "actions/symbolic/document-export-symbolic.svg" "actions/symbolic/edit-undo-symbolic.svg" "actions/symbolic/edit-redo-symbolic.svg" "actions/symbolic/format-justify-left-symbolic.svg" diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index 91b9976..a3182e6 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -22,7 +22,7 @@ APPS=( "wikipedia|apps/scalable/web-browser.svg" "imageviewer|apps/scalable/utilities-terminal.svg" "fontpreview|apps/scalable/utilities-terminal.svg" - "pdfviewer|apps/scalable/utilities-terminal.svg" + "pdfviewer|mimetypes/scalable/application-pdf.svg" "disks|apps/scalable/gparted.svg" "devexplorer|apps/scalable/hardware.svg" "installer|mimetypes/scalable/text-x-install.svg"