diff --git a/crates/ely-pdf-engine/shim/ely_text.c b/crates/ely-pdf-engine/shim/ely_text.c index e1cd458..9e75044 100644 --- a/crates/ely-pdf-engine/shim/ely_text.c +++ b/crates/ely-pdf-engine/shim/ely_text.c @@ -4,6 +4,7 @@ #include #include "ely_shim.h" +#include "mupdf/ucdn.h" /* The page's text, read the same way every time. */ static fz_stext_page *read_text(fz_context *ctx, fz_document *doc, int number) @@ -58,14 +59,55 @@ static void block_targets(fz_context *ctx, fz_stext_block *block, fz_matrix to_u } } +/* `c` as stext spells it (stext-device.c); ligatures split. */ +static int spell(int c, int *out) +{ + static const char *named[] = { "ff", "fi", "fl", "ffi", "ffl", "st", "st" }; + int i, n = 0; + uint32_t lig[18]; + if (c >= 0xFB00 && c <= 0xFB06) + { + for (i = 0; named[c - 0xFB00][i]; i++) + out[n++] = named[c - 0xFB00][i]; + return n; + } + if ((c >= 0xfb00 && c <= 0xfdff) || (c >= 0xfe70 && c <= 0xfefc)) + n = ucdn_compat_decompose((uint32_t)c, lig); + for (i = 0; i < n; i++) + out[i] = (int)lig[i]; + if (n > 0) + return n; + out[0] = c; + return 1; +} + +/* Targets from `i` one glyph spells: a ligature spells several. */ +static int spelled(targets *t, int i, const int *ucs, int ucslen) +{ + int j = 1; + if (ucslen < 2 || ucs[0] != t->c[i]) + return 1; + while (j < ucslen && i + j < t->n && t->c[i + j] == ucs[j] && (!t->owner || t->owner[i + j] == t->owner[i])) + j++; + return j; +} + /* Removes a glyph drawn at one of the targets. */ static int take_glyph(fz_context *ctx, void *opaque, int *ucs, int ucslen, fz_matrix trm, fz_matrix ctm, fz_rect bbox, int tr, float ca, float CA) { targets *t = opaque; fz_point at = fz_transform_point(fz_transform_point(fz_make_point(0, 0), trm), ctm); - int i, k, found = 0; + /* MuPDF gives at most PDF_MRANGE_CAP; each spells to 18. */ + int i, k, found = 0, spelt[18 * PDF_MRANGE_CAP], n = 0; t->glyph++; + if (ucslen > PDF_MRANGE_CAP) + fz_throw(ctx, FZ_ERROR_ARGUMENT, "a glyph with %d characters", ucslen); + /* Match what stext reads: ligature code points spelled out. */ + for (k = 0; k < ucslen; k++) + n += spell(ucs[k], spelt + n); + ucs = spelt; + ucslen = n; for (i = 0; i < t->n; i++) { int match = 0; @@ -77,14 +119,14 @@ static int take_glyph(fz_context *ctx, void *opaque, int *ucs, int ucslen, fz_ma continue; if (!t->owner) { - t->hit++; + t->hit += spelled(t, i, ucs, ucslen); return 1; } /* Every block this glyph matches counts it once. */ if (t->seen[t->owner[i]] != t->glyph) { t->seen[t->owner[i]] = t->glyph; - t->hits[t->owner[i]]++; + t->hits[t->owner[i]] += spelled(t, i, ucs, ucslen); } found = 1; } diff --git a/crates/ely-pdf-engine/tests/fonts.rs b/crates/ely-pdf-engine/tests/fonts.rs index e55ce82..9fdecb7 100644 --- a/crates/ely-pdf-engine/tests/fonts.rs +++ b/crates/ely-pdf-engine/tests/fonts.rs @@ -80,6 +80,36 @@ fn a_font_whose_codes_ely_cannot_write_says_so() { assert!(matches!(worker.put_text(doc, 0, &helvetica, 0.0).expect("writes"), Wrote::Done(_))); } +#[test] +fn a_ligature_block_edits_like_any_other() { + // Code 1 is one ligature glyph, in either Unicode form. + let mut forms = vec![("00660069".to_string(), "find".to_string()), ("FB01".into(), "find".into()), ("FB03".into(), "ffind".into())]; + // Long mappings, to MuPDF's cap of 256: none cut short. + forms.push(("0066".repeat(48), format!("{}nd", "f".repeat(48)))); + forms.push(("FB03".repeat(256), format!("{}nd", "ffi".repeat(256)))); + for (unicode, text) in forms { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("lig.pdf"); + let cmap = format!("/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Lig def 1 begincodespacerange <00> endcodespacerange 3 beginbfchar <01> <{unicode}> <6E> <006E> <64> <0064> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end"); + let content = "BT /F1 24 Tf 72 700 Td <016E64> Tj ET"; + let pdf = one_font_page(content, &[ + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [1 /fi] >> /ToUnicode 6 0 R >>".to_string(), + format!("<< /Length {} >>\nstream\n{cmap}\nendstream", cmap.len()), + ]); + std::fs::write(&path, pdf).expect("writes"); + let mut worker = worker(); + let doc = worker.open(&path).expect("opens").doc; + let block = worker.text_blocks(doc, 0).expect("blocks").into_iter().next().expect("a block"); + assert_eq!(block.text, text); + assert!(block.editable, "{unicode}: {block:?}"); + let writing = rewrite(&mut worker, doc, 0, "Yo", "helvetica"); + let Wrote::Done(mark) = worker.put_text(doc, 0, &writing, 0.0).expect("writes") else { + panic!("{unicode}: the ligature block was not rewritten"); + }; + assert_eq!(page_text(&mut worker, doc.at(&mark).expect("a mark")), "Yo"); + } +} + #[test] fn a_font_written_into_the_page_says_it_cannot_write() { let dir = tempfile::tempdir().expect("a temp dir"); diff --git a/tasks/BUGS.md b/tasks/BUGS.md index b49a34a..3f36727 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -39,7 +39,7 @@ - [x] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应 — 整页一遍干跑判定全部文字块(1000 块 1.5 s→0.1 s);R1 转向文字后错位、共用原点只计一块;R2 PASS - [x] P-05 连续快画或快点被拒:“文档在此间已改动,未执行” — 按坐标的编辑排队、跑在出队时的状态上,只在页面变动时拒绝;R2 PASS - [x] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话 — 原字体写不了新字(非 CID 编码、页面内直接字体、不在本页)时面板提示换字体;R2 PASS -- [ ] P-07 含连字的文字块永远不可编辑 +- [x] P-07 含连字的文字块永远不可编辑 — 字形按 stext 的写法展开(多字字形、连字码点),整页、单块与删除同一判法;R3 PASS - [ ] P-08 页面读取失败后每帧重试、每帧弹提示 - [ ] P-09 识别文字的写入被拒后仍弹“成功” - [ ] P-10 预览回复被丢弃时“应用”静默无事