diff --git a/apps/ely-pdf/src/visible.rs b/apps/ely-pdf/src/visible.rs index 404cf2d..688dbd7 100644 --- a/apps/ely-pdf/src/visible.rs +++ b/apps/ely-pdf/src/visible.rs @@ -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); diff --git a/apps/ely-pdf/src/writes.rs b/apps/ely-pdf/src/writes.rs index 59bda49..c2b7ce5 100644 --- a/apps/ely-pdf/src/writes.rs +++ b/apps/ely-pdf/src/writes.rs @@ -96,6 +96,13 @@ impl Viewer { cx: &mut Context, ) { 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, diff --git a/crates/ely-pdf-engine/shim/ely_pen.c b/crates/ely-pdf-engine/shim/ely_pen.c index 6af5ccd..695fbe0 100644 --- a/crates/ely-pdf-engine/shim/ely_pen.c +++ b/crates/ely-pdf-engine/shim/ely_pen.c @@ -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); } diff --git a/crates/ely-pdf-engine/shim/ely_shim.h b/crates/ely-pdf-engine/shim/ely_shim.h index 2e2ba80..be74d80 100644 --- a/crates/ely-pdf-engine/shim/ely_shim.h +++ b/crates/ely-pdf-engine/shim/ely_shim.h @@ -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); diff --git a/crates/ely-pdf-engine/shim/ely_write.c b/crates/ely-pdf-engine/shim/ely_write.c index bb1f551..1766870 100644 --- a/crates/ely-pdf-engine/shim/ely_write.c +++ b/crates/ely-pdf-engine/shim/ely_write.c @@ -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; } diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/content.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/content.rs index 2175f2a..ca531d6 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/content.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/content.rs @@ -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, (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"))?)); diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs index 9ffbd95..f0f3432 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs @@ -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() } diff --git a/crates/ely-pdf-engine/src/content.rs b/crates/ely-pdf-engine/src/content.rs index 7d63c0f..98e4728 100644 --- a/crates/ely-pdf-engine/src/content.rs +++ b/crates/ely-pdf-engine/src/content.rs @@ -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, (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}"))), } } diff --git a/crates/ely-pdf-engine/tests/fonts.rs b/crates/ely-pdf-engine/tests/fonts.rs new file mode 100644 index 0000000..e55ce82 --- /dev/null +++ b/crates/ely-pdf-engine/tests/fonts.rs @@ -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 { + 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 { + 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 { + 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(_))); +} diff --git a/crates/ely-ui/src/i18n/pdf.rs b/crates/ely-ui/src/i18n/pdf.rs index 557b175..c17ab7b 100644 --- a/crates/ely-ui/src/i18n/pdf.rs +++ b/crates/ely-ui/src/i18n/pdf.rs @@ -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", "文档已改动,未执行。"), diff --git a/tasks/BUGS.md b/tasks/BUGS.md index 2e53471..b49a34a 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -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 识别文字的写入被拒后仍弹“成功”