ElyPDF worker: text blocks are always valid JSON, whatever the text or font name holds (P-14)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-10 00:29:44 -04:00
1 parent dff9ed3bf4
commit ca4a32f2fa
4 files changed
+81 -12

No files matched your search

+23 -7
View File
@@ -9,18 +9,34 @@ static const char *stamps[] = { "Approved", "AsIs", "Confidential", "Departmenta
"Expired", "Final", "ForComment", "ForPublicRelease", "NotApproved", "NotForPublicRelease", "Sold",
"TopSecret", NULL };
/* Valid UTF-8 always: a stray byte becomes U+FFFD. */
void ely_json_string(fz_context *ctx, fz_output *out, const char *s)
{
char utf8[FZ_UTFMAX];
int rune;
fz_write_byte(ctx, out, '"');
for (; *s; s++)
while (*s)
{
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\')
fz_write_printf(ctx, out, "\\%c", c);
else if (c < 32)
fz_write_printf(ctx, out, "\\u%04x", c);
s += fz_chartorune(&rune, s);
/* A surrogate pair joins; a lone one is no character. */
if (rune >= 0xD800 && rune < 0xDC00)
{
int low;
int n = fz_chartorune(&low, s);
if (low >= 0xDC00 && low < 0xE000)
{
rune = 0x10000 + ((rune - 0xD800) << 10) + (low - 0xDC00);
s += n;
}
}
if ((rune >= 0xD800 && rune < 0xE000) || rune > 0x10FFFF)
rune = FZ_REPLACEMENT_CHARACTER;
if (rune == '"' || rune == '\\')
fz_write_printf(ctx, out, "\\%c", rune);
else if (rune < 32)
fz_write_printf(ctx, out, "\\u%04x", rune);
else
fz_write_byte(ctx, out, c);
fz_write_data(ctx, out, utf8, fz_runetochar(utf8, rune));
}
fz_write_byte(ctx, out, '"');
}
+7 -4
View File
@@ -313,10 +313,13 @@ int ely_text_blocks(fz_context *ctx, fz_document *doc, int number, unsigned char
words = fz_new_buffer(ctx, 256);
block_text(ctx, words, block);
fz_terminate_buffer(ctx, words);
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":%q,\"font\":%q,\"size\":%g,\"color\":%d,\"editable\":%s}",
first ? "" : ",", block->bbox.x0, block->bbox.y0, block->bbox.x1, block->bbox.y1,
fz_string_from_buffer(ctx, words), fz_font_name(ctx, ch->font), ch->size, (int)(ch->argb & 0xffffff),
editable ? "true" : "false");
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":",
first ? "" : ",", block->bbox.x0, block->bbox.y0, block->bbox.x1, block->bbox.y1);
ely_json_string(ctx, out, fz_string_from_buffer(ctx, words));
fz_write_string(ctx, out, ",\"font\":");
ely_json_string(ctx, out, fz_font_name(ctx, ch->font));
fz_write_printf(ctx, out, ",\"size\":%g,\"color\":%d,\"editable\":%s}",
ch->size, (int)(ch->argb & 0xffffff), editable ? "true" : "false");
first = 0;
}
fz_write_string(ctx, out, "]");
+50
View File
@@ -110,6 +110,56 @@ fn a_ligature_block_edits_like_any_other() {
}
}
#[test]
fn a_control_character_in_text_still_reads() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("control.pdf");
// Code 1 names U+0001, as broken encodings do.
let content = "BT /F1 24 Tf 72 700 Td <41014243> Tj ET";
let pdf = one_font_page(content, &[
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [1 /uni0001] >> >>".to_string(),
]);
std::fs::write(&path, pdf).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
let text = page_text(&mut worker, doc);
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].text, text, "the same text as the page reads");
}
#[test]
fn a_font_name_with_a_stray_byte_still_reads() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("name.pdf");
// #E9 alone is not UTF-8.
let content = "BT /F1 24 Tf 72 700 Td (ABC) Tj ET";
let pdf = one_font_page(content, &[
"<< /Type /Font /Subtype /Type1 /BaseFont /Review#E9Font /Encoding /WinAnsiEncoding >>".to_string(),
]);
std::fs::write(&path, pdf).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
assert_eq!((blocks[0].text.as_str(), blocks[0].font.as_str()), ("ABC", "Review\u{FFFD}Font"));
}
#[test]
fn a_font_name_in_surrogate_halves_still_reads() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "BT /F1 24 Tf 72 700 Td (ABC) Tj ET";
let mut worker = worker();
// A pair joins; a lone half is no character.
for (base, want) in [("Review#ED#A0#BD#ED#B8#80Font", "Review\u{1F600}Font"), ("Lone#ED#A0#BDFont", "Lone\u{FFFD}Font")] {
let path = dir.path().join(format!("{want}.pdf"));
let font = format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} /Encoding /WinAnsiEncoding >>");
std::fs::write(&path, one_font_page(content, &[font])).expect("writes");
let doc = worker.open(&path).expect("opens").doc;
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
assert_eq!(blocks[0].font, want);
}
}
#[test]
fn a_font_written_into_the_page_says_it_cannot_write() {
let dir = tempfile::tempdir().expect("a temp dir");
+1 -1
View File
@@ -46,7 +46,7 @@
- [x] P-11 撤销或重做在 MuPDF 侧失败后 worker 的步数不再同步 — 撤销失败后重读历史标记
- [x] P-12 `unhook` 中途抛出时泄漏保留的对象 — 保留的链接随保留随计数
- [x] P-13 优化导出的“之前”大小取磁盘文件而非当前文档 — 有未保存编辑时不与磁盘文件比大小
- [ ] P-14 文字块含控制字符时 JSON 回复不可解析
- [x] P-14 文字块含控制字符时 JSON 回复不可解析 — 共用 JSON 写入:控制字符转义,非法字节与孤立代理为 U+FFFD
- [x] D-01 导出或打印对话框开着时关闭其标签即崩溃 — Docs 实现 tab_closed,关标签即撤下其导出对话框;R1 PASS
- [x] D-02 邮件合并以当前标签为模板,预览后生成的每份都是同一条记录 — 选数据时记下模板标签,预览副本不作模板;R1 测试绕过选 CSV 入口;R2 PASS
- [ ] D-03 界面线程同步持 worker 锁读内核(样式、字体、书签、公式、分节名)