ElyPDF: write in a document's subset font through the page's own glyph codes (R2.17)
This commit is contained in:
3 files changed
+109
-4
No files matched your search
@@ -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 按子集的占位表生成的那份会把新字读成乱码)。页面上没有的字仍报缺字。
|
||||
@@ -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;
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
Reference in new issue
Block a user