diff --git a/agents.md b/agents.md index b3676b9..735b324 100644 --- a/agents.md +++ b/agents.md @@ -262,3 +262,4 @@ 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 按子集的占位表生成的那份会把新字读成乱码)。页面上没有的字仍报缺字。 diff --git a/crates/ely-pdf-engine/shim/ely_write.c b/crates/ely-pdf-engine/shim/ely_write.c index bd94547..2d263fa 100644 --- a/crates/ely-pdf-engine/shim/ely_write.c +++ b/crates/ely-pdf-engine/shim/ely_write.c @@ -19,6 +19,8 @@ typedef struct 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. */ @@ -42,8 +44,42 @@ void ely_script_font(const unsigned char *data, size_t len) 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, pen *p) +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" } }; @@ -64,6 +100,7 @@ static void choose_pen(fz_context *ctx, const char *choice, fz_font *own, pen *p 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++) @@ -84,6 +121,46 @@ static void choose_pen(fz_context *ctx, const char *choice, fz_font *own, pen *p 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) { @@ -91,6 +168,9 @@ static void add_pen(fz_context *ctx, pdf_document *pdf, pen *p) { 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); @@ -104,8 +184,15 @@ static void add_pen(fz_context *ctx, pdf_document *pdf, pen *p) /* 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); } @@ -269,16 +356,17 @@ 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); fz_free(ctx, l->u); fz_free(ctx, l->from); fz_free(ctx, l->to); } /* Lays `a` out from `first`, or names a missing glyph. */ -static int lay_text(fz_context *ctx, text_args *a, fz_font *own, fz_point first, float gap, 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, &l->p); + 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])) @@ -409,7 +497,7 @@ int ely_put_text(fz_context *ctx, fz_document *doc, int number, int block, const first = shape.first; gap = shape.gap; } - *missing = lay_text(ctx, &a, shape.font, first, gap, &l); + *missing = lay_text(ctx, &a, shape.font, page, first, gap, &l); if (!*missing) { fz_rect drawn = laid_area(&l, size), region = drawn; diff --git a/crates/ely-pdf-engine/tests/content.rs b/crates/ely-pdf-engine/tests/content.rs index 94dd113..9db87b1 100644 --- a/crates/ely-pdf-engine/tests/content.rs +++ b/crates/ely-pdf-engine/tests/content.rs @@ -275,3 +275,19 @@ fn runs_on_one_baseline_still_wrap_onto_new_lines() { tops.dedup(); assert!(tops.len() >= 3, "each wrapped line has its own baseline: {tops:?}"); } + +#[test] +fn a_subset_font_writes_with_its_own_glyphs_and_reads_back() { + // LibreOffice's export: a subset font whose own tables are placeholders. + 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 at = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.contains("BUNDLE")).expect("the block"); + let mut writing = rewrite(&mut worker, doc, at, "TEXT BUNDLE", "original"); + // Room for one line, so the space is not a line break. + writing.bx[2] *= 2.0; + assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("answers"), Wrote::Done(_)), "the page's glyphs suffice"); + doc = worker.now(doc).expect("the state"); + let now = page_text(&mut worker, doc); + assert!(now.contains("TEXT BUNDLE"), "the new words read back: {now:?}"); +}