ElyPDF: a document font that cannot take new text asks for another font (P-06)
This commit is contained in:
11 files changed
+144
-23
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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}"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(_)));
|
||||
}
|
||||
@@ -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
@@ -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 识别文字的写入被拒后仍弹“成功”
|
||||
|
||||
Reference in new issue
Block a user