Browse / github.com/storytold/pdfcraft / patch
edit: a variation selector after existing text draws the base glyph instead of failing the edit
Existing text edits fail on U+FE0F (variation selector) ## Observed behavior With the official v0.14.0 web release, opening a PDF succeeds, but modifying existing text containing U+FE0F fails with: > the Inter fallback font has no glyph for ️ (U+FE0F), and no fallback face covers the text The edited block contains a rightwards arrow. Loading a full DejaVu Sans font and selecting it for the block unblocks the edit, but moves the entire block in the browser. Changing the whole block's font is therefore not an acceptable way to preserve the existing layout. No private PDF, private filename, screenshot or deployment configuration is included in thi
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- midasmatterhorn (imported from pull request #4899, open)
- Base
f94668f1c8c78c8cf024a2b29375336607a1f76d- Size
- +27 −3 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
6bc5aae449fee6faf8143ade
How it went
No other operator has reported using it yet.
Diff
raw · crates/edit/src/text.rs
@@ -2148,6 +2148,13 @@ fn needs_japanese(ch: char) -> bool { matches!(u32::from(ch), 0x3000..=0x30FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF) } +/// A Unicode variation selector (U+FE00..U+FE0F): it picks a variant of the character before it+/// and has no glyph of its own. The Type 3 fallback draws the nominal base glyph and skips the+/// selector instead of failing the edit on it.+fn is_variation_selector(ch: char) -> bool {+ matches!(u32::from(ch), 0xFE00..=0xFE0F)+}+ /// 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.@@ -2216,10 +2223,11 @@ fn type3_font( max_glyphs: usize, ) -> Result<Type3Fallback, EditError> { let (faces, hebrew) = type3_faces(text, family, bold)?;- // Each unique character once, in order of first appearance.+ // Each unique character once, in order of first appearance. Variation selectors are left+ // out: no face has an outline for them, and the edit must not fail on one. let mut chars = Vec::new(); let mut seen = std::collections::HashSet::new();- for ch in text.chars() {+ for ch in text.chars().filter(|ch| !is_variation_selector(*ch)) { if seen.insert(ch) { chars.push(ch); }@@ -2350,7 +2358,8 @@ fn add_type3_font(doc: &mut Document, fonts_res: &mut Dict, face: &CraftFont, he /// a font is switched only where the text moves to another font. `None` if a character has no code. fn type3_encode(fallback: &Type3Fallback, text: &str) -> Option<Vec<(String, Vec<u8>)>> { let mut runs: Vec<(usize, Vec<u8>)> = Vec::new();- for ch in text.chars() {+ // Selectors show nothing and add no advance: the base glyph before them carries the text.+ for ch in text.chars().filter(|ch| !is_variation_selector(*ch)) { let (font, code) = *fallback.codes.get(&ch)?; match runs.last_mut() { Some((last, bytes)) if *last == font => bytes.push(code),@@ -3527,6 +3536,21 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize mod tests { use super::*; + #[test]+ fn variation_selectors_take_no_glyph_and_no_advance() {+ assert!(is_variation_selector('\u{FE0F}'));+ assert!(!is_variation_selector('\u{27A1}'));+ let fallback = Type3Fallback {+ family: "Inter",+ names: vec!["PCLat".into()],+ widths: vec![('1', 0.556)],+ codes: [('1', (0, 1))].into_iter().collect(),+ };+ let runs = type3_encode(&fallback, "1\u{FE0F}").expect("the selector after a drawn base is skipped, not an error");+ assert_eq!(runs, vec![("PCLat".to_string(), vec![1u8])], "the base draws once; the selector adds no glyph and no advance");+ assert!(type3_encode(&fallback, "\u{FE0F}").is_some_and(|runs| runs.is_empty()), "a lone selector encodes to nothing rather than failing");+ }+ fn block(text: &str, rect: [f64; 4]) -> TextBlock { TextBlock { text: text.into(),