mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 04:58:34 -06:00
Merge PR #2609: fix(edit): draw the Cyrillic the Japanese faces lack with the bundled Inter face (#2593)
parity/acrobat-features.toml: edit.edit-text evidence and notes take both sides' additions
This commit is contained in:
@@ -1172,6 +1172,44 @@ fn cyrillic_in_a_serif_line_falls_back_to_a_face_that_has_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2593: the craft-fonts faces are Japanese, and part of the Cyrillic (Uzbek қ ғ ҳ, Kazakh ә ө ү
|
||||
/// ұ һ) is in none of them. The bundled Inter face has it, so the replacement is written instead of
|
||||
/// being refused, and a build without craft-fonts can do it too.
|
||||
#[test]
|
||||
fn uzbek_and_kazakh_cyrillic_use_the_bundled_latin_face() {
|
||||
for replacement in ["The quick brown fox jumps over the lazy dog қизил", "әже", "ўзбекча ғўза"] {
|
||||
for paragraph in [false, true] {
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
|
||||
let edit =
|
||||
if paragraph { text::replace_block(&mut doc, 0, 0, replacement) } else { text::replace_line(&mut doc, 0, 0, replacement) }.unwrap();
|
||||
assert_eq!(edit.substituted.as_deref(), Some("Inter Type3"), "{replacement}: {edit:?}");
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned();
|
||||
assert!(content.contains("/PCLat"), "{replacement}: {content}");
|
||||
let lines = text::text_lines(&reopen(&doc), 0).unwrap();
|
||||
assert_eq!(lines[0].text, replacement);
|
||||
assert_eq!(lines[0].base_font, "Inter-Regular", "the generated font reports the face it drew");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Japanese faces keep first refusal for the Cyrillic they do carry, so Russian text keeps the
|
||||
/// face it was drawn with before the bundled Latin face was added.
|
||||
#[test]
|
||||
fn cyrillic_the_japanese_faces_carry_keeps_them() {
|
||||
if without_craft_fonts("cyrillic_the_japanese_faces_carry_keeps_them") {
|
||||
return;
|
||||
}
|
||||
let face = pdfcraft_fonts::document_japanese_font_for_style(false, false).unwrap();
|
||||
if pdfcraft_fonts::japanese_glyph_from(face, 'ф').is_err() {
|
||||
eprintln!("skipping: the Japanese fallback face has no Cyrillic");
|
||||
return;
|
||||
}
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
|
||||
let edit = text::replace_line(&mut doc, 0, 0, "Привет, world").unwrap();
|
||||
let expected = format!("{} Type3", face.family);
|
||||
assert_eq!(edit.substituted.as_deref(), Some(expected.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_paragraph_uses_unicode_type3_fallback() {
|
||||
if without_craft_fonts("japanese_paragraph_uses_unicode_type3_fallback") {
|
||||
@@ -4717,16 +4755,18 @@ fn a_new_character_comes_from_the_source_fonts_own_embedded_program() {
|
||||
|
||||
#[test]
|
||||
fn a_subset_program_is_not_trusted_for_new_characters_and_the_error_names_the_character() {
|
||||
let mut doc = embedded_program_page("BT /F1 12 Tf 72 700 Td (Stable text) Tj ET", "ABCDEF+Inter-Regular");
|
||||
// The subset's own program is not trusted for a character: the bundled Inter face draws it
|
||||
// through a generated font instead (the subset may map a code whose outline was dropped).
|
||||
let edit = text::rewrite_block(&mut doc, 0, 0, Some("Stable tőxt"), &text::BlockStyle::default()).unwrap();
|
||||
assert_eq!(edit.substituted.as_deref(), Some("Inter Type3"));
|
||||
assert_eq!(text::text_blocks(&reopen(&doc), 0).unwrap()[0].editor_text, "Stable tőxt");
|
||||
// A character no fallback face has is refused, named, and left as it was.
|
||||
let mut doc = embedded_program_page("BT /F1 12 Tf 72 700 Td (Stable text) Tj ET", "ABCDEF+Inter-Regular");
|
||||
let before = page_content_bytes(&doc, 0);
|
||||
let result = text::rewrite_block(&mut doc, 0, 0, Some("Stable tőxt"), &text::BlockStyle::default());
|
||||
if pdfcraft_fonts::document_japanese_font().is_some() {
|
||||
eprintln!("built with craft-fonts: the fallback font draws the new character");
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
return;
|
||||
}
|
||||
let result = text::rewrite_block(&mut doc, 0, 0, Some("Stable กtext"), &text::BlockStyle::default());
|
||||
let Err(EditError::Invalid(msg)) = result else { panic!("expected a clear error, got {result:?}") };
|
||||
assert!(msg.contains("ő (U+0151)") && msg.contains("Load font") && !msg.contains("Japanese"), "{msg}");
|
||||
assert!(msg.contains("ก (U+0E01)") && msg.contains("Load font") && !msg.contains("Japanese"), "{msg}");
|
||||
assert_eq!(page_content_bytes(&doc, 0), before);
|
||||
}
|
||||
|
||||
|
||||
+74
-16
@@ -1448,12 +1448,28 @@ fn needs_type3(text: &str) -> bool {
|
||||
const JAPANESE_FONT_NAME: &str = "PCJapanese";
|
||||
/// The `/Name` of the Type 3 fonts generated for Hebrew text.
|
||||
const HEBREW_FONT_NAME: &str = "PCHebrew";
|
||||
/// The `/Name` of the Type 3 fonts generated from the bundled Latin (Inter) face.
|
||||
const LATIN_FONT_NAME: &str = "PCLatin";
|
||||
|
||||
/// Whether `font` is one of those generated fonts (Japanese or Hebrew): a replacement longer than
|
||||
/// one of them is split across several, which read back as one line (see [`same_font`]).
|
||||
/// Whether `font` is one of those generated fonts (Latin, Japanese or Hebrew): a replacement longer
|
||||
/// than one of them is split across several, which read back as one line (see [`same_font`]).
|
||||
fn is_type3_fallback(font: &Dict) -> bool {
|
||||
font.name(b"Subtype") == Some(b"Type3")
|
||||
&& font.name(b"Name").is_some_and(|n| n == JAPANESE_FONT_NAME.as_bytes() || n == HEBREW_FONT_NAME.as_bytes())
|
||||
&& font
|
||||
.name(b"Name")
|
||||
.is_some_and(|n| n == JAPANESE_FONT_NAME.as_bytes() || n == HEBREW_FONT_NAME.as_bytes() || n == LATIN_FONT_NAME.as_bytes())
|
||||
}
|
||||
|
||||
/// The generated font's `/Name` and its resource prefix: the bundled Latin face, Hebrew, or the
|
||||
/// Japanese ones.
|
||||
fn fallback_names(face: &CraftFont, hebrew: bool) -> (&'static str, &'static str) {
|
||||
if hebrew {
|
||||
(HEBREW_FONT_NAME, "PCHe")
|
||||
} else if face.family == "Inter" {
|
||||
(LATIN_FONT_NAME, "PCLat")
|
||||
} else {
|
||||
(JAPANESE_FONT_NAME, "PCJp")
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a run shown in `font` (a resource name, and the Japanese or Hebrew fallback face it subsets, if
|
||||
@@ -1485,7 +1501,8 @@ const MAX_TYPE3_GLYPHS: usize = 240;
|
||||
struct Type3Fallback {
|
||||
/// The family its glyphs come from ("Shippori Mincho").
|
||||
family: &'static str,
|
||||
/// The fonts' resource names in order; the first is `PCJp` (`PCHe` for Hebrew text).
|
||||
/// The fonts' resource names in order; the first is `PCJp` (`PCLat` for the bundled Latin
|
||||
/// face, `PCHe` for Hebrew text).
|
||||
names: Vec<String>,
|
||||
/// Every unique character with its advance (em), in order of first appearance.
|
||||
widths: Vec<(char, f64)>,
|
||||
@@ -1559,11 +1576,19 @@ fn unicode_hex(ch: char) -> String {
|
||||
encoded.iter().map(|u| format!("{u:04X}")).collect()
|
||||
}
|
||||
|
||||
/// No font this build has can draw `text`. Only Japanese text is about the Japanese face: other
|
||||
/// The same outline for a face that may simply lack the glyph: `None` then, so the caller can try
|
||||
/// the next face (and report the character once none of them has it).
|
||||
fn type3_glyph(face: &CraftFont, ch: char) -> Result<Option<(Vec<u8>, f64)>, EditError> {
|
||||
match japanese_glyph_from(face, ch) {
|
||||
Err(GlyphError::Missing) => Ok(None),
|
||||
_ => type3_path(face, ch).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// No face this build has can draw `text`. Only Japanese text is about the Japanese face: other
|
||||
/// characters are missing from the paragraph's font, and choosing a font that has them works too.
|
||||
fn no_fallback_font(text: &str) -> EditError {
|
||||
let japanese = |c: char| matches!(u32::from(c), 0x3000..=0x30FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF);
|
||||
if text.chars().any(japanese) {
|
||||
if text.chars().any(needs_japanese) {
|
||||
return no_japanese_font();
|
||||
}
|
||||
let Some(ch) = text.chars().find(|c| !c.is_whitespace() && !is_win_ansi_char(*c)) else { return no_japanese_font() };
|
||||
@@ -1575,6 +1600,28 @@ fn no_fallback_font(text: &str) -> EditError {
|
||||
))
|
||||
}
|
||||
|
||||
/// The character the craft-fonts Japanese faces are for: kana, CJK ideographs and their compatibility
|
||||
/// and full-width forms.
|
||||
fn needs_japanese(ch: char) -> bool {
|
||||
matches!(u32::from(ch), 0x3000..=0x30FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF)
|
||||
}
|
||||
|
||||
/// The chosen face has no outline for `ch`, and neither has any other: say which character it is and
|
||||
/// what the writer can do about it. A CJK character without a Japanese face in the build is about
|
||||
/// that build input, which is the one answer that changes it.
|
||||
fn no_glyph_for(face: &CraftFont, ch: char) -> EditError {
|
||||
if needs_japanese(ch) && pdfcraft_fonts::document_japanese_font().is_none() {
|
||||
return no_japanese_font();
|
||||
}
|
||||
EditError::Invalid(format!(
|
||||
"the {} fallback font has no glyph for {ch} (U+{:04X}), and no fallback face covers the text; \
|
||||
choose a font that has it (Load font… or Installed fonts…), or use an official release, which includes \
|
||||
PdfCraft's fallback fonts (to build them in, set CRAFT_FONTS_DIR to a craft-fonts checkout)",
|
||||
face.family,
|
||||
u32::from(ch)
|
||||
))
|
||||
}
|
||||
|
||||
/// This build has no Japanese face to draw replacement text with.
|
||||
fn no_japanese_font() -> EditError {
|
||||
EditError::Invalid(
|
||||
@@ -1599,12 +1646,15 @@ fn no_hebrew_font() -> EditError {
|
||||
}
|
||||
|
||||
/// The faces a Type 3 replacement may draw `text` with, best first. Text with Hebrew takes the
|
||||
/// Hebrew face first (its Latin and digits too, so a line keeps one look), then the Japanese
|
||||
/// faces for anything it lacks; other text takes the Japanese faces, as before.
|
||||
/// Hebrew face first (its Latin and digits too, so a line keeps one look), then the Japanese faces
|
||||
/// and the bundled Latin one for anything it lacks; other text takes the Japanese faces, and the
|
||||
/// bundled Latin face behind them, which covers the Latin, Greek and Cyrillic letters they lack
|
||||
/// (#2593: the Japanese faces have no қ, ғ, ҳ, ә, ө, ү, ұ or һ).
|
||||
fn type3_faces(text: &str, family: crate::added::Family, bold: bool) -> Result<(Vec<&'static CraftFont>, bool), EditError> {
|
||||
let japanese = pdfcraft_fonts::document_japanese_fonts_for_style(family == crate::added::Family::Times, bold);
|
||||
let latin = pdfcraft_fonts::document_latin_fonts(bold);
|
||||
if !text.chars().any(crate::bidi::is_hebrew) {
|
||||
return Ok((japanese, false));
|
||||
return Ok((japanese.into_iter().chain(latin).collect(), false));
|
||||
}
|
||||
// Points are drawn on their letter with the font's positioning, which a one-glyph-per-
|
||||
// character Type 3 font can't do; refuse rather than print them beside the letter.
|
||||
@@ -1612,7 +1662,7 @@ fn type3_faces(text: &str, family: crate::added::Family, bold: bool) -> Result<(
|
||||
return Err(EditError::Invalid("Hebrew points (niqqud) can't be written into a PDF yet; type the text without them".into()));
|
||||
}
|
||||
let hebrew = pdfcraft_fonts::document_hebrew_font(bold).ok_or_else(no_hebrew_font)?;
|
||||
Ok((std::iter::once(hebrew).chain(japanese).collect(), true))
|
||||
Ok((std::iter::once(hebrew).chain(japanese).chain(latin).collect(), true))
|
||||
}
|
||||
|
||||
fn type3_font(
|
||||
@@ -1646,8 +1696,16 @@ fn type3_font(
|
||||
let per_font = max_glyphs.clamp(1, MAX_TYPE3_GLYPHS);
|
||||
let mut glyphs = Vec::with_capacity(chars.len());
|
||||
for (i, ch) in chars.into_iter().enumerate() {
|
||||
// Hebrew text: the first face with the glyph, else the Hebrew face's error.
|
||||
let (path, width) = others.iter().fold(type3_path(face, ch), |found, f| found.or_else(|e| type3_path(f, ch).map_err(|_| e)))?;
|
||||
// The chosen face first, then the faces after it (Hebrew text takes a character the Hebrew
|
||||
// face lacks from the next face that has it); a character no face draws is reported once.
|
||||
let mut drawn = type3_glyph(face, ch)?;
|
||||
for f in others.iter().copied() {
|
||||
if drawn.is_some() {
|
||||
break;
|
||||
}
|
||||
drawn = type3_glyph(f, ch)?;
|
||||
}
|
||||
let Some((path, width)) = drawn else { return Err(no_glyph_for(face, ch)) };
|
||||
let code = u8::try_from((i % per_font).saturating_add(1))
|
||||
.map_err(|_| EditError::Invalid("replacement text has too many unique characters".into()))?;
|
||||
glyphs.push(Type3Glyph { code, ch, path, width });
|
||||
@@ -1671,7 +1729,7 @@ fn type3_font(
|
||||
}
|
||||
|
||||
/// Adds one generated font whose codes 1 to `glyphs.len()` show `glyphs`, and returns its resource
|
||||
/// name (`PCJp`, then `PCJp1`, ...; `PCHe`, ... for Hebrew text). Nothing here can fail: the
|
||||
/// name (`PCLat`, `PCJp`, then `PCJp1`, ...; `PCHe`, ... for Hebrew text). Nothing here can fail: the
|
||||
/// outlines were read already.
|
||||
fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, hebrew: bool, glyphs: Vec<Type3Glyph>) -> String {
|
||||
let last_char = glyphs.len();
|
||||
@@ -1702,7 +1760,8 @@ fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, he
|
||||
let mut font = Dict::new();
|
||||
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(if hebrew { HEBREW_FONT_NAME } else { JAPANESE_FONT_NAME }));
|
||||
let (font_name, prefix) = fallback_names(face, hebrew);
|
||||
font.set(b"Name".to_vec(), Object::name(font_name));
|
||||
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(' ', "")));
|
||||
@@ -1722,7 +1781,6 @@ fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, he
|
||||
font.set(b"Encoding".to_vec(), Object::Dict(encoding));
|
||||
font.set(b"CharProcs".to_vec(), Object::Dict(charprocs));
|
||||
font.set(b"ToUnicode".to_vec(), Object::Ref(cmap_ref));
|
||||
let prefix = if hebrew { "PCHe" } else { "PCJp" };
|
||||
let mut name = String::from(prefix);
|
||||
let mut suffix = 0usize;
|
||||
while fonts_res.contains(name.as_bytes()) {
|
||||
|
||||
Reference in New Issue
Block a user