Skip to main content

pdfrum_edit/font/
embed.rs

1//! Embed a font program, or one of the standard 14, as PDF font objects.
2//!
3//! A caller hands over bytes (or a [`StandardFont`]); this allocates the
4//! `/Font` dictionary chain [`super::collect::admit`] already recognises.
5
6use std::collections::{BTreeMap, HashMap};
7use std::fmt::Write as _;
8
9use pdfrum_font::{
10    FaceEncoding, FontFlags, GlyphSource, StandardFont, canonical_font_name, em_adjust,
11};
12use pdfrum_object::{Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Stream};
13
14use crate::doc::EditDoc;
15use crate::error::Error;
16use crate::font::is_opentype_cff;
17use crate::names;
18
19/// How character codes in a content stream select glyphs of an embedded font.
20///
21/// The oracle's API spells this choice as a boolean; here it is an enum.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum FontEncoding {
24    /// One-byte codes, `/FirstChar` `/LastChar` `/Widths` (ISO 32000-1 §9.6.2).
25    Simple,
26    /// Identity-H, two-byte GIDs as CIDs, `/W` and `/ToUnicode` (§9.7).
27    Composite,
28}
29
30/// A font dictionary this session added, ready to name from a content stream.
31///
32/// [`EmbeddedFont::object`] is the `/Font` resource [`crate::content::ResourceTable::realize`]
33/// will allocate a name for. [`EmbeddedFont::encode`] turns Unicode into the
34/// character codes a `Tj`/`TJ` of that font expects.
35#[derive(Debug, Clone)]
36pub struct EmbeddedFont {
37    font: ObjRef,
38    kind: EncodeKind,
39}
40
41/// How [`EmbeddedFont::encode`] turns a character into codes.
42#[derive(Debug, Clone)]
43enum EncodeKind {
44    /// Identity-H: two-byte big-endian GIDs. Missing cmap entries become 0.
45    Identity { unicode_to_gid: HashMap<u32, u16> },
46    /// One-byte codes from the Unicode cmap, clipped to `0xFF`.
47    Simple { unicode_to_code: HashMap<u32, u8> },
48    /// `/WinAnsiEncoding`, used by the standard 14.
49    WinAnsi,
50    /// A composite font whose `/ToUnicode` the caller wrote: two-byte
51    /// big-endian CIDs, taken by inverting that CMap rather than the
52    /// program's own cmap.
53    CustomCid { unicode_to_cid: HashMap<char, u32> },
54}
55
56impl EmbeddedFont {
57    /// The `/Font` dictionary to name from a page resource.
58    #[must_use]
59    pub fn object(&self) -> ObjRef {
60        self.font
61    }
62
63    /// Character codes for `text` in this font's encoding.
64    ///
65    /// Unmappable characters become `.notdef` (code 0). A composite font
66    /// writes two-byte big-endian GIDs (Identity-H); a simple or standard
67    /// font writes one byte.
68    ///
69    /// A font from [`EditDoc::embed_cid_font`] writes two-byte big-endian
70    /// **CIDs**, and finds them by inverting the caller's `/ToUnicode` CMap
71    /// rather than by consulting the program's cmap. That is not a
72    /// convenience: under a caller-supplied CMap the program's cmap does not
73    /// describe the file's code space, and the CMap is the document's only
74    /// statement of what its codes mean. A character the CMap does not reach
75    /// is code 0, as everywhere else.
76    //
77    // Both rules are the oracle's too: `CPDF_Font::CharCodeFromUnicode`
78    // (`core/fpdfapi/font/cpdf_font.cpp:110-115`) falls back to 0, and for a
79    // font with a caller-supplied CMap it *is*
80    // `to_unicode_map_->ReverseLookup` over that same `/ToUnicode`, reached
81    // by `FPDFText_SetText`.
82    #[must_use]
83    pub fn encode(&self, text: &str) -> Vec<u8> {
84        let mut out = Vec::with_capacity(text.len());
85        let notdef = self.kind.notdef();
86        for ch in text.chars() {
87            push_code(&mut out, self.code_of(ch).unwrap_or(notdef));
88        }
89        out
90    }
91
92    /// Character codes for `text`, refusing a character this font cannot
93    /// draw.
94    ///
95    /// The same encoding as [`EmbeddedFont::encode`], with the `.notdef`
96    /// fallback replaced by an error. This is what a caller who is *placing*
97    /// text wants: a watermark whose degree sign silently became a blank is
98    /// worse than one that refused to be written.
99    ///
100    /// ```
101    /// use pdfrum_edit::{EditDoc, StandardFont};
102    /// use pdfrum_parser::{LoadOptions, load};
103    /// use std::sync::Arc;
104    ///
105    /// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../../tests/files/hello.pdf")[..]);
106    /// let doc = load(bytes, &LoadOptions::default())?;
107    /// let mut edit = EditDoc::new(&doc);
108    /// let font = edit.standard_font(StandardFont::Helvetica)?;
109    ///
110    /// assert!(font.encode_checked("Hi").is_ok());
111    /// // WinAnsi has no Han: refused rather than drawn blank.
112    /// assert_eq!(font.encode_checked("\u{4e00}").unwrap_err().character, '\u{4e00}');
113    /// # Ok::<(), Box<dyn std::error::Error>>(())
114    /// ```
115    ///
116    /// # Errors
117    ///
118    /// [`MissingGlyph`] naming the first character with no glyph, and its
119    /// byte offset in `text`.
120    pub fn encode_checked(&self, text: &str) -> Result<Vec<u8>, MissingGlyph> {
121        let mut out = Vec::with_capacity(text.len());
122        for (offset, ch) in text.char_indices() {
123            let code = self.code_of(ch).ok_or(MissingGlyph {
124                character: ch,
125                offset,
126            })?;
127            push_code(&mut out, code);
128        }
129        Ok(out)
130    }
131
132    /// The code `ch` is drawn through, or `None` when this font has no glyph
133    /// for it.
134    fn code_of(&self, ch: char) -> Option<Code> {
135        let missing = |code: u32| (code != 0).then_some(code);
136        match &self.kind {
137            EncodeKind::Identity { unicode_to_gid } => unicode_to_gid
138                .get(&u32::from(ch))
139                .copied()
140                .and_then(|gid| (gid != 0).then_some(gid))
141                .map(Code::Two),
142            EncodeKind::Simple { unicode_to_code } => unicode_to_code
143                .get(&u32::from(ch))
144                .copied()
145                .and_then(|code| (code != 0).then_some(code))
146                .map(Code::One),
147            EncodeKind::CustomCid { unicode_to_cid } => unicode_to_cid
148                .get(&ch)
149                .copied()
150                .and_then(missing)
151                .and_then(|cid| u16::try_from(cid).ok())
152                .map(Code::Two),
153            EncodeKind::WinAnsi => u16::try_from(u32::from(ch))
154                .ok()
155                .map(|u| FaceEncoding::Latin1.charcode_from_unicode(u))
156                .and_then(missing)
157                .and_then(|code| u8::try_from(code).ok())
158                .map(Code::One),
159        }
160    }
161}
162
163/// One character's code in an embedded font's encoding: one byte for a simple
164/// font, two big-endian for a composite one.
165///
166/// A tiny enum rather than a `(bytes, width)` pair so the two widths cannot be
167/// mixed up at the push.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum Code {
170    /// A one-byte code (simple, or one of the standard 14).
171    One(u8),
172    /// A two-byte big-endian CID or GID (composite).
173    Two(u16),
174}
175
176impl EncodeKind {
177    /// This encoding's `.notdef`, **in its own code width**.
178    ///
179    /// A composite font's codes are two bytes, so its `.notdef` is two zero
180    /// bytes and not one: writing a single zero would shift every code after
181    /// it by a byte and turn the rest of the string into noise.
182    fn notdef(&self) -> Code {
183        match self {
184            Self::Identity { .. } | Self::CustomCid { .. } => Code::Two(0),
185            Self::Simple { .. } | Self::WinAnsi => Code::One(0),
186        }
187    }
188}
189
190/// Append `code`'s bytes.
191fn push_code(out: &mut Vec<u8>, code: Code) {
192    match code {
193        Code::One(byte) => out.push(byte),
194        Code::Two(pair) => out.extend_from_slice(&pair.to_be_bytes()),
195    }
196}
197
198/// A character an embedded font has no glyph for.
199///
200/// What [`EmbeddedFont::encode_checked`] returns instead of writing
201/// `.notdef`.
202///
203/// ```
204/// use pdfrum_edit::MissingGlyph;
205///
206/// let missing = MissingGlyph { character: '\u{4e00}', offset: 3 };
207/// assert_eq!(missing.to_string(), "the font has no glyph for '\u{4e00}' at byte 3");
208/// ```
209#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
210#[error("the font has no glyph for {character:?} at byte {offset}")]
211pub struct MissingGlyph {
212    /// The character with no glyph.
213    pub character: char,
214    /// Its byte offset in the string that was encoded.
215    pub offset: usize,
216}
217
218/// Kind of program, sniffed from the leading bytes.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220enum ProgramKind {
221    TrueType,
222    OpenTypeCff,
223    Type1,
224}
225
226impl EditDoc<'_> {
227    /// Embed `program` as a new `/Font` in this document.
228    ///
229    /// The program kind is detected from its bytes: an `OTTO` tag is
230    /// OpenType/CFF, `0x00010000` / `true` / `typ1` is TrueType, and a PFB
231    /// marker or `%!PS` banner is Type 1.
232    ///
233    /// # Errors
234    ///
235    /// [`Error::UnrecognisedFontProgram`] when no backend can read the bytes,
236    /// [`Error::EmptyFontProgram`] when the face declares no glyphs.
237    pub fn embed_font(
238        &mut self,
239        program: &[u8],
240        encoding: FontEncoding,
241    ) -> Result<EmbeddedFont, Error> {
242        let kind = sniff_kind(program).ok_or(Error::UnrecognisedFontProgram)?;
243        let glyphs = GlyphSource::from_bytes(program).ok_or(Error::UnrecognisedFontProgram)?;
244        if glyphs.num_glyphs() == 0 {
245            return Err(Error::EmptyFontProgram);
246        }
247        match encoding {
248            FontEncoding::Simple => embed_simple(self, program, kind, &glyphs),
249            FontEncoding::Composite => embed_composite(self, program, kind, &glyphs),
250        }
251    }
252
253    /// Embed `program` as a `/Type0` + `/CIDFontType2` font whose
254    /// `/ToUnicode` and `/CIDToGIDMap` are the **caller's**, not generated
255    /// from the program's own cmap.
256    ///
257    /// The dictionary chain is the one [`Self::embed_font`] builds for
258    /// [`FontEncoding::Composite`], with three differences, all of them the
259    /// point of the call:
260    ///
261    /// - **`/CIDToGIDMap`** is a stream holding `cid_to_gid` verbatim: one
262    ///   big-endian `u16` glyph index per CID, indexed by CID (ISO 32000-1
263    ///   §9.7.4.2). [`Self::embed_font`] writes no `/CIDToGIDMap` at all,
264    ///   which means `/Identity` — CID *is* GID.
265    /// - **`/W`** is computed per CID from that map rather than per GID from
266    ///   the cmap: each two-byte entry names the glyph whose advance that CID
267    ///   gets, so the array is dense from CID 0 and holds one width per map
268    ///   entry.
269    /// - **`/ToUnicode`** is a stream holding `to_unicode` verbatim.
270    ///
271    /// The descendant is always a `/CIDFontType2`, because that is what
272    /// `/CIDToGIDMap` means: a `/CIDFontType0` reaches glyphs with the CID as
273    /// the glyph index and never consults the map.
274    ///
275    /// [`EmbeddedFont::encode`] on the result writes CIDs found by inverting
276    /// `to_unicode`, not GIDs found in the program.
277    ///
278    /// # Errors
279    ///
280    /// [`Error::UnrecognisedFontProgram`] when no backend can read the bytes,
281    /// [`Error::EmptyFontProgram`] when the face declares no glyphs,
282    /// [`Error::EmptyToUnicodeCMap`] for an empty `to_unicode`, and
283    /// [`Error::BadCidToGidMap`] when `cid_to_gid` is empty or is not a whole
284    /// number of two-byte entries.
285    pub fn embed_cid_font(
286        &mut self,
287        program: &[u8],
288        to_unicode: &str,
289        cid_to_gid: &[u8],
290    ) -> Result<EmbeddedFont, Error> {
291        // `FPDFText_LoadCidType2Font` (`fpdfsdk/fpdf_edittext.cpp:481-516` →
292        // `LoadCustomCompositeFont` `:281-334`) is where this shape comes
293        // from. The per-CID `/W` walk is `:307-315`, and the "always
294        // TrueType" choice is `FPDF_FONT_TRUETYPE` at `:296-303`; a
295        // `/CIDFontType0` ignoring `/CIDToGIDMap` is
296        // `core/fpdfapi/font/cpdf_cidfont.cpp:508-518`.
297        if to_unicode.is_empty() {
298            return Err(Error::EmptyToUnicodeCMap);
299        }
300        if cid_to_gid.is_empty() || !cid_to_gid.len().is_multiple_of(2) {
301            return Err(Error::BadCidToGidMap(cid_to_gid.len()));
302        }
303        let glyphs = GlyphSource::from_bytes(program).ok_or(Error::UnrecognisedFontProgram)?;
304        if glyphs.num_glyphs() == 0 {
305            return Err(Error::EmptyFontProgram);
306        }
307        Ok(embed_custom_composite(
308            self, program, to_unicode, cid_to_gid, &glyphs,
309        ))
310    }
311
312    /// Add a non-embedded standard-14 Type 1 font (`/BaseFont`, `/Encoding
313    /// /WinAnsiEncoding`, no `/Widths`).
314    ///
315    /// # Errors
316    ///
317    /// This path does not fail today; the [`Result`] is for symmetry with
318    /// [`Self::embed_font`] and for a caller that wants to handle both the
319    /// same way.
320    pub fn standard_font(&mut self, which: StandardFont) -> Result<EmbeddedFont, Error> {
321        let name = canonical_font_name(which);
322        let dict = Dict::from_pairs([
323            (names::TYPE.clone(), Object::Name(names::FONT.clone())),
324            (names::SUBTYPE.clone(), Object::Name(names::TYPE1.clone())),
325            (
326                names::BASE_FONT.clone(),
327                Object::Name(Name::from(name.as_bytes())),
328            ),
329            (
330                names::ENCODING.clone(),
331                Object::Name(names::WIN_ANSI_ENCODING.clone()),
332            ),
333        ]);
334        Ok(EmbeddedFont {
335            font: self.add(Object::Dict(dict)),
336            kind: EncodeKind::WinAnsi,
337        })
338    }
339}
340
341fn sniff_kind(bytes: &[u8]) -> Option<ProgramKind> {
342    if is_opentype_cff(bytes) {
343        return Some(ProgramKind::OpenTypeCff);
344    }
345    if matches!(
346        bytes.get(..4),
347        Some(&[0x00, 0x01, 0x00, 0x00] | b"true" | b"typ1")
348    ) {
349        return Some(ProgramKind::TrueType);
350    }
351    if bytes.first() == Some(&0x80) {
352        return Some(ProgramKind::Type1);
353    }
354    let start = bytes
355        .iter()
356        .position(|b| !b.is_ascii_whitespace())
357        .unwrap_or(bytes.len());
358    let rest = bytes.get(start..).unwrap_or_default();
359    if rest.starts_with(b"%!PS-AdobeFont") || rest.starts_with(b"%!FontType1") {
360        return Some(ProgramKind::Type1);
361    }
362    // Ambiguous header: trust the parser.
363    let glyphs = GlyphSource::from_bytes(bytes)?;
364    if matches!(glyphs, GlyphSource::Type1(_)) {
365        Some(ProgramKind::Type1)
366    } else if glyphs.is_truetype() {
367        Some(ProgramKind::TrueType)
368    } else {
369        Some(ProgramKind::OpenTypeCff)
370    }
371}
372
373fn embed_simple(
374    doc: &mut EditDoc<'_>,
375    program: &[u8],
376    kind: ProgramKind,
377    glyphs: &GlyphSource,
378) -> Result<EmbeddedFont, Error> {
379    let pairs = char_maps(glyphs, 0xFF);
380    if pairs.is_empty() {
381        return Err(Error::EmptyFontProgram);
382    }
383    let name = font_name(glyphs);
384    let first = pairs.first().map_or(0, |p| p.0);
385    let last = pairs.last().map_or(first, |p| p.0);
386    let by_code: BTreeMap<u32, u16> = pairs.iter().copied().collect();
387    let mut widths = Vec::new();
388    let mut code = first;
389    while code <= last {
390        let w = by_code
391            .get(&code)
392            .map_or(0, |gid| glyphs.default_advance(pdfrum_font::Gid(*gid)));
393        widths.push(Object::Int(i64::from(w)));
394        code = code.saturating_add(1);
395        if code == 0 && last == 0 {
396            break;
397        }
398    }
399    let widths_ref = doc.add(Object::Array(Array::of(widths)));
400    let descriptor = load_font_desc(doc, &name, program, kind, glyphs);
401    let subtype = match kind {
402        ProgramKind::Type1 => names::TYPE1.clone(),
403        ProgramKind::TrueType | ProgramKind::OpenTypeCff => names::TRUE_TYPE.clone(),
404    };
405    let dict = Dict::from_pairs([
406        (names::TYPE.clone(), Object::Name(names::FONT.clone())),
407        (names::SUBTYPE.clone(), Object::Name(subtype)),
408        (
409            names::BASE_FONT.clone(),
410            Object::Name(Name::from(name.as_slice())),
411        ),
412        (names::FIRST_CHAR.clone(), Object::Int(i64::from(first))),
413        (names::LAST_CHAR.clone(), Object::Int(i64::from(last))),
414        (names::WIDTHS.clone(), Object::Ref(widths_ref)),
415        (names::FONT_DESCRIPTOR.clone(), Object::Ref(descriptor)),
416    ]);
417    let unicode_to_code: HashMap<u32, u8> = pairs
418        .into_iter()
419        .filter_map(|(cp, _)| u8::try_from(cp).ok().map(|b| (cp, b)))
420        .collect();
421    Ok(EmbeddedFont {
422        font: doc.add(Object::Dict(dict)),
423        kind: EncodeKind::Simple { unicode_to_code },
424    })
425}
426
427fn embed_composite(
428    doc: &mut EditDoc<'_>,
429    program: &[u8],
430    kind: ProgramKind,
431    glyphs: &GlyphSource,
432) -> Result<EmbeddedFont, Error> {
433    let pairs = char_maps(glyphs, 0x0010_FFFF);
434    if pairs.is_empty() {
435        return Err(Error::EmptyFontProgram);
436    }
437    let name = font_name(glyphs);
438    let base = match kind {
439        ProgramKind::Type1 => {
440            let mut n = name.clone();
441            n.extend_from_slice(b"-Identity-H");
442            n
443        }
444        ProgramKind::TrueType | ProgramKind::OpenTypeCff => name.clone(),
445    };
446    let descriptor = load_font_desc(doc, &name, program, kind, glyphs);
447
448    let mut widths_map: BTreeMap<u32, u32> = BTreeMap::new();
449    let mut to_unicode: BTreeMap<u32, u32> = BTreeMap::new();
450    let mut unicode_to_gid = HashMap::new();
451    for (cp, gid) in &pairs {
452        let w = glyphs.default_advance(pdfrum_font::Gid(*gid));
453        widths_map
454            .entry(u32::from(*gid))
455            .or_insert(u32::try_from(w).unwrap_or(0));
456        to_unicode.entry(u32::from(*gid)).or_insert(*cp);
457        unicode_to_gid.insert(*cp, *gid);
458    }
459    let w_ref = doc.add(Object::Array(create_widths_array(&widths_map)));
460    let tounicode_ref = doc.add(Object::Stream(Box::new(Stream::new(
461        Dict::new(),
462        ByteSpan::from(load_unicode(&to_unicode)),
463    ))));
464
465    let cid_subtype = match kind {
466        ProgramKind::TrueType => names::CID_FONT_TYPE2.clone(),
467        ProgramKind::Type1 | ProgramKind::OpenTypeCff => names::CID_FONT_TYPE0.clone(),
468    };
469    let dict = cid_font_dict(doc, &name, cid_subtype, descriptor, w_ref);
470    let cid_font = doc.add(Object::Dict(dict));
471    Ok(EmbeddedFont {
472        font: doc.add(Object::Dict(type0_font_dict(
473            &base,
474            cid_font,
475            tounicode_ref,
476        ))),
477        kind: EncodeKind::Identity { unicode_to_gid },
478    })
479}
480
481/// A composite font whose `/ToUnicode` and `/CIDToGIDMap` the caller supplied.
482///
483/// The one place the two composite paths genuinely differ is `/W`: here it is
484/// keyed by **CID**, walked out of the caller's map, so the array runs dense
485/// from CID 0 and is exactly as long as the map has entries. `embed_composite`
486/// keys it by GID out of the program's cmap instead, which is only the same
487/// array when `/CIDToGIDMap` is the identity.
488fn embed_custom_composite(
489    doc: &mut EditDoc<'_>,
490    program: &[u8],
491    to_unicode: &str,
492    cid_to_gid: &[u8],
493    glyphs: &GlyphSource,
494) -> EmbeddedFont {
495    let name = font_name(glyphs);
496    // `LoadCustomCompositeFont` is `fpdfsdk/fpdf_edittext.cpp:281-334`, and
497    // `FPDF_FONT_TRUETYPE` at `:296-303`: /CIDToGIDMap is what makes this a
498    // /CIDFontType2, and a /CIDFontType0 would ignore it outright.
499    let descriptor = load_font_desc(doc, &name, program, ProgramKind::TrueType, glyphs);
500
501    let mut widths: BTreeMap<u32, u32> = BTreeMap::new();
502    for (cid, entry) in cid_to_gid.as_chunks::<2>().0.iter().enumerate() {
503        let gid = u16::from_be_bytes(*entry);
504        let advance = glyphs.default_advance(pdfrum_font::Gid(gid));
505        widths.insert(
506            u32::try_from(cid).unwrap_or(u32::MAX),
507            u32::try_from(advance).unwrap_or(0),
508        );
509    }
510    let w_ref = doc.add(Object::Array(create_widths_array(&widths)));
511
512    let map_ref = doc.add(Object::Stream(Box::new(Stream::new(
513        Dict::new(),
514        ByteSpan::from(cid_to_gid.to_vec()),
515    ))));
516    let tounicode_ref = doc.add(Object::Stream(Box::new(Stream::new(
517        Dict::new(),
518        ByteSpan::from(to_unicode.as_bytes().to_vec()),
519    ))));
520
521    let mut dict = cid_font_dict(doc, &name, names::CID_FONT_TYPE2.clone(), descriptor, w_ref);
522    dict.push(names::CID_TO_GID_MAP.clone(), Object::Ref(map_ref));
523    let cid_font = doc.add(Object::Dict(dict));
524
525    // The caller's CMap is the file's statement of what its codes mean, so it
526    // is what `encode` inverts — `CPDF_Font::CharCodeFromUnicode` is
527    // `to_unicode_map_->ReverseLookup` over this same stream
528    // (`core/fpdfapi/font/cpdf_font.cpp:110-115`).
529    let unicode_to_cid = pdfrum_font::invert_to_unicode(
530        to_unicode.as_bytes(),
531        &pdfrum_common::Limits::default(),
532        &mut pdfrum_common::Diagnostics::default(),
533    );
534    EmbeddedFont {
535        font: doc.add(Object::Dict(type0_font_dict(
536            &name,
537            cid_font,
538            tounicode_ref,
539        ))),
540        kind: EncodeKind::CustomCid { unicode_to_cid },
541    }
542}
543
544/// The descendant `/CIDFontType0` / `/CIDFontType2` dictionary and its
545/// `/CIDSystemInfo`.
546///
547/// `Adobe`/`Identity`/`0` because the root's `/Encoding` is `Identity-H`: the
548/// CID *is* the code, so no registry ordering applies.
549fn cid_font_dict(
550    doc: &mut EditDoc<'_>,
551    name: &[u8],
552    subtype: Name,
553    descriptor: ObjRef,
554    widths: ObjRef,
555) -> Dict {
556    let system_info = doc.add(Object::Dict(Dict::from_pairs([
557        (
558            names::REGISTRY.clone(),
559            Object::Str(PdfString::literal(b"Adobe")),
560        ),
561        (
562            names::ORDERING.clone(),
563            Object::Str(PdfString::literal(b"Identity")),
564        ),
565        (names::SUPPLEMENT.clone(), Object::Int(0)),
566    ])));
567    Dict::from_pairs([
568        (names::TYPE.clone(), Object::Name(names::FONT.clone())),
569        (names::SUBTYPE.clone(), Object::Name(subtype)),
570        (names::BASE_FONT.clone(), Object::Name(Name::from(name))),
571        (names::CID_SYSTEM_INFO.clone(), Object::Ref(system_info)),
572        (names::FONT_DESCRIPTOR.clone(), Object::Ref(descriptor)),
573        (names::W.clone(), Object::Ref(widths)),
574    ])
575}
576
577/// The `/Type0` root naming `Identity-H`, one descendant and a `/ToUnicode`.
578fn type0_font_dict(base: &[u8], cid_font: ObjRef, to_unicode: ObjRef) -> Dict {
579    Dict::from_pairs([
580        (names::TYPE.clone(), Object::Name(names::FONT.clone())),
581        (names::SUBTYPE.clone(), Object::Name(names::TYPE0.clone())),
582        (
583            names::ENCODING.clone(),
584            Object::Name(names::IDENTITY_H.clone()),
585        ),
586        (names::BASE_FONT.clone(), Object::Name(Name::from(base))),
587        (
588            names::DESCENDANT_FONTS.clone(),
589            Object::Array(Array::of([Object::Ref(cid_font)])),
590        ),
591        (names::TO_UNICODE.clone(), Object::Ref(to_unicode)),
592    ])
593}
594
595/// `/FontDescriptor` plus the program stream.
596///
597/// Flags, bbox, italic angle, ascent/descent and `/StemV` are read off the
598/// face. Three keys are `[oracle-bug]` sites, each fixed here to what
599/// ISO 32000-1 asks for; the `//` comments on the body carry the oracle's
600/// defect and the pdf.js reading for each.
601///
602/// - **OTTO** goes to `/FontFile3` with `/Subtype /OpenType` (table 126),
603///   not `/FontFile2`. `is_opentype_cff` and the subsetter already agree.
604/// - **Type 1** is unwrapped out of its PFB container and stored raw, with
605///   `/Length1` `/Length2` `/Length3` partitioning what was stored
606///   (table 127).
607/// - **`/CapHeight`** is OS/2 `sCapHeight` when the face has it, which is the
608///   capital-letter height ISO 32000-1 table 122 asks for; the ascent is only
609///   the fallback.
610fn load_font_desc(
611    doc: &mut EditDoc<'_>,
612    font_name: &[u8],
613    program: &[u8],
614    kind: ProgramKind,
615    glyphs: &GlyphSource,
616) -> ObjRef {
617    let mut flags = FontFlags::NON_SYMBOLIC;
618    if glyphs.is_fixed_pitch() {
619        flags = flags.with(FontFlags::FIXED_PITCH);
620    }
621    if font_name.windows(5).any(|w| w == b"Serif") {
622        flags = flags.with(FontFlags::SERIF);
623    }
624    if glyphs.is_italic() {
625        flags = flags.with(FontFlags::ITALIC);
626    }
627    if glyphs.is_bold() {
628        flags = flags.with(FontFlags::FORCE_BOLD);
629    }
630
631    let upem = glyphs.units_per_em();
632    let ascent = glyphs.unscaled_ascent().map_or(0, |v| em_adjust(v, upem));
633    let descent = glyphs.unscaled_descent().map_or(0, |v| em_adjust(v, upem));
634    let (x0, y0, x1, y1) = glyphs.unscaled_bbox().map_or((0, 0, 0, 0), |(l, b, r, t)| {
635        (
636            em_adjust(l, upem),
637            em_adjust(b, upem),
638            em_adjust(r, upem),
639            em_adjust(t, upem),
640        )
641    });
642    // [oracle-bug] fpdfsdk/fpdf_edittext.cpp:160-161 always writes
643    // CapHeight = GetAscent(); ISO 32000-1 table 122 has CapHeight as the
644    // capital-letter height. OS/2 sCapHeight is that value when present.
645    // pdf.js is a reader, not a writer: `translateFont` *depends* on the
646    // descriptor value — `let capHeight = descriptor.get("CapHeight")`
647    // (src/core/evaluator.js:4731) — and `Font` stores
648    // `this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS`
649    // (src/core/fonts.js:1123).
650    let cap_height = glyphs.cap_height_unscaled().map_or(ascent, |v| {
651        #[expect(
652            clippy::cast_possible_truncation,
653            reason = "font units rounded to the integer the PDF descriptor stores"
654        )]
655        let n = v.round() as i32;
656        em_adjust(n, upem)
657    });
658    let italic_angle = if glyphs.is_italic() { -12 } else { 0 };
659    let stem_v = if glyphs.is_bold() { 120 } else { 70 };
660
661    let file = embed_program(doc, program, kind);
662    let mut desc = Dict::from_pairs([
663        (
664            names::TYPE.clone(),
665            Object::Name(Name::from("FontDescriptor")),
666        ),
667        (
668            names::FONT_NAME.clone(),
669            Object::Name(Name::from(font_name)),
670        ),
671        (names::FLAGS.clone(), Object::Int(i64::from(flags.bits()))),
672        (
673            names::FONT_BBOX.clone(),
674            Object::Array(Array::of(
675                [x0, y0, x1, y1].map(|v| Object::Int(i64::from(v))),
676            )),
677        ),
678        (names::ITALIC_ANGLE.clone(), Object::Int(italic_angle)),
679        (names::ASCENT.clone(), Object::Int(i64::from(ascent))),
680        (names::DESCENT.clone(), Object::Int(i64::from(descent))),
681        (
682            names::CAP_HEIGHT.clone(),
683            Object::Int(i64::from(cap_height)),
684        ),
685        (names::STEM_V.clone(), Object::Int(stem_v)),
686    ]);
687    let file_key = match kind {
688        ProgramKind::Type1 => names::FONT_FILE.clone(),
689        ProgramKind::TrueType => names::FONT_FILE2.clone(),
690        // [oracle-bug] fpdfsdk/fpdf_edittext.cpp:171-174 writes /FontFile2
691        // for every non-Type1 program, including OTTO. ISO 32000-1 §9.9
692        // table 126 puts a CFF-in-OpenType program in /FontFile3 with
693        // /Subtype /OpenType; `is_opentype_cff` and the subsetter already
694        // follow that. pdf.js is a reader: `isOpenTypeFile` sniffs `OTTO`
695        // (src/core/fonts.js:319) and `getFontFileType` classifies it
696        // `"OpenType"` (`:357`); `checkAndRepair` then requires
697        // `fontFileN === "FontFile3"` for an OTTO CFF CID (`:2761-2763`).
698        // `translateFont` walks FontFile/2/3 (src/core/evaluator.js:4633)
699        // and reads the stream dict `/Subtype` (`:4668-4670`).
700        ProgramKind::OpenTypeCff => names::FONT_FILE3.clone(),
701    };
702    desc.push(file_key, Object::Ref(file));
703    doc.add(Object::Dict(desc))
704}
705
706/// The `/FontFile*` stream: the program bytes, and the length keys that
707/// describe them.
708///
709/// A TrueType or OpenType program is stored exactly as handed over. A **Type 1
710/// program is unwrapped first**: a PFB is a container whose `[0x80, type,
711/// len:u32le]` record headers and `80 03` end marker are framing, not font
712/// data, and ISO 32000-1 §9.9 table 127 defines `/Length1` `/Length2`
713/// `/Length3` as a partition of the *stored* stream. Concatenating the PFB's
714/// record bodies gives the raw PFA-shaped program those three lengths measure.
715fn embed_program(doc: &mut EditDoc<'_>, program: &[u8], kind: ProgramKind) -> ObjRef {
716    let mut dict = Dict::new();
717    let bytes = match kind {
718        ProgramKind::TrueType => {
719            dict.push(
720                names::LENGTH1.clone(),
721                Object::Int(i64::try_from(program.len()).unwrap_or(i64::MAX)),
722            );
723            program.to_vec()
724        }
725        ProgramKind::OpenTypeCff => {
726            dict.push(
727                names::SUBTYPE.clone(),
728                Object::Name(names::OPEN_TYPE.clone()),
729            );
730            program.to_vec()
731        }
732        ProgramKind::Type1 => {
733            // [oracle-bug] fpdfsdk/fpdf_edittext.cpp:166-174 stores the
734            // caller's bytes verbatim under `TODO(npm): Lengths for Type1
735            // fonts.` and writes none of /Length1 /Length2 /Length3 — so a PFB
736            // reaches /FontFile with its segment framing intact and nothing
737            // describing it. ISO 32000-1 §9.9 table 127 requires all three,
738            // and requires them to partition the stream: clear-text portion,
739            // encrypted portion, fixed-content (`cleartomark`) portion of a
740            // *raw* Type 1 program. So the PFB is unwrapped and the raw
741            // program is what we store. pdf.js is a reader: `translateFont`
742            // pulls the three lengths off the stream dict
743            // (src/core/evaluator.js:4672-4674) and `Type1Font.#parseType1`
744            // splits the header and eexec blocks with `properties.length1` /
745            // `properties.length2` (src/core/type1_font.js:195-201) — given
746            // the oracle's output it would slice PFB headers as font data.
747            let file = pdfrum_font::type1_font_file(program);
748            dict.push(names::LENGTH1.clone(), Object::Int(i64::from(file.length1)));
749            dict.push(names::LENGTH2.clone(), Object::Int(i64::from(file.length2)));
750            dict.push(names::LENGTH3.clone(), Object::Int(i64::from(file.length3)));
751            file.program
752        }
753    };
754    doc.add(Object::Stream(Box::new(Stream::new(
755        dict,
756        ByteSpan::from(bytes),
757    ))))
758}
759
760fn font_name(glyphs: &GlyphSource) -> Vec<u8> {
761    match glyphs.postscript_name().filter(|s| !s.is_empty()) {
762        Some(name) => name.into_bytes(),
763        None => b"Untitled".to_vec(),
764    }
765}
766
767fn char_maps(glyphs: &GlyphSource, max: u32) -> Vec<(u32, u16)> {
768    let mut pairs = glyphs.unicode_mappings(max);
769    if pairs.is_empty() {
770        let n = glyphs.num_glyphs().min(max.saturating_add(1));
771        pairs = (0..n)
772            .filter_map(|i| u16::try_from(i).ok().map(|g| (u32::from(g), g)))
773            .collect();
774    }
775    pairs
776}
777
778/// The `/W` array: a run of consecutive CIDs sharing one width goes out as
779/// `first last width`, any other consecutive block as `first [w w …]`.
780fn create_widths_array(widths: &BTreeMap<u32, u32>) -> Array {
781    let mut out = Array::new();
782    let keys: Vec<u32> = widths.keys().copied().collect();
783    let mut i = 0;
784    while i < keys.len() {
785        let Some(&cid) = keys.get(i) else {
786            break;
787        };
788        let width = widths.get(&cid).copied().unwrap_or(0);
789        let mut j = i + 1;
790        let same_run = keys.get(j).copied() == Some(cid.saturating_add(1))
791            && widths.get(&cid.saturating_add(1)).copied() == Some(width);
792        if same_run {
793            let mut last = cid;
794            while let Some(&next) = keys.get(j) {
795                if next != last.saturating_add(1) || widths.get(&next).copied() != Some(width) {
796                    break;
797                }
798                last = next;
799                j += 1;
800            }
801            out.push(Object::Int(i64::from(cid)));
802            out.push(Object::Int(i64::from(last)));
803            out.push(Object::Int(i64::from(width)));
804            i = j;
805            continue;
806        }
807        out.push(Object::Int(i64::from(cid)));
808        let mut inner = Array::new();
809        inner.push(Object::Int(i64::from(width)));
810        let mut last = cid;
811        while let Some(&next) = keys.get(j) {
812            if next != last.saturating_add(1) {
813                break;
814            }
815            inner.push(Object::Int(i64::from(
816                widths.get(&next).copied().unwrap_or(0),
817            )));
818            last = next;
819            j += 1;
820        }
821        out.push(Object::Array(inner));
822        i = j;
823    }
824    out
825}
826
827const TO_UNICODE_START: &str = "/CIDInit /ProcSet findresource begin\n\
82812 dict begin\n\
829begincmap\n\
830/CIDSystemInfo\n\
831<</Registry (Adobe)\n\
832/Ordering (Identity)\n\
833/Supplement 0\n\
834>> def\n\
835/CMapName /Adobe-Identity-H def\n\
836/CMapType 2 def\n\
8371 begincodespacerange\n\
838<0000> <FFFF>\n\
839endcodespacerange\n";
840
841const TO_UNICODE_END: &str = "endcmap\n\
842CMapName currentdict /CMap defineresource pop\n\
843end\n\
844end\n";
845
846const MAX_BF_ENTRIES: usize = 100;
847
848/// The generated `/ToUnicode` CMap: `bfchar` for isolated codes, `bfrange`
849/// for consecutive runs — with the destination as a list when the Unicode
850/// values are not themselves consecutive, and as a single start value when
851/// they are.
852///
853/// Every range is confined to one 256-code block: a `bfrange` may not span a
854/// change of high byte, so a run crossing that boundary is cut at it.
855fn load_unicode(to_unicode: &BTreeMap<u32, u32>) -> Vec<u8> {
856    // A faithful port of `LoadUnicode`
857    // (`core/fpdfapi/edit/cpdf_font_util.cpp:120-273`), including the
858    // `max_extra = 255 - (code % 256)` cap and the `code % 256 == 0` case
859    // that falls back to two singles rather than opening a range.
860    let entries: Vec<(u32, u32)> = to_unicode.iter().map(|(&c, &u)| (c, u)).collect();
861    let mut singles: BTreeMap<u32, u32> = BTreeMap::new();
862    let mut range_list: BTreeMap<(u32, u32), Vec<u32>> = BTreeMap::new();
863    let mut range_consec: BTreeMap<(u32, u32), u32> = BTreeMap::new();
864
865    let mut i = 0;
866    while i < entries.len() {
867        let Some(&(first_code, first_uni)) = entries.get(i) else {
868            break;
869        };
870        let next = entries.get(i + 1).copied();
871        if next.is_none_or(|(c, _)| c != first_code.saturating_add(1)) {
872            singles.insert(first_code, first_uni);
873            i += 1;
874            continue;
875        }
876        let Some((current_code, current_uni)) = next else {
877            break;
878        };
879        i += 1;
880        if current_code % 256 == 0 {
881            singles.insert(first_code, first_uni);
882            singles.insert(current_code, current_uni);
883            i += 1;
884            continue;
885        }
886        let max_extra = 255 - (current_code % 256);
887        if first_uni.saturating_add(1) != current_uni {
888            let mut unicodes = vec![first_uni, current_uni];
889            let mut last_code = current_code;
890            let mut extra = 0;
891            while extra < max_extra {
892                let Some(&(ncode, nuni)) = entries.get(i + 1) else {
893                    break;
894                };
895                if ncode != last_code.saturating_add(1) {
896                    break;
897                }
898                i += 1;
899                last_code = ncode;
900                unicodes.push(nuni);
901                extra += 1;
902            }
903            range_list.insert((first_code, last_code), unicodes);
904            i += 1;
905            continue;
906        }
907        let mut last_code = current_code;
908        let mut last_uni = current_uni;
909        let mut extra = 0;
910        while extra < max_extra {
911            let Some(&(ncode, nuni)) = entries.get(i + 1) else {
912                break;
913            };
914            if ncode != last_code.saturating_add(1) || nuni != last_uni.saturating_add(1) {
915                break;
916            }
917            i += 1;
918            last_code = ncode;
919            last_uni = nuni;
920            extra += 1;
921        }
922        range_consec.insert((first_code, last_code), first_uni);
923        i += 1;
924    }
925
926    let mut buf = String::from(TO_UNICODE_START);
927    write_bfchar(&mut buf, &singles);
928    write_bfrange_list(&mut buf, &range_list);
929    write_bfrange_consec(&mut buf, &range_consec);
930    buf.push_str(TO_UNICODE_END);
931    buf.into_bytes()
932}
933
934fn write_bfchar(buf: &mut String, map: &BTreeMap<u32, u32>) {
935    let items: Vec<(u32, u32)> = map.iter().map(|(&c, &u)| (c, u)).collect();
936    for chunk in items.chunks(MAX_BF_ENTRIES) {
937        let _ = writeln!(buf, "{} beginbfchar", chunk.len());
938        for &(code, uni) in chunk {
939            add_charcode(buf, code);
940            buf.push(' ');
941            add_unicode(buf, uni);
942            buf.push('\n');
943        }
944        buf.push_str("endbfchar\n");
945    }
946}
947
948fn write_bfrange_list(buf: &mut String, map: &BTreeMap<(u32, u32), Vec<u32>>) {
949    let items: Vec<(&(u32, u32), &Vec<u32>)> = map.iter().collect();
950    for chunk in items.chunks(MAX_BF_ENTRIES) {
951        let _ = writeln!(buf, "{} beginbfrange", chunk.len());
952        for ((start, end), unicodes) in chunk {
953            add_charcode(buf, *start);
954            buf.push(' ');
955            add_charcode(buf, *end);
956            buf.push_str(" [");
957            for (i, u) in unicodes.iter().enumerate() {
958                if i > 0 {
959                    buf.push(' ');
960                }
961                add_unicode(buf, *u);
962            }
963            buf.push_str("]\n");
964        }
965        buf.push_str("endbfrange\n");
966    }
967}
968
969fn write_bfrange_consec(buf: &mut String, map: &BTreeMap<(u32, u32), u32>) {
970    let items: Vec<((u32, u32), u32)> = map.iter().map(|(&k, &v)| (k, v)).collect();
971    for chunk in items.chunks(MAX_BF_ENTRIES) {
972        let _ = writeln!(buf, "{} beginbfrange", chunk.len());
973        for &((start, end), uni) in chunk {
974            add_charcode(buf, start);
975            buf.push(' ');
976            add_charcode(buf, end);
977            buf.push(' ');
978            add_unicode(buf, uni);
979            buf.push('\n');
980        }
981        buf.push_str("endbfrange\n");
982    }
983}
984
985fn add_charcode(buf: &mut String, number: u32) {
986    let _ = std::fmt::Write::write_fmt(buf, format_args!("<{number:04X}>"));
987}
988
989fn add_unicode(buf: &mut String, unicode: u32) {
990    let u = if (0xD800..=0xDFFF).contains(&unicode) {
991        0
992    } else {
993        unicode
994    };
995    if let Some(ch) = char::from_u32(u) {
996        let mut utf16 = [0u16; 2];
997        let enc = ch.encode_utf16(&mut utf16);
998        buf.push('<');
999        for unit in enc.iter() {
1000            let _ = std::fmt::Write::write_fmt(buf, format_args!("{unit:04X}"));
1001        }
1002        buf.push('>');
1003    } else {
1004        buf.push_str("<0000>");
1005    }
1006}
1007
1008/// The advance of `codes` in the font `font` names, in thousandths of an em.
1009///
1010/// Falls back to half an em a code — the proportions of a typical Latin face
1011/// — when the dictionary carries no usable metrics.
1012#[must_use]
1013pub fn string_width(
1014    font: ObjRef,
1015    codes: &[u8],
1016    r: &impl pdfrum_object::Resolve,
1017    limits: &pdfrum_common::Limits,
1018    diags: &mut pdfrum_common::Diagnostics,
1019) -> f64 {
1020    let loaded = r
1021        .fetch(font)
1022        .ok()
1023        .as_deref()
1024        .and_then(Object::as_dict)
1025        .and_then(|dict| pdfrum_font::load(dict, r, &pdfrum_font::FontCache::new(), limits, diags));
1026    match loaded {
1027        Some(metrics) => f64::from(metrics.string_width(codes)),
1028        None => 500.0 * f64::from(u32::try_from(codes.len()).unwrap_or(u32::MAX)),
1029    }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::{FontEncoding, ProgramKind, sniff_kind};
1035    use crate::doc::EditDoc;
1036    use crate::names;
1037    use pdfrum_object::{Name, ObjRef, Resolve};
1038    use pdfrum_parser::{Document, LoadOptions, load};
1039    use std::sync::Arc;
1040
1041    const TINY: &[u8] = include_bytes!("../../tests/files/tiny.ttf");
1042    const HELLO: &[u8] = include_bytes!("../../tests/files/hello.pdf");
1043
1044    fn loaded() -> Document {
1045        load(Arc::from(HELLO), &LoadOptions::default()).expect("opens")
1046    }
1047
1048    fn fetch_dict(edit: &EditDoc<'_>, r: ObjRef) -> pdfrum_object::Dict {
1049        edit.fetch(r)
1050            .expect("fetches")
1051            .as_dict()
1052            .expect("dict")
1053            .clone()
1054    }
1055
1056    #[test]
1057    fn sniff_recognises_truetype_and_otto() {
1058        assert_eq!(sniff_kind(TINY), Some(ProgramKind::TrueType));
1059        assert_eq!(sniff_kind(b"OTTO\x00\x01"), Some(ProgramKind::OpenTypeCff));
1060        assert_eq!(sniff_kind(b"%!PS-AdobeFont-1.0"), Some(ProgramKind::Type1));
1061        assert_eq!(sniff_kind(b"\x80\x01"), Some(ProgramKind::Type1));
1062        assert_eq!(sniff_kind(b"not a font"), None);
1063    }
1064
1065    #[test]
1066    fn simple_writes_firstchar_lastchar_widths_and_length1() {
1067        let doc = loaded();
1068        let mut edit = EditDoc::new(&doc);
1069        let font = edit.embed_font(TINY, FontEncoding::Simple).expect("embeds");
1070        let dict = fetch_dict(&edit, font.object());
1071        assert_eq!(
1072            dict.name(names::SUBTYPE).map(Name::as_bytes),
1073            Some(&b"TrueType"[..])
1074        );
1075        let first = dict.direct_int(names::FIRST_CHAR).expect("FirstChar");
1076        let last = dict.direct_int(names::LAST_CHAR).expect("LastChar");
1077        assert!(last >= first);
1078        let widths_ref = dict.reference(names::WIDTHS).expect("Widths");
1079        let widths = edit
1080            .fetch(widths_ref)
1081            .expect("widths")
1082            .as_array()
1083            .expect("array")
1084            .clone();
1085        assert_eq!(
1086            widths.len(),
1087            usize::try_from(last - first + 1).expect("fits")
1088        );
1089
1090        let desc_ref = dict.reference(names::FONT_DESCRIPTOR).expect("desc");
1091        let desc = fetch_dict(&edit, desc_ref);
1092        let flags = desc.direct_int(names::FLAGS).expect("Flags");
1093        assert_eq!(flags & (1 << 5), 1 << 5, "NonSymbolic");
1094        let file = desc.reference(names::FONT_FILE2).expect("FontFile2");
1095        let stream = edit
1096            .fetch(file)
1097            .expect("file")
1098            .as_stream()
1099            .expect("stream")
1100            .clone();
1101        let length1 = stream.dict.direct_int(names::LENGTH1).expect("Length1");
1102        assert_eq!(length1, i64::try_from(TINY.len()).expect("fits"));
1103        assert!(desc.reference(names::FONT_FILE3).is_none());
1104    }
1105
1106    #[test]
1107    fn composite_writes_w_and_tounicode() {
1108        let doc = loaded();
1109        let mut edit = EditDoc::new(&doc);
1110        let font = edit
1111            .embed_font(TINY, FontEncoding::Composite)
1112            .expect("embeds");
1113        let dict = fetch_dict(&edit, font.object());
1114        assert_eq!(
1115            dict.name(names::SUBTYPE).map(Name::as_bytes),
1116            Some(&b"Type0"[..])
1117        );
1118        assert_eq!(
1119            dict.name(names::ENCODING).map(Name::as_bytes),
1120            Some(&b"Identity-H"[..])
1121        );
1122        assert!(dict.reference(names::TO_UNICODE).is_some());
1123        let descendants = dict
1124            .array(names::DESCENDANT_FONTS, &edit)
1125            .expect("DescendantFonts");
1126        let cid_ref = descendants.reference_at(0).expect("cid");
1127        let cid = fetch_dict(&edit, cid_ref);
1128        assert_eq!(
1129            cid.name(names::SUBTYPE).map(Name::as_bytes),
1130            Some(&b"CIDFontType2"[..])
1131        );
1132        let w_ref = cid.reference(names::W).expect("W");
1133        let w = edit
1134            .fetch(w_ref)
1135            .expect("W")
1136            .as_array()
1137            .expect("array")
1138            .clone();
1139        assert!(!w.is_empty());
1140        let tu = dict.reference(names::TO_UNICODE).expect("ToUnicode");
1141        let stream = edit
1142            .fetch(tu)
1143            .expect("tu")
1144            .as_stream()
1145            .expect("stream")
1146            .clone();
1147        let bytes: &[u8] = &stream.data;
1148        assert!(bytes.windows(9).any(|w| w == b"begincmap"));
1149        assert!(bytes.windows(7).any(|w| w == b"endcmap"));
1150    }
1151
1152    #[test]
1153    fn otto_tag_selects_fontfile3() {
1154        let mut otto = TINY.to_vec();
1155        if let Some(head) = otto.get_mut(..4) {
1156            head.copy_from_slice(b"OTTO");
1157        }
1158        let doc = loaded();
1159        let mut edit = EditDoc::new(&doc);
1160        match edit.embed_font(&otto, FontEncoding::Composite) {
1161            Ok(font) => {
1162                let dict = fetch_dict(&edit, font.object());
1163                let descendants = dict
1164                    .array(names::DESCENDANT_FONTS, &edit)
1165                    .expect("DescendantFonts");
1166                let cid = fetch_dict(&edit, descendants.reference_at(0).expect("cid"));
1167                assert_eq!(
1168                    cid.name(names::SUBTYPE).map(Name::as_bytes),
1169                    Some(&b"CIDFontType0"[..])
1170                );
1171                let desc = fetch_dict(&edit, cid.reference(names::FONT_DESCRIPTOR).expect("desc"));
1172                let file = desc.reference(names::FONT_FILE3).expect("FontFile3");
1173                assert!(desc.reference(names::FONT_FILE2).is_none());
1174                let stream = edit
1175                    .fetch(file)
1176                    .expect("file")
1177                    .as_stream()
1178                    .expect("stream")
1179                    .clone();
1180                assert_eq!(
1181                    stream.dict.name(names::SUBTYPE).map(Name::as_bytes),
1182                    Some(&b"OpenType"[..])
1183                );
1184            }
1185            Err(_) => {
1186                // A glyf font with an OTTO tag may fail to parse; the sniff
1187                // still has to call it OpenType/CFF.
1188                assert_eq!(sniff_kind(&otto), Some(ProgramKind::OpenTypeCff));
1189            }
1190        }
1191    }
1192
1193    #[test]
1194    fn encode_unmapped_is_notdef() {
1195        let doc = loaded();
1196        let mut edit = EditDoc::new(&doc);
1197        let font = edit
1198            .embed_font(TINY, FontEncoding::Composite)
1199            .expect("embeds");
1200        let codes = font.encode("Hello");
1201        assert_eq!(codes.len(), 10, "two bytes per character");
1202        let standard = edit
1203            .standard_font(pdfrum_font::StandardFont::Helvetica)
1204            .expect("standard");
1205        assert_eq!(standard.encode("Hi"), b"Hi");
1206        assert_eq!(standard.encode("\u{4e00}"), vec![0]);
1207    }
1208
1209    // A composite font's `.notdef` is **two** bytes, not one. A one-byte
1210    // fallback would shift every code after it by a byte and turn the rest of
1211    // the string into noise, which is exactly what a shared `.notdef`
1212    // constant did when `encode_checked` was factored out of `encode`.
1213    #[test]
1214    fn a_composite_notdef_keeps_the_two_byte_width() {
1215        let doc = loaded();
1216        let mut edit = EditDoc::new(&doc);
1217        let font = edit
1218            .embed_font(TINY, FontEncoding::Composite)
1219            .expect("embeds");
1220        // One unmappable character between two mappable ones: still three
1221        // two-byte codes, with the middle one zero.
1222        let codes = font.encode("H\u{4e00}H");
1223        assert_eq!(codes.len(), 6, "two bytes per character");
1224        assert_eq!(codes.get(2..4), Some(&[0, 0][..]), "a two-byte .notdef");
1225        // And the checked encoder refuses rather than writing a `.notdef` at
1226        // all. `tiny.ttf` has no glyph for either character, so the refusal
1227        // names the first one — which is the rule: the first miss stops the
1228        // whole string.
1229        let missing = font.encode_checked("H\u{4e00}H").expect_err("refused");
1230        assert_eq!((missing.character, missing.offset), ('H', 0));
1231    }
1232
1233    #[test]
1234    fn standard_font_has_no_widths_and_no_program() {
1235        let doc = loaded();
1236        let mut edit = EditDoc::new(&doc);
1237        let font = edit
1238            .standard_font(pdfrum_font::StandardFont::Helvetica)
1239            .expect("standard");
1240        let dict = fetch_dict(&edit, font.object());
1241        assert_eq!(
1242            dict.name(names::SUBTYPE).map(Name::as_bytes),
1243            Some(&b"Type1"[..])
1244        );
1245        assert_eq!(
1246            dict.name(names::BASE_FONT).map(Name::as_bytes),
1247            Some(&b"Helvetica"[..])
1248        );
1249        assert_eq!(
1250            dict.name(names::ENCODING).map(Name::as_bytes),
1251            Some(&b"WinAnsiEncoding"[..])
1252        );
1253        assert!(dict.raw(names::WIDTHS).is_none());
1254        assert!(dict.raw(names::FONT_DESCRIPTOR).is_none());
1255    }
1256
1257    #[test]
1258    fn junk_is_refused() {
1259        let doc = loaded();
1260        let mut edit = EditDoc::new(&doc);
1261        assert!(
1262            edit.embed_font(b"not a font", FontEncoding::Simple)
1263                .is_err()
1264        );
1265        assert!(edit.embed_font(&[], FontEncoding::Composite).is_err());
1266    }
1267}