From f4c1a1665b162aa5e5922284f30e36e4fe4aaed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 03:17:08 -0400 Subject: [PATCH] ElyPDF: the document's font writes through its own object and codes; shared glyphs keep their letters (R2.17) --- agents.md | 2 +- crates/ely-pdf-engine/build.rs | 2 + crates/ely-pdf-engine/shim/ely_pen.c | 183 ++++++++++++++++ crates/ely-pdf-engine/shim/ely_shim.h | 30 +++ crates/ely-pdf-engine/shim/ely_write.c | 232 ++------------------- crates/ely-pdf-engine/tests/content.rs | 45 ++++ crates/ely-pdf-engine/tests/independent.rs | 25 +++ 7 files changed, 298 insertions(+), 221 deletions(-) create mode 100644 crates/ely-pdf-engine/shim/ely_pen.c diff --git a/agents.md b/agents.md index 285679b..519546a 100644 --- a/agents.md +++ b/agents.md @@ -262,5 +262,5 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-06:ElyPDF 阅读(C11.1,PDF-01)。MuPDF 只链进 `ely-pdf-worker`(`crates/ely-pdf-engine`,build.rs 把 C 垫片 `shim/ely_mupdf.c` 与 `libmupdf.a`、`libmupdf-third.a` 只链给这个二进制),应用不链 MuPDF。build.rs 以协议、垫片与 worker 源码的散列为版本戳,应用启动 worker 先握手(`HELLO`),戳不同即拒绝(单测不重建 worker,旧二进制曾被误用)。worker 在打开第一个文件时才启动(PERF-01),`tests/scripts/pdf-idle.sh` 查空白窗口无子进程。垫片每个调用都在 `fz_try`/`fz_catch` 里,返回码加最后一条错误信息,MuPDF 的跳转从不越过 Rust。worker 与应用之间是 `ely-ipc` 的帧(从 ely-lok 抽出,两者共用):打开(锁定即报)、解锁、页数、页框、渲染一块(设备像素、缩放、旋转、深色)、按阅读顺序的字符与外框、页内搜索(每处命中及其全部四边形,跨行短语算一处,数量不设上限)、书签(JSON)、关闭;文件读不出只报错,worker 照常工作。查看器:打开时只读页数、前 100 页尺寸与书签,其余尺寸后台每批 100 页补齐;视图只排屏上的页(连续、单页、双页;适合宽度、适合页面、实际大小与缩放档;视图旋转 90°),按 512 设备像素一块请求,一次一块、由近及远,只留屏上的块(横纵两向),后台回来的块不在屏上即弃;画不出的块记下不再请求,换文档或重开前不重试;深色外观下页面按亮度反相(MuPDF `fz_invert_pixmap_luminance`,色相保留)。文字选择按字符、可跨页,复制按行换行,未读的页先读;查找每批 20 页在后台进行,命中逐个跳转并标出,Esc 关闭(组件库 617a8d6);书签栏用 `lists::Outline`,点即跳到页与位置。切换视图、缩放、旋转后仍停在正在读的那页。加密文件打开时问密码(外壳的密码框,见上一条),错了再问,取消即不打开。没有页的文件报错并关闭,不开标签。双页时同一行在视口内的两页都可为当前页;当前页只在横纵两向都可见的页里定,跳转在目标页左缘不在视口时横向移过去。图块按视口半开区间取。首次按下时文字未读,读到后照原按下处起选;查找结果按查找实例与所属标签核对,只在该标签为当前时跳转。书签为空时不留左栏。启动参数里的文件直接打开。 - 2026-10-09(用户定:“打开方式”里不得出现 LibreOffice):系统会登记磁盘上能找到的 .app、挂载镜像里的 .app 与运行过的包,构建目录里的 `LibreOffice.app` 与测试挂载的官方镜像曾因此成为 xlsx 等的默认程序。现构建后由 `scripts/inert-lo.sh` 去掉其全部文件与 URL 声明、快速查看与聚焦插件,注销它,并在 `engines/` 放 `.metadata_never_index`;Ely 只取其 Frameworks 与 Resources。任何脚本与测试都不挂载 LibreOffice 镜像:LOK 探测用本构建的内核,单测所需的官方内核由 `scripts/extract-official-lo.sh` 以 7zz 解成无包的文件夹。`tests/scripts/lo-unregistered.sh` 以 NSWorkspace 核对 docx、xlsx、pptx、odt、csv 的“打开方式”里没有 LibreOffice。 - 2026-10-09(用户报:ElyTable 点格跳到别处、拖不出选区):Calc 在 `sc_print_twips_msgs` 下以打印 twips(行高原值)报光标、选区与 `.uno:SheetGeometryData`,图块与鼠标却在图块 twips(每行按 `ScViewData::ToPixel` 截成整像素),两者随行号漂移(5000 行处约 140 行)。视图对外一律打印 twips;`LokView::set_geometry` 收下工作表几何后,`sheet::Axis::tile`、`print` 照内核的截断换算(每像素 twips 取图块的像素与 twips 之比),绘制、显示、跳转与冻结都先换成图块 twips,鼠标以图块 twips 发出(`tile_point`),拖动句柄仍为打印 twips;`.uno:` 读回与应用(表头、填充柄)照旧用打印 twips,表头的起点取 `LokView::scrolled_start`。内核读鼠标用的比例原取最后一块图块,worker 现于每次 `setClientVisibleArea` 前以 `setClientZoom` 告知图块比例,点击不再依赖先画过图块。 -- 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时,从该页字体资源里找到同一字体(`pdf_load_font` 按对象缓存,与文字层的字体同一对象),按文字提取的读法(ToUnicode 或 cid_to_ucs、`pdf_font_cid_to_gid`)倒出 Unicode 到字形号的表,写字只用页面上已有的字形;新字体的 `/ToUnicode` 以这张表重写(MuPDF 按子集的占位表生成的那份会把新字读成乱码)。页面上没有的字仍报缺字。 +- 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时直接用页面上的那个字体对象(`shim/ely_pen.c`;`pdf_load_font` 按对象缓存,与文字层的字体同一对象),以它自己的编码写:按文字提取的读法(ToUnicode 或 cid_to_ucs)倒出 Unicode 到编码的表,排版取它的宽度(`pdf_lookup_hmtx`),原有的 ToUnicode 与宽度不动,两个字共用一个字形时各写各的编码(原先按字形号写、重建 ToUnicode,共用字形的字被改写,且 CMap 漏写条目数,PDFKit 读不出字)。删去原文字块后页面资源里可能已无该字体,故按对象号以新名字重新引用。只支持编码即 CID 的字体(简单字体与 Identity-H),其余拒绝并说明;页面上没有的字仍报缺字。排版按同一套宽度量出的框,比较时留 0.01 点的余量(原块宽度即这些宽度之和,浮点误差曾使末字折行)。 - 2026-10-09:ElyTable 的自动筛选(R2.9)。内核每次打开筛选列表都把未命名的筛选区向下扩到紧邻的数据行(为接上后来追加的行),分不出选区时就在下方的合计行,合计因此进了列表、随数据一起被隐藏。补丁 `0040-lok-calc-autofilter-keeps-a-marked-block.patch`:LOK 下对跨行的选区开自动筛选时,筛选区记下“选定的行”(不存入文件),此后不再扩展;单格开筛选照旧取整个数据区。已知限制:存后重开的文件不带此记号,其筛选区照内核原样扩展。 diff --git a/crates/ely-pdf-engine/build.rs b/crates/ely-pdf-engine/build.rs index 4305f90..a0454bf 100644 --- a/crates/ely-pdf-engine/build.rs +++ b/crates/ely-pdf-engine/build.rs @@ -13,6 +13,7 @@ fn main() { "shim/ely_insert.c", "shim/ely_text.c", "shim/ely_write.c", + "shim/ely_pen.c", "shim/ely_place.c", "shim/ely_annot.c", "shim/ely_form.c", @@ -68,6 +69,7 @@ fn main() { .file("shim/ely_insert.c") .file("shim/ely_text.c") .file("shim/ely_write.c") + .file("shim/ely_pen.c") .file("shim/ely_place.c") .file("shim/ely_annot.c") .file("shim/ely_form.c") diff --git a/crates/ely-pdf-engine/shim/ely_pen.c b/crates/ely-pdf-engine/shim/ely_pen.c new file mode 100644 index 0000000..bd10477 --- /dev/null +++ b/crates/ely-pdf-engine/shim/ely_pen.c @@ -0,0 +1,183 @@ +/* The font new text is written in (PDF-03). */ + +#include + +#include "ely_shim.h" + +/* The script face for typed signatures, from the worker. */ +static const unsigned char *script_data; +static size_t script_len; + +void ely_script_font(const unsigned char *data, size_t len) +{ + script_data = data; + script_len = len; +} + +/* Unicode and code pairs of `desc`; its codes are CIDs. */ +static void own_codes(fz_context *ctx, pdf_font_desc *desc, ely_pen *p) +{ + pdf_cmap *enc = desc->encoding; + int cid, end; + if (!enc || strcmp(enc->cmap_name, "Identity-H") || enc->codespace_len != 1 || enc->codespace[0].n > 2) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font uses codes Ely cannot write"); + p->bytes = enc->codespace[0].n; + end = 1 << (8 * p->bytes); + p->map = fz_malloc_array(ctx, 2 * end, int); + for (cid = 0; cid < end; cid++) + { + int u = desc->to_unicode ? pdf_lookup_cmap(desc->to_unicode, cid) + : cid < (int)desc->cid_to_ucs_len ? desc->cid_to_ucs[cid] : 0; + if (u > 0 && pdf_font_cid_to_gid(ctx, desc, cid) > 0) + p->map[2 * p->mapped] = u, p->map[2 * p->mapped + 1] = cid, p->mapped++; + } +} + +/* The page font drawn as `own`, with its codes. */ +static void own_font(fz_context *ctx, pdf_page *page, fz_font *own, ely_pen *p) +{ + pdf_obj *res = pdf_page_resources(ctx, page); + pdf_resource_stack stack = { NULL, res }; + pdf_obj *fonts = pdf_dict_get(ctx, res, PDF_NAME(Font)); + int i, n = pdf_dict_len(ctx, fonts); + for (i = 0; i < n && !p->desc; i++) + { + pdf_font_desc *desc = pdf_load_font(ctx, page->doc, &stack, pdf_dict_get_val(ctx, fonts, i)); + if (desc->font != own) + { + pdf_drop_font(ctx, desc); + continue; + } + p->desc = desc; + p->num = pdf_to_num(ctx, pdf_dict_get_val(ctx, fonts, i)); + if (!p->num) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font is not an object of its own"); + own_codes(ctx, desc, p); + } + if (!p->desc) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "the block's font is not a font of its page"); +} + +void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p) +{ + static const struct { const char *choice, *name; } latin[] = { + { "helvetica", "Helvetica" }, { "times", "Times-Roman" }, { "courier", "Courier" } }; + static const struct { const char *choice; int ordering; } cjk[] = { + { "cjk-sc", FZ_ADOBE_GB }, { "cjk-tc", FZ_ADOBE_CNS }, { "cjk-jp", FZ_ADOBE_JAPAN }, { "cjk-kr", FZ_ADOBE_KOREA } }; + size_t i; + if (!strcmp(choice, "script")) + { + if (!script_data) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "no script font was given"); + p->kind = PEN_CID; + p->font = fz_new_font_from_memory(ctx, "GreatVibes", script_data, (int)script_len, 0, 0); + return; + } + if (!strcmp(choice, "original")) + { + if (!own) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "the block has no font of its own"); + p->kind = PEN_OWN; + own_font(ctx, page, own, p); + return; + } + for (i = 0; i < nelem(latin); i++) + if (!strcmp(choice, latin[i].choice)) + { + p->kind = PEN_SIMPLE; + p->font = fz_new_base14_font(ctx, latin[i].name); + return; + } + for (i = 0; i < nelem(cjk); i++) + if (!strcmp(choice, cjk[i].choice)) + { + p->kind = PEN_CJK; + p->ordering = cjk[i].ordering; + p->font = fz_new_cjk_font(ctx, cjk[i].ordering); + return; + } + fz_throw(ctx, FZ_ERROR_ARGUMENT, "no font %s", choice); +} + +const char *ely_pen_resource(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_pen *p, char *name, size_t room) +{ + pdf_drop_obj(ctx, p->ref); + p->ref = NULL; + switch (p->kind) + { + case PEN_OWN: + /* Removing the block may drop the font from the page. */ + p->ref = pdf_new_indirect(ctx, pdf, p->num, 0); + break; + case PEN_CID: + p->ref = pdf_add_cid_font(ctx, pdf, p->font); + break; + case PEN_SIMPLE: + p->ref = pdf_add_simple_font(ctx, pdf, p->font, PDF_SIMPLE_ENCODING_LATIN); + break; + case PEN_CJK: + p->ref = pdf_add_cjk_font(ctx, pdf, p->font, p->ordering, 0, 0); + break; + } + return ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyF", p->ref, name, room); +} + +/* The own font's code for `u`, or -1. */ +static int own_code(ely_pen *p, int u) +{ + int i; + for (i = 0; i < p->mapped; i++) + if (p->map[2 * i] == u) + return p->map[2 * i + 1]; + return -1; +} + +/* The new font's glyph for `u`, or 0. */ +static int glyph(fz_context *ctx, ely_pen *p, int u) +{ + if (p->kind == PEN_SIMPLE && fz_windows_1252_from_unicode(u) < 0) + return 0; + return fz_encode_character(ctx, p->font, u); +} + +int ely_pen_has(fz_context *ctx, ely_pen *p, int u) +{ + return p->kind == PEN_OWN ? own_code(p, u) >= 0 : glyph(ctx, p, u) > 0; +} + +float ely_pen_advance(fz_context *ctx, ely_pen *p, int u, float size) +{ + if (p->kind == PEN_OWN) + return pdf_lookup_hmtx(ctx, p->desc, own_code(p, u)).w * size / 1000; + return fz_advance_glyph(ctx, p->font, glyph(ctx, p, u), 0) * size; +} + +void ely_pen_code(fz_context *ctx, fz_buffer *buf, ely_pen *p, int u) +{ + switch (p->kind) + { + case PEN_OWN: + fz_append_printf(ctx, buf, p->bytes == 1 ? "%02x" : "%04x", own_code(p, u)); + break; + case PEN_CID: + fz_append_printf(ctx, buf, "%04x", glyph(ctx, p, u)); + break; + case PEN_SIMPLE: + fz_append_printf(ctx, buf, "%02x", fz_windows_1252_from_unicode(u)); + break; + case PEN_CJK: + if (u > 0xffff) + fz_append_printf(ctx, buf, "%04x%04x", 0xd800 + ((u - 0x10000) >> 10), 0xdc00 + ((u - 0x10000) & 0x3ff)); + else + fz_append_printf(ctx, buf, "%04x", u); + break; + } +} + +void ely_drop_pen(fz_context *ctx, ely_pen *p) +{ + fz_drop_font(ctx, p->font); + pdf_drop_obj(ctx, p->ref); + pdf_drop_font(ctx, p->desc); + fz_free(ctx, p->map); +} diff --git a/crates/ely-pdf-engine/shim/ely_shim.h b/crates/ely-pdf-engine/shim/ely_shim.h index 6535c51..2e2ba80 100644 --- a/crates/ely-pdf-engine/shim/ely_shim.h +++ b/crates/ely-pdf-engine/shim/ely_shim.h @@ -53,4 +53,34 @@ int ely_watch_end(fz_context *ctx); const char *ely_open_warnings(void); void ely_drop_alt(fz_context *ctx, pdf_document *pdf, pdf_page *page); +/* A font to write with: new, or the page's own. */ +typedef enum +{ + PEN_CID, + PEN_SIMPLE, + PEN_CJK, + PEN_OWN +} ely_pen_kind; + +typedef struct +{ + ely_pen_kind kind; + fz_font *font; + int ordering; + pdf_obj *ref; + /* The page's own font: object number, description, code width. */ + int num; + pdf_font_desc *desc; + int bytes; + /* Unicode and code pairs of the own font. */ + int *map, mapped; +} ely_pen; + +void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p); +const char *ely_pen_resource(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_pen *p, char *name, size_t room); +int ely_pen_has(fz_context *ctx, ely_pen *p, int u); +float ely_pen_advance(fz_context *ctx, ely_pen *p, int u, float size); +void ely_pen_code(fz_context *ctx, fz_buffer *buf, ely_pen *p, int u); +void ely_drop_pen(fz_context *ctx, ely_pen *p); + #endif diff --git a/crates/ely-pdf-engine/shim/ely_write.c b/crates/ely-pdf-engine/shim/ely_write.c index 2d263fa..bb1f551 100644 --- a/crates/ely-pdf-engine/shim/ely_write.c +++ b/crates/ely-pdf-engine/shim/ely_write.c @@ -5,24 +5,6 @@ #include "ely_shim.h" -typedef enum -{ - PEN_CID, - PEN_SIMPLE, - PEN_CJK -} pen_kind; - -/* A font to write with and its resource. */ -typedef struct -{ - pen_kind kind; - fz_font *font; - int ordering; - pdf_obj *ref; - /* For the page's own font: Unicode and glyph pairs. */ - int *map, mapped; -} pen; - /* What to write, where, in what. */ typedef struct { @@ -34,188 +16,6 @@ typedef struct float box[3]; } text_args; -/* The script face for typed signatures, from the worker. */ -static const unsigned char *script_data; -static size_t script_len; - -void ely_script_font(const unsigned char *data, size_t len) -{ - script_data = data; - script_len = len; -} - -/* Unicode to glyph for the page font drawn as `own`. */ -static void own_glyphs(fz_context *ctx, pdf_page *page, fz_font *own, pen *p) -{ - pdf_obj *res = pdf_page_resources(ctx, page); - pdf_resource_stack stack = { NULL, res }; - pdf_obj *fonts = pdf_dict_get(ctx, res, PDF_NAME(Font)); - int i, n = pdf_dict_len(ctx, fonts); - for (i = 0; i < n && !p->map; i++) - { - pdf_font_desc *desc = pdf_load_font(ctx, page->doc, &stack, pdf_dict_get_val(ctx, fonts, i)); - fz_try(ctx) - { - int cid, end = desc->to_unicode ? 65536 : (int)desc->cid_to_ucs_len; - if (desc->font == own) - { - p->map = fz_malloc_array(ctx, 2 * end + 2, int); - for (cid = 0; cid < end; cid++) - { - int u = desc->to_unicode ? pdf_lookup_cmap(desc->to_unicode, cid) : desc->cid_to_ucs[cid]; - int gid = u > 0 ? pdf_font_cid_to_gid(ctx, desc, cid) : 0; - if (gid > 0) - p->map[2 * p->mapped] = u, p->map[2 * p->mapped + 1] = gid, p->mapped++; - } - } - } - fz_always(ctx) - pdf_drop_font(ctx, desc); - fz_catch(ctx) - fz_rethrow(ctx); - } - if (!p->map) - fz_throw(ctx, FZ_ERROR_ARGUMENT, "the block's font is not a font of its page"); -} - -/* The pen named `choice`; "original" writes in `own`. */ -static void choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, pen *p) -{ - static const struct { const char *choice, *name; } latin[] = { - { "helvetica", "Helvetica" }, { "times", "Times-Roman" }, { "courier", "Courier" } }; - static const struct { const char *choice; int ordering; } cjk[] = { - { "cjk-sc", FZ_ADOBE_GB }, { "cjk-tc", FZ_ADOBE_CNS }, { "cjk-jp", FZ_ADOBE_JAPAN }, { "cjk-kr", FZ_ADOBE_KOREA } }; - size_t i; - if (!strcmp(choice, "script")) - { - if (!script_data) - fz_throw(ctx, FZ_ERROR_ARGUMENT, "no script font was given"); - p->kind = PEN_CID; - p->font = fz_new_font_from_memory(ctx, "GreatVibes", script_data, (int)script_len, 0, 0); - return; - } - if (!strcmp(choice, "original")) - { - if (!own || !pdf_font_writing_supported(ctx, own)) - fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font cannot write new text"); - p->kind = PEN_CID; - p->font = fz_keep_font(ctx, own); - own_glyphs(ctx, page, own, p); - return; - } - for (i = 0; i < nelem(latin); i++) - if (!strcmp(choice, latin[i].choice)) - { - p->kind = PEN_SIMPLE; - p->font = fz_new_base14_font(ctx, latin[i].name); - return; - } - for (i = 0; i < nelem(cjk); i++) - if (!strcmp(choice, cjk[i].choice)) - { - p->kind = PEN_CJK; - p->ordering = cjk[i].ordering; - p->font = fz_new_cjk_font(ctx, cjk[i].ordering); - return; - } - fz_throw(ctx, FZ_ERROR_ARGUMENT, "no font %s", choice); -} - -/* A ToUnicode CMap from the pen's map. */ -static pdf_obj *map_to_unicode(fz_context *ctx, pdf_document *pdf, pen *p) -{ - fz_buffer *buf = fz_new_buffer(ctx, 256); - pdf_obj *ref = NULL; - fz_var(ref); - fz_try(ctx) - { - int i, j, block = 0; - fz_append_string(ctx, buf, "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n" - "/CIDSystemInfo <> def\n" - "/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n" - "1 begincodespacerange\n<0000> \nendcodespacerange\n"); - for (i = 0; i < p->mapped; i++) - { - int u = p->map[2 * i], gid = p->map[2 * i + 1], seen = 0; - for (j = 0; j < i && !seen; j++) - seen = p->map[2 * j + 1] == gid; - if (seen) - continue; - if (block % 100 == 0) - fz_append_printf(ctx, buf, "%sbeginbfchar\n", block ? "endbfchar\n" : ""); - if (u > 0xffff) - fz_append_printf(ctx, buf, "<%04x> <%04x%04x>\n", gid, 0xd800 + ((u - 0x10000) >> 10), 0xdc00 + ((u - 0x10000) & 0x3ff)); - else - fz_append_printf(ctx, buf, "<%04x> <%04x>\n", gid, u); - block++; - } - /* Each beginbfchar holds 100 entries at most. */ - fz_append_printf(ctx, buf, "%sendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n", - block ? "endbfchar\n" : ""); - ref = pdf_add_stream(ctx, pdf, buf, NULL, 0); - } - fz_always(ctx) - fz_drop_buffer(ctx, buf); - fz_catch(ctx) - fz_rethrow(ctx); - return ref; -} - -/* The pen's font as a resource of `pdf`. */ -static void add_pen(fz_context *ctx, pdf_document *pdf, pen *p) -{ - switch (p->kind) - { - case PEN_CID: - p->ref = pdf_add_cid_font(ctx, pdf, p->font); - /* Subset fonts carry bogus tables; the page's codes are true. */ - if (p->map) - pdf_dict_put_drop(ctx, p->ref, PDF_NAME(ToUnicode), map_to_unicode(ctx, pdf, p)); - break; - case PEN_SIMPLE: - p->ref = pdf_add_simple_font(ctx, pdf, p->font, PDF_SIMPLE_ENCODING_LATIN); - break; - case PEN_CJK: - p->ref = pdf_add_cjk_font(ctx, pdf, p->font, p->ordering, 0, 0); - break; - } -} - -/* The pen's glyph for `u`; 0 when it has none. */ -static int glyph(fz_context *ctx, pen *p, int u) -{ - int i; - if (p->kind == PEN_SIMPLE && fz_windows_1252_from_unicode(u) < 0) - return 0; - /* The page's own codes beat a subset font's tables. */ - for (i = 0; i < p->mapped; i++) - if (p->map[2 * i] == u) - return p->map[2 * i + 1]; - if (p->map) - return 0; - return fz_encode_character(ctx, p->font, u); -} - -/* `u` as the pen's code, in hex. */ -static void code(fz_context *ctx, fz_buffer *buf, pen *p, int u, int gid) -{ - switch (p->kind) - { - case PEN_CID: - fz_append_printf(ctx, buf, "%04x", gid); - break; - case PEN_SIMPLE: - fz_append_printf(ctx, buf, "%02x", fz_windows_1252_from_unicode(u)); - break; - case PEN_CJK: - if (u > 0xffff) - fz_append_printf(ctx, buf, "%04x%04x", 0xd800 + ((u - 0x10000) >> 10), 0xdc00 + ((u - 0x10000) & 0x3ff)); - else - fz_append_printf(ctx, buf, "%04x", u); - break; - } -} - /* `text` as code points; the count is returned. */ static int runes(fz_context *ctx, const char *text, int **out) { @@ -229,13 +29,8 @@ static int runes(fz_context *ctx, const char *text, int **out) return n; } -static float advance(fz_context *ctx, pen *p, int u, float size) -{ - return fz_advance_glyph(ctx, p->font, glyph(ctx, p, u), 0) * size; -} - /* Lines no wider than `width`: [from, to) each. */ -static int lay_out(fz_context *ctx, pen *p, const int *u, int n, float size, float width, int *from, int *to, +static int lay_out(fz_context *ctx, ely_pen *p, const int *u, int n, float size, float width, int *from, int *to, float *widest) { int i = 0, lines = 0; @@ -246,8 +41,9 @@ static int lay_out(fz_context *ctx, pen *p, const int *u, int n, float size, flo float run = 0, before = 0; for (j = i; j < n && u[j] != '\n'; j++) { - float w = advance(ctx, p, u[j], size); - if (run + w > width && j > i) + float w = ely_pen_advance(ctx, p, u[j], size); + /* A box measured from these widths fits them. */ + if (run + w > width + 0.01f && j > i) break; if (u[j] == ' ') space = j, before = run; @@ -344,7 +140,7 @@ pdf_obj *ely_append_content(fz_context *ctx, pdf_document *pdf, pdf_obj *page, f /* Text laid out: the pen, code points, lines, placement. */ typedef struct { - pen p; + ely_pen p; int *u, n; int *from, *to, lines; float widest; @@ -354,9 +150,7 @@ typedef struct static void drop_laid(fz_context *ctx, laid *l) { - fz_drop_font(ctx, l->p.font); - pdf_drop_obj(ctx, l->p.ref); - fz_free(ctx, l->p.map); + ely_drop_pen(ctx, &l->p); fz_free(ctx, l->u); fz_free(ctx, l->from); fz_free(ctx, l->to); @@ -366,10 +160,10 @@ static void drop_laid(fz_context *ctx, laid *l) static int lay_text(fz_context *ctx, text_args *a, fz_font *own, pdf_page *page, fz_point first, float gap, laid *l) { int i; - choose_pen(ctx, a->font, own, page, &l->p); + ely_choose_pen(ctx, a->font, own, page, &l->p); l->n = runes(ctx, a->text, &l->u); for (i = 0; i < l->n; i++) - if (l->u[i] != '\n' && !glyph(ctx, &l->p, l->u[i])) + if (l->u[i] != '\n' && !ely_pen_has(ctx, &l->p, l->u[i])) return l->u[i]; l->from = fz_malloc_array(ctx, l->n + 2, int); l->to = fz_malloc_array(ctx, l->n + 2, int); @@ -395,10 +189,7 @@ static void draw_laid(fz_context *ctx, pdf_document *pdf, pdf_page *page, laid * fz_matrix to_user = ely_to_user(ctx, page); char name[32]; int i, line; - pdf_drop_obj(ctx, l->p.ref); - l->p.ref = NULL; - add_pen(ctx, pdf, &l->p); - ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyF", l->p.ref, name, sizeof name); + ely_pen_resource(ctx, pdf, page, &l->p, name, sizeof name); buf = fz_new_buffer(ctx, 256); fz_append_string(ctx, buf, "q\n"); for (line = 0; line < l->lines; line++) @@ -408,7 +199,7 @@ static void draw_laid(fz_context *ctx, pdf_document *pdf, pdf_page *page, laid * ((a->rgb >> 16) & 255) / 255.0f, ((a->rgb >> 8) & 255) / 255.0f, (a->rgb & 255) / 255.0f, tm.a, tm.b, tm.c, tm.d, tm.e, tm.f); for (i = l->from[line]; i < l->to[line]; i++) - code(ctx, buf, &l->p, l->u[i], glyph(ctx, &l->p, l->u[i])); + ely_pen_code(ctx, buf, &l->p, l->u[i]); fz_append_string(ctx, buf, "> Tj ET\n"); } fz_append_string(ctx, buf, "Q\n"); @@ -476,11 +267,12 @@ int ely_put_text(fz_context *ctx, fz_document *doc, int number, int block, const pdf_document *pdf = NULL; pdf_page *page = NULL; ely_shape shape = { NULL }; - laid l = { { PEN_SIMPLE, NULL, 0, NULL } }; + laid l; int begun = 0; fz_var(pdf); fz_var(page); fz_var(begun); + memset(&l, 0, sizeof l); *pixels = NULL; *grew = 0; fz_try(ctx) diff --git a/crates/ely-pdf-engine/tests/content.rs b/crates/ely-pdf-engine/tests/content.rs index b842cad..d221b7a 100644 --- a/crates/ely-pdf-engine/tests/content.rs +++ b/crates/ely-pdf-engine/tests/content.rs @@ -291,3 +291,48 @@ fn a_subset_font_writes_with_its_own_glyphs_and_reads_back() { let now = page_text(&mut worker, doc); assert!(now.contains("TEXT BUNDLE"), "the new words read back: {now:?}"); } + +/// "AyB", where "x" and "y" share one glyph. +fn shared_glyph_pdf() -> Vec { + let font = std::fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("fonts/GreatVibes-Regular.ttf")).expect("the font"); + let stream = |dict: &str, data: &[u8]| [format!("<<{dict}/Length {}>>stream\n", data.len()).as_bytes(), data, b"\nendstream"].concat(); + let unicode = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /U def\n1 begincodespacerange <0000> endcodespacerange\n4 beginbfchar <0001> <0041> <0002> <0078> <0003> <0079> <0004> <0042> endbfchar\nendcmap CMapName currentdict /CMap defineresource pop end end"; + let gids: Vec = [0u16, 36, 16, 16, 37].iter().flat_map(|g| g.to_be_bytes()).collect(); + let objects: Vec> = vec![ + b"<>".to_vec(), + b"<>".to_vec(), + b"<>>>/Contents 5 0 R>>".to_vec(), + b"<>".to_vec(), + stream("", b"BT /F1 24 Tf 72 700 Td <000100030004> Tj ET"), + b"<>/FontDescriptor 8 0 R/CIDToGIDMap 9 0 R/DW 600>>".to_vec(), + stream("", unicode), + b"<>".to_vec(), + stream("", &gids), + stream(&format!("/Length1 {}", font.len()), &font), + ]; + let mut pdf = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (at, body) in objects.iter().enumerate() { + offsets.push(pdf.len()); + pdf.extend([format!("{} 0 obj\n", at + 1).as_bytes(), body, b"\nendobj\n"].concat()); + } + let xref = pdf.len(); + pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes()); + offsets.iter().for_each(|at| pdf.extend(format!("{at:010} 00000 n \n").as_bytes())); + pdf.extend(format!("trailer\n<>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes()); + pdf +} + +#[test] +fn a_letter_that_shares_its_glyph_keeps_its_own_code() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("shared.pdf"); + std::fs::write(&path, shared_glyph_pdf()).expect("writes"); + let mut worker = worker(); + let mut doc = worker.open(&path).expect("opens").doc; + assert_eq!(page_text(&mut worker, doc), "AyB", "the sample reads as written"); + let writing = rewrite(&mut worker, doc, 0, "ByA", "original"); + assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("answers"), Wrote::Done(_))); + doc = worker.now(doc).expect("the state"); + assert_eq!(page_text(&mut worker, doc), "ByA", "y stays y, not the glyph's other letter"); +} diff --git a/crates/ely-pdf-engine/tests/independent.rs b/crates/ely-pdf-engine/tests/independent.rs index 924e268..dfdb9f0 100644 --- a/crates/ely-pdf-engine/tests/independent.rs +++ b/crates/ely-pdf-engine/tests/independent.rs @@ -61,3 +61,28 @@ fn text_a_field_and_page_order_read_back_elsewhere() { assert!(pages[1].0.contains("Changed words") && !pages[1].0.contains("First page"), "{pages:?}"); assert_eq!(pages[1].1, vec!["Ada".to_string()]); } + +#[test] +fn text_in_a_subset_font_reads_back_elsewhere() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/office/t.pdf"); + let mut worker = worker(); + let mut doc = worker.open(&path).expect("opens").doc; + let blocks = worker.text_blocks(doc, 0).expect("blocks"); + let (block, read) = blocks.iter().enumerate().find(|(_, b)| b.text.contains("BUNDLE")).expect("the block"); + let writing = Writing { + block: Some(block), + expected: read.text.clone(), + text: "TEXT BUNDLE".into(), + font: "original".into(), + size: read.size, + rgb: read.rgb, + bx: [read.rect[0], read.rect[1], (read.rect[2] - read.rect[0]) * 2.0], + }; + assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_))); + doc = worker.now(doc).expect("the state"); + let out = dir.path().join("edited.pdf"); + worker.save(doc, &out).expect("saves"); + let pages = read_back(&out); + assert!(pages[0].0.contains("TEXT BUNDLE"), "{pages:?}"); +}