ElyPDF: write in a document's subset font through the page's own glyph codes (R2.17)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 02:20:36 -04:00
1 parent 286631daf9
commit 371b1e81c8
3 files changed
+109 -4

No files matched your search

+1
View File
@@ -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 按子集的占位表生成的那份会把新字读成乱码)。页面上没有的字仍报缺字。
+92 -4
View File
@@ -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 <</Registry(Adobe)/Ordering(UCS)/Supplement 0>> def\n"
"/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n"
"1 begincodespacerange\n<0000> <FFFF>\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;
+16
View File
@@ -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:?}");
}