ElyPDF: text blocks with ligatures can be edited (P-07)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 22:51:33 -04:00
1 parent 15a54bcf2e
commit a28e618c07
3 files changed
+76 -4

No files matched your search

+45 -3
View File
@@ -4,6 +4,7 @@
#include <string.h>
#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;
}
+30
View File
@@ -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> <FF> 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");
+1 -1
View File
@@ -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 预览回复被丢弃时“应用”静默无事