ElyPDF: a document font that cannot take new text asks for another font (P-06)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 22:18:33 -04:00
1 parent 301365a7e6
commit a49b0b5db3
11 files changed
+144 -23

No files matched your search

+1
View File
@@ -95,6 +95,7 @@ impl PdfView {
let write = move |worker: &mut ely_pdf_engine::Worker, doc| match worker.put_text(doc, page, &writing, 0.0)? {
Wrote::Done(mark) => Ok(mark),
Wrote::Missing(c) => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("the font has no “{c}”"))),
Wrote::Unwritable => Err(io::Error::other("the document's font cannot write")),
Wrote::Preview(..) => Err(io::Error::other("a preview where a write was asked")),
};
self.edit_at("placing a typed signature", write, cx);
+8
View File
@@ -96,6 +96,13 @@ impl Viewer {
cx: &mut Context<Self>,
) {
match answer {
Ok(Wrote::Unwritable) => {
let note = Self::text(cx, "pdf.text.unwritable", &[]);
self.edit_panel.update(cx, |panel, cx| {
panel.note = Some(note);
cx.notify();
});
}
Ok(Wrote::Missing(c)) => {
let note = Self::text(cx, "pdf.text.missing", &[("char", &c.to_string())]);
self.edit_panel.update(cx, |panel, cx| {
@@ -128,6 +135,7 @@ impl Viewer {
move |worker, doc| match worker.put_text(doc, page, &writing, 0.0)? {
Wrote::Done(mark) => Ok(mark),
Wrote::Missing(c) => Err(io::Error::other(format!("the font has no {c}"))),
Wrote::Unwritable => Err(io::Error::other("the document's font cannot write")),
Wrote::Preview(..) => Err(io::Error::other("a write came back as a preview")),
},
cx,
+15 -16
View File
@@ -29,13 +29,13 @@ static int unicode_of(pdf_font_desc *desc, int cid)
return n == 1 && ucs[0] > 0 && ucs[0] != FZ_REPLACEMENT_CHARACTER ? ucs[0] : -1;
}
/* Unicode and code pairs of `desc`; its codes are CIDs. */
static void own_codes(fz_context *ctx, pdf_font_desc *desc, ely_pen *p)
/* `desc` Unicode-to-code pairs; -1 when codes are not CIDs. */
static int own_codes(fz_context *ctx, pdf_font_desc *desc, ely_pen *p)
{
pdf_cmap *enc = desc->encoding;
int cid, end;
if (!enc || strcmp(enc->cmap_name, "Identity-H") || enc->codespace_len != 1 || enc->codespace[0].n > 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font uses codes Ely cannot write");
return -1;
p->bytes = enc->codespace[0].n;
end = 1 << (8 * p->bytes);
p->map = fz_malloc_array(ctx, 2 * end, int);
@@ -45,10 +45,11 @@ static void own_codes(fz_context *ctx, pdf_font_desc *desc, ely_pen *p)
if (u > 0 && pdf_font_cid_to_gid(ctx, desc, cid) > 0)
p->map[2 * p->mapped] = u, p->map[2 * p->mapped + 1] = cid, p->mapped++;
}
return 0;
}
/* The page font drawn as `own`, with its codes. */
static void own_font(fz_context *ctx, pdf_page *page, fz_font *own, ely_pen *p)
/* The page font drawn as `own`; -1: cannot write. */
static int own_font(fz_context *ctx, pdf_page *page, fz_font *own, ely_pen *p)
{
pdf_obj *res = pdf_page_resources(ctx, page);
pdf_resource_stack stack = { NULL, res };
@@ -63,16 +64,15 @@ static void own_font(fz_context *ctx, pdf_page *page, fz_font *own, ely_pen *p)
continue;
}
p->desc = desc;
/* A direct font cannot be named again after the edit. */
p->num = pdf_to_num(ctx, pdf_dict_get_val(ctx, fonts, i));
if (!p->num)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font is not an object of its own");
own_codes(ctx, desc, p);
return p->num ? own_codes(ctx, desc, p) : -1;
}
if (!p->desc)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the block's font is not a font of its page");
/* Drawn from elsewhere, as in a form: not writable here. */
return -1;
}
void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p)
int ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p)
{
static const struct { const char *choice, *name; } latin[] = {
{ "helvetica", "Helvetica" }, { "times", "Times-Roman" }, { "courier", "Courier" } };
@@ -85,22 +85,21 @@ void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no script font was given");
p->kind = PEN_CID;
p->font = fz_new_font_from_memory(ctx, "GreatVibes", script_data, (int)script_len, 0, 0);
return;
return 0;
}
if (!strcmp(choice, "original"))
{
if (!own)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the block has no font of its own");
p->kind = PEN_OWN;
own_font(ctx, page, own, p);
return;
return own_font(ctx, page, own, p);
}
for (i = 0; i < nelem(latin); i++)
if (!strcmp(choice, latin[i].choice))
{
p->kind = PEN_SIMPLE;
p->font = fz_new_base14_font(ctx, latin[i].name);
return;
return 0;
}
for (i = 0; i < nelem(cjk); i++)
if (!strcmp(choice, cjk[i].choice))
@@ -108,7 +107,7 @@ void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page
p->kind = PEN_CJK;
p->ordering = cjk[i].ordering;
p->font = fz_new_cjk_font(ctx, cjk[i].ordering);
return;
return 0;
}
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no font %s", choice);
}
+1 -1
View File
@@ -76,7 +76,7 @@ typedef struct
int *map, mapped;
} ely_pen;
void ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p);
int ely_choose_pen(fz_context *ctx, const char *choice, fz_font *own, pdf_page *page, ely_pen *p);
const char *ely_pen_resource(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_pen *p, char *name, size_t room);
int ely_pen_has(fz_context *ctx, ely_pen *p, int u);
float ely_pen_advance(fz_context *ctx, ely_pen *p, int u, float size);
+5 -4
View File
@@ -156,11 +156,12 @@ static void drop_laid(fz_context *ctx, laid *l)
fz_free(ctx, l->to);
}
/* Lays `a` out from `first`, or names a missing glyph. */
/* Lays `a` out; a missing glyph, or -1: unwritable font. */
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;
ely_choose_pen(ctx, a->font, own, page, &l->p);
if (ely_choose_pen(ctx, a->font, own, page, &l->p) < 0)
return -1;
l->n = runes(ctx, a->text, &l->u);
for (i = 0; i < l->n; i++)
if (l->u[i] != '\n' && !ely_pen_has(ctx, &l->p, l->u[i]))
@@ -259,7 +260,7 @@ static void preview(fz_context *ctx, pdf_document *pdf, int number, int block, c
fz_rethrow(ctx);
}
/* Edits, adds or previews text: 0, 2 missing, -1 failed. */
/* Writes or previews text: 0, 2 missing, 3 unwritable, -1. */
int ely_put_text(fz_context *ctx, fz_document *doc, int number, int block, const char *expected, const char *text,
const char *font, float size, int rgb, const float box[3], const char *password, float scale, int *missing,
int *grew, unsigned char **pixels, int *w, int *h)
@@ -325,5 +326,5 @@ int ely_put_text(fz_context *ctx, fz_document *doc, int number, int block, const
*pixels = NULL;
return ely_edit_failed(ctx, pdf, begun);
}
return *missing ? 2 : 0;
return *missing < 0 ? 3 : *missing ? 2 : 0;
}
@@ -59,6 +59,8 @@ pub enum Wrote {
Done,
/// A character the font cannot draw; nothing changed.
Missing(char),
/// The document's own font cannot write; nothing changed.
Unwritable,
/// The would-be result: BGRA, size, and whether it grew.
Preview(Vec<u8>, (u32, u32), bool),
}
@@ -106,6 +108,9 @@ impl Mupdf {
&mut height,
)
})?;
if status == 3 {
return Ok(Wrote::Unwritable);
}
if status == 2 {
let missing = u32::try_from(missing).ok().and_then(char::from_u32);
return Ok(Wrote::Missing(missing.ok_or_else(|| io::Error::other("a missing glyph out of range"))?));
@@ -147,6 +147,7 @@ impl Worker {
Ok(match wrote? {
content::Wrote::Done => Pack::new().u64(0).str(&doc.mark()).bytes(),
content::Wrote::Missing(c) => Pack::new().u64(2).u64(u64::from(c)).bytes(),
content::Wrote::Unwritable => Pack::new().u64(3).bytes(),
content::Wrote::Preview(pixels, (w, h), grew) => {
Pack::new().u64(1).u64(u64::from(w)).u64(u64::from(h)).u64(u64::from(grew)).blob(&pixels).bytes()
}
+5 -1
View File
@@ -49,6 +49,8 @@ pub enum Wrote {
Done(String),
/// The font lacks this character; nothing changed.
Missing(char),
/// The document's own font cannot write; nothing changed.
Unwritable,
/// BGRA of the area, its size, whether text grew.
Preview(Vec<u8>, (u32, u32), bool),
}
@@ -113,11 +115,13 @@ impl Worker {
let code = u32::try_from(reply.u64()?).map_err(io::Error::other)?;
Ok(Wrote::Missing(char::from_u32(code).ok_or_else(|| io::Error::other("a bad missing glyph"))?))
}
_ => {
1 => {
let size = (reply.u64()? as u32, reply.u64()? as u32);
let grew = reply.u64()? != 0;
Ok(Wrote::Preview(reply.blob()?, size, grew))
}
3 => Ok(Wrote::Unwritable),
other => Err(io::Error::other(format!("a text reply of kind {other}"))),
}
}
+100
View File
@@ -0,0 +1,100 @@
//! Fonts the editor meets in the wild (PDF-03).
use std::path::Path;
use ely_pdf_engine::{Doc, Worker, Writing, Wrote};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn page_text(worker: &mut Worker, doc: Doc) -> String {
worker.text(doc, 0).expect("reads").iter().map(|c| c.c).collect()
}
/// A rewrite of block `block` as `text` in `font`.
fn rewrite(worker: &mut Worker, doc: Doc, block: usize, text: &str, font: &str) -> Writing {
let read = worker.text_blocks(doc, 0).expect("blocks")[block].clone();
Writing {
block: Some(block),
expected: read.text,
text: text.into(),
font: font.into(),
size: read.size,
rgb: read.rgb,
bx: [read.rect[0], read.rect[1], read.rect[2] - read.rect[0]],
}
}
/// A page whose text uses a CMap-encoded CID font.
fn cmap_font_pdf() -> Vec<u8> {
let content = "BT /F1 24 Tf 72 700 Td <00480069> Tj ET";
one_font_page(content, &[
"<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>".to_string(),
"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor 7 0 R >>".to_string(),
"<< /Type /FontDescriptor /FontName /STSong-Light /Flags 4 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 880 /Descent -120 /CapHeight 700 /StemV 80 >>".to_string(),
])
}
/// A page drawing `content`; font objects start at 5.
fn one_font_page(content: &str, font: &[String]) -> Vec<u8> {
page_with("<< /F1 5 0 R >>", content, font)
}
/// A page with font resources `fonts`; objects from 5.
fn page_with(fonts: &str, content: &str, font: &[String]) -> Vec<u8> {
let mut objects = vec![
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font {fonts} >> /Contents 4 0 R >>"),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
];
objects.extend_from_slice(font);
let mut pdf = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, body) in objects.iter().enumerate() {
offsets.push(pdf.len());
pdf.extend(format!("{} 0 obj\n{body}\nendobj\n", i + 1).bytes());
}
let xref = pdf.len();
pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).bytes());
for offset in offsets {
pdf.extend(format!("{offset:010} 00000 n \n").bytes());
}
pdf.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).bytes());
pdf
}
#[test]
fn a_font_whose_codes_ely_cannot_write_says_so() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("cmap.pdf");
std::fs::write(&path, cmap_font_pdf()).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
assert_eq!(page_text(&mut worker, doc), "Hi");
let writing = rewrite(&mut worker, doc, 0, "Yo", "original");
assert_eq!(worker.put_text(doc, 0, &writing, 2.0).expect("answers"), Wrote::Unwritable);
assert_eq!(page_text(&mut worker, doc), "Hi", "nothing changed");
let helvetica = rewrite(&mut worker, doc, 0, "Yo", "helvetica");
assert!(matches!(worker.put_text(doc, 0, &helvetica, 0.0).expect("writes"), Wrote::Done(_)));
}
#[test]
fn a_font_written_into_the_page_says_it_cannot_write() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("direct.pdf");
// The font dictionary sits in the page itself.
let fonts = "<< /F1 << /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /Identity-H /DescendantFonts [5 0 R] >> >>";
let pdf = page_with(fonts, "BT /F1 24 Tf 72 700 Td <00480069> Tj ET", &[
"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor 6 0 R >>".to_string(),
"<< /Type /FontDescriptor /FontName /STSong-Light /Flags 4 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 880 /Descent -120 /CapHeight 700 /StemV 80 >>".to_string(),
]);
std::fs::write(&path, pdf).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
let writing = rewrite(&mut worker, doc, 0, "Yo", "original");
assert_eq!(worker.put_text(doc, 0, &writing, 2.0).expect("answers"), Wrote::Unwritable);
let helvetica = rewrite(&mut worker, doc, 0, "Yo", "helvetica");
assert!(matches!(worker.put_text(doc, 0, &helvetica, 0.0).expect("writes"), Wrote::Done(_)));
}
+2
View File
@@ -67,6 +67,7 @@ pub(super) const EN: &[(&str, &str)] = &[
"pdf.text.missing",
"This font cannot draw “{char}”. Choose another font.",
),
("pdf.text.unwritable", "The document's own font cannot write new text. Choose another font."),
("pdf.object.replace", "Replace Image…"),
("pdf.object.delete", "Delete"),
(
@@ -288,6 +289,7 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("pdf.text.apply", "应用"),
("pdf.text.erase", "删除文字"),
("pdf.text.missing", "这种字体画不出“{char}”。请换一种字体。"),
("pdf.text.unwritable", "文档自带的字体写不了新字。请换一种字体。"),
("pdf.object.replace", "替换图片…"),
("pdf.object.delete", "删除"),
("pdf.object.changed", "文档已改动,未执行。"),
+1 -1
View File
@@ -38,7 +38,7 @@
- [x] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图 — 包住文字块的对象让位于文字,移动须过 3 个窗口像素;R1 漏掉同点的小图;R2 PASS
- [x] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应 — 整页一遍干跑判定全部文字块(1000 块 1.5 s→0.1 s);R1 转向文字后错位、共用原点只计一块;R2 PASS
- [x] P-05 连续快画或快点被拒:“文档在此间已改动,未执行” — 按坐标的编辑排队、跑在出队时的状态上,只在页面变动时拒绝;R2 PASS
- [ ] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话
- [x] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话 — 原字体写不了新字(非 CID 编码、页面内直接字体、不在本页)时面板提示换字体;R2 PASS
- [ ] P-07 含连字的文字块永远不可编辑
- [ ] P-08 页面读取失败后每帧重试、每帧弹提示
- [ ] P-09 识别文字的写入被拒后仍弹“成功”