mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M7: report Japanese Type 3 fallback font names (#387)
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
1 parent
dc356f0dfc
commit
438a269bac
6 files changed
+97
-11
No files matched your search
@@ -998,7 +998,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
.ro()
|
||||
.cmd("edit.edit_text")
|
||||
.with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])),
|
||||
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character; otherwise the line is set in Helvetica (the result shows the font used). Text that no available font can show is refused. Undoable.")
|
||||
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character; otherwise a standard font or the Japanese Type 3 fallback is used (the result shows the font used). The Japanese fallback does not yet preserve the source's family or weight. Text that no available font can show is refused. Undoable.")
|
||||
.cmd("edit.edit_text")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "line": { "type": "integer", "minimum": 1 },
|
||||
|
||||
@@ -304,6 +304,30 @@ fn japanese_line_uses_unicode_type3_fallback() {
|
||||
assert_eq!(text::text_lines(&reopened, 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_fallback_reports_the_face_after_save_and_reopen() {
|
||||
if without_craft_fonts("japanese_fallback_reports_the_face_after_save_and_reopen") {
|
||||
return;
|
||||
}
|
||||
let face = pdfcraft_fonts::document_japanese_font().unwrap();
|
||||
let expected = format!("{}-{}", face.family, face.style).replace(' ', "");
|
||||
for paragraph in [false, true] {
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
|
||||
let replacement = "見本商会 御中";
|
||||
if paragraph {
|
||||
text::replace_block(&mut doc, 0, 0, replacement).unwrap();
|
||||
} else {
|
||||
text::replace_line(&mut doc, 0, 0, replacement).unwrap();
|
||||
}
|
||||
for d in [&doc, &reopen(&doc)] {
|
||||
let lines = text::text_lines(d, 0).unwrap();
|
||||
assert_eq!(lines[0].text, replacement);
|
||||
assert_eq!(lines[0].base_font, expected, "report the real fallback face, not an empty BaseFont");
|
||||
assert_eq!(text::text_blocks(d, 0).unwrap()[0].base_font, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_paragraph_uses_unicode_type3_fallback() {
|
||||
if without_craft_fonts("japanese_paragraph_uses_unicode_type3_fallback") {
|
||||
|
||||
+11
-2
@@ -27,7 +27,7 @@ pub struct TextLine {
|
||||
pub text: String,
|
||||
/// Its box in user space.
|
||||
pub rect: [f64; 4],
|
||||
/// The font's resource name and `/BaseFont`, and its size in text space.
|
||||
/// The font's resource name and `/BaseFont` (or Type 3 descriptor's `/FontName`), and its size in text space.
|
||||
pub font: String,
|
||||
pub base_font: String,
|
||||
pub size: f64,
|
||||
@@ -569,7 +569,8 @@ fn no_japanese_font() -> EditError {
|
||||
}
|
||||
|
||||
fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Type3Fallback, EditError> {
|
||||
let family = pdfcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?.family;
|
||||
let face = pdfcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?;
|
||||
let family = face.family;
|
||||
let mut chars = Vec::new();
|
||||
for ch in text.chars() {
|
||||
if !chars.contains(&ch) {
|
||||
@@ -614,6 +615,14 @@ fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Ty
|
||||
font.set(b"Type".to_vec(), Object::name("Font"));
|
||||
font.set(b"Subtype".to_vec(), Object::name("Type3"));
|
||||
font.set(b"Name".to_vec(), Object::name("PCJapanese"));
|
||||
let mut descriptor = Dict::new();
|
||||
descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor"));
|
||||
descriptor.set(b"FontName".to_vec(), Object::name(&format!("{}-{}", face.family, face.style).replace(' ', "")));
|
||||
descriptor.set(b"FontFamily".to_vec(), Object::String(PdfString::literal(face.family.as_bytes().to_vec())));
|
||||
descriptor.set(b"Flags".to_vec(), Object::Int(if face.family.contains("Mincho") { 6 } else { 4 }));
|
||||
descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0));
|
||||
// PDF 1.7 tables 5.9 and 5.19: a Type 3 descriptor is indirect; Ascent/Descent may be omitted.
|
||||
font.set(b"FontDescriptor".to_vec(), Object::Ref(doc.add(Object::Dict(descriptor))));
|
||||
font.set(b"FontBBox".to_vec(), Object::Array(vec![Object::Int(0), Object::Int(-300), Object::Int(1000), Object::Int(1000)]));
|
||||
font.set(
|
||||
b"FontMatrix".to_vec(),
|
||||
|
||||
@@ -30,6 +30,13 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes:
|
||||
`japanese_glyph` returns `GlyphError::NoFont` and the editor reports a clear error.
|
||||
- `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`.
|
||||
|
||||
Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect
|
||||
`FontDescriptor`. `pdf::Metrics::base_font` uses its `FontName` when the Type 3 dictionary has
|
||||
no `BaseFont`, so text inspection and edit-tool results identify the fallback after save/reopen.
|
||||
The descriptor does not change glyph programs or replace the Type 3 `FontBBox` metrics when
|
||||
`Ascent`/`Descent` are omitted (PDF Reference 1.7, tables 5.9 and 5.19). Matching the source
|
||||
font's family and weight remains unsupported.
|
||||
|
||||
wasm32 builds embed only BIZ UDPGothic Regular and any `Arab` or `Telu` face, to keep the web build small: the web build
|
||||
currently has no Chinese face, so Chinese there still shows the replacement glyph.
|
||||
Font files are never
|
||||
|
||||
+52
-6
@@ -47,7 +47,7 @@ pub struct Metrics {
|
||||
pub composite: bool,
|
||||
/// Code → Unicode text.
|
||||
unicode: HashMap<u32, String>,
|
||||
/// `/BaseFont`, and whether it is a subset (`ABCDEF+Name`: other glyphs are missing).
|
||||
/// `/BaseFont` (or Type 3 descriptor's `/FontName`), and whether it is a subset (`ABCDEF+Name`: other glyphs are missing).
|
||||
pub base_font: String,
|
||||
pub subset: bool,
|
||||
/// Whether the PDF identifies this face as bold or italic. These come from the
|
||||
@@ -162,7 +162,16 @@ impl Metrics {
|
||||
|
||||
pub fn from_dict(doc: &Document, font: &Dict) -> Self {
|
||||
let mut m = Self::read_metrics(doc, font);
|
||||
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).into_owned();
|
||||
let base = font
|
||||
.name(b"BaseFont")
|
||||
.map(|name| String::from_utf8_lossy(name).into_owned())
|
||||
.or_else(|| {
|
||||
(font.name(b"Subtype") == Some(b"Type3"))
|
||||
.then(|| dict(doc, font.get(b"FontDescriptor")))
|
||||
.flatten()
|
||||
.and_then(|d| d.name(b"FontName").map(|name| String::from_utf8_lossy(name).into_owned()))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
m.subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase());
|
||||
let (bold, italic) = style_from_name(&base);
|
||||
m.bold |= bold;
|
||||
@@ -263,11 +272,18 @@ impl Metrics {
|
||||
let weight = d.get(b"FontWeight").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0);
|
||||
m.italic |= flags & 64 != 0 || angle.abs() > 0.1;
|
||||
m.bold |= flags & 262_144 != 0 || weight >= 600.0;
|
||||
let a = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0;
|
||||
let de = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0;
|
||||
// Type 3 descriptors may omit these metrics; the glyph-space FontBBox still applies.
|
||||
let ascent = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64());
|
||||
let descent = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64());
|
||||
// Fonts often claim 0; never shrink the glyph box below a sensible minimum.
|
||||
m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 };
|
||||
m.descent = if de < -0.05 { de.max(-0.6) } else { -0.25 };
|
||||
if ascent.is_some() || subtype != b"Type3" {
|
||||
let a = ascent.unwrap_or(0.0) / 1000.0;
|
||||
m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 };
|
||||
}
|
||||
if descent.is_some() || subtype != b"Type3" {
|
||||
let de = descent.unwrap_or(0.0) / 1000.0;
|
||||
m.descent = if de < -0.05 { de.max(-0.6) } else { -0.25 };
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
@@ -505,6 +521,36 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type3_descriptor_names_the_font_without_changing_its_metrics() {
|
||||
let mut doc = Document::new_empty();
|
||||
let mut f = font(
|
||||
&mut doc,
|
||||
vec![
|
||||
("Subtype", Object::name("Type3")),
|
||||
(
|
||||
"FontMatrix",
|
||||
Object::Array(vec![Object::Real(0.001), Object::Int(0), Object::Int(0), Object::Real(0.001), Object::Int(0), Object::Int(0)]),
|
||||
),
|
||||
("FontBBox", Object::Array(vec![Object::Int(0), Object::Int(-300), Object::Int(1000), Object::Int(1000)])),
|
||||
],
|
||||
);
|
||||
let before = Metrics::from_dict(&doc, &f);
|
||||
let mut descriptor = Dict::new();
|
||||
descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor"));
|
||||
descriptor.set(b"FontName".to_vec(), Object::name("ExampleMincho-Regular"));
|
||||
descriptor.set(b"Flags".to_vec(), Object::Int(6));
|
||||
descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0));
|
||||
let reference = doc.add(Object::Dict(descriptor));
|
||||
f.set(b"FontDescriptor".to_vec(), Object::Ref(reference));
|
||||
let after = Metrics::from_dict(&doc, &f);
|
||||
assert_eq!(after.base_font, "ExampleMincho-Regular");
|
||||
assert_eq!((after.ascent, after.descent, after.scale), (before.ascent, before.descent, before.scale));
|
||||
assert!(!after.bold && !after.italic && !after.subset);
|
||||
f.set(b"BaseFont".to_vec(), Object::name("ExplicitName"));
|
||||
assert_eq!(Metrics::from_dict(&doc, &f).base_font, "ExplicitName", "an explicit BaseFont takes precedence");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_fonts_decode_and_encode_through_their_encoding() {
|
||||
let mut doc = Document::new_empty();
|
||||
|
||||
@@ -1806,8 +1806,8 @@ milestone = "M7"
|
||||
status = "shipped"
|
||||
commands = ["edit.edit_text"]
|
||||
tools = ["text_edit"]
|
||||
evidence = ["crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
|
||||
notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese font-style fallback remains limited (#184)"
|
||||
evidence = ["crates/edit/src/tests.rs::japanese_fallback_reports_the_face_after_save_and_reopen", "crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
|
||||
notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese Type 3 fallback reports its face through FontDescriptor/FontName after editing and save/reopen; source family/weight matching remains limited (#184)"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.text-move-resize"
|
||||
|
||||
Reference in new issue
Block a user