diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 3d47c83caf..40153a4372 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -998,7 +998,7 @@ pub fn tools() -> Vec { .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 }, diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index a48e96473c..0ca8ad2ee0 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -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") { diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index ff559cf248..ee749980c9 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -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 { - 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, - /// `/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(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 0da89682e4..647ddbf180 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"