1use crate::error::{ManipError, Result};
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Outlines {
67 TrueType,
69 Cff,
71}
72use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream};
73use std::collections::BTreeMap;
74use std::sync::Arc;
75
76pub(crate) const UNICODE_FONT_RESOURCE: &str = "F__Uni";
78
79#[derive(Clone)]
83pub struct UnicodeFont {
84 data: Arc<Vec<u8>>,
85 units_per_em: u16,
86 outlines: Outlines,
88 fingerprint: [u8; 8],
91 postscript_name: String,
92 variations: Vec<([u8; 4], f32)>,
101}
102
103const WGHT: [u8; 4] = *b"wght";
105const REGULAR_WEIGHT: f32 = 400.0;
107
108impl std::fmt::Debug for UnicodeFont {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("UnicodeFont")
113 .field("name", &self.postscript_name)
114 .field("bytes", &self.data.len())
115 .field("fingerprint", &hex8(&self.fingerprint))
116 .finish()
117 }
118}
119
120impl PartialEq for UnicodeFont {
121 fn eq(&self, other: &Self) -> bool {
122 self.fingerprint == other.fingerprint
123 }
124}
125impl Eq for UnicodeFont {}
126
127#[cfg(feature = "serde")]
128impl serde::Serialize for UnicodeFont {
129 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
130 use serde::ser::SerializeStruct;
131 let mut st = s.serialize_struct("UnicodeFont", 2)?;
133 st.serialize_field("name", &self.postscript_name)?;
134 st.serialize_field("fingerprint", &hex8(&self.fingerprint))?;
135 st.end()
136 }
137}
138
139fn hex8(bytes: &[u8; 8]) -> String {
140 bytes.iter().map(|b| format!("{b:02x}")).collect()
141}
142
143impl UnicodeFont {
144 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
155 let face = ttf_parser::Face::parse(&data, 0)
156 .map_err(|e| ManipError::Other(format!("not a usable font program: {e}")))?;
157
158 let outlines = if face.tables().glyf.is_some() {
161 Outlines::TrueType
162 } else if face.tables().cff.is_some() {
163 Outlines::Cff
164 } else {
165 return Err(ManipError::Other(
166 "font carries neither TrueType (glyf) nor CFF outlines, so there is \
167 nothing to embed"
168 .to_string(),
169 ));
170 };
171
172 let units_per_em = face.units_per_em();
173 if units_per_em == 0 {
174 return Err(ManipError::Other(
175 "font declares unitsPerEm = 0, cannot scale widths".to_string(),
176 ));
177 }
178
179 let postscript_name = face
180 .names()
181 .into_iter()
182 .find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
183 .and_then(|n| n.to_string())
184 .unwrap_or_else(|| "EmbeddedFont".to_string());
185
186 let fingerprint = {
187 use sha2::{Digest, Sha256};
188 let digest = Sha256::digest(&data);
189 let mut fp = [0u8; 8];
190 fp.copy_from_slice(&digest[..8]);
191 fp
192 };
193
194 let variations = face
198 .variation_axes()
199 .into_iter()
200 .find(|a| a.tag.to_bytes() == WGHT)
201 .filter(|a| (a.def_value - REGULAR_WEIGHT).abs() > f32::EPSILON)
202 .map(|a| vec![(WGHT, REGULAR_WEIGHT.clamp(a.min_value, a.max_value))])
203 .unwrap_or_default();
204
205 Ok(Self {
206 data: Arc::new(data),
207 units_per_em,
208 outlines,
209 fingerprint,
210 postscript_name,
211 variations,
212 })
213 }
214
215 #[must_use]
219 pub fn with_variation(mut self, axis: &[u8; 4], value: f32) -> Self {
220 self.variations.retain(|(t, _)| t != axis);
221 self.variations.push((*axis, value));
222 self
223 }
224
225 #[must_use]
227 pub fn is_variable(&self) -> bool {
228 ttf_parser::Face::parse(&self.data, 0)
229 .map(|f| !f.variation_axes().is_empty())
230 .unwrap_or(false)
231 }
232
233 #[must_use]
239 pub fn missing_chars(&self, text: &str) -> Vec<char> {
240 let Ok(face) = ttf_parser::Face::parse(&self.data, 0) else {
241 return text.chars().collect();
242 };
243 let mut missing = Vec::new();
244 for ch in text.chars() {
245 if face.glyph_index(ch).is_none() && !missing.contains(&ch) {
246 missing.push(ch);
247 }
248 }
249 missing
250 }
251
252 #[must_use]
254 pub fn covers(&self, text: &str) -> bool {
255 self.missing_chars(text).is_empty()
256 }
257
258 #[must_use]
260 pub fn name(&self) -> &str {
261 &self.postscript_name
262 }
263}
264
265pub struct UnicodeEncoder {
271 font: UnicodeFont,
272 remapper: subsetter::GlyphRemapper,
273 to_unicode: BTreeMap<u16, String>,
275 advances: BTreeMap<u16, u16>,
277}
278
279impl UnicodeEncoder {
280 #[must_use]
282 pub fn new(font: UnicodeFont) -> Self {
283 Self {
284 font,
285 remapper: subsetter::GlyphRemapper::new(),
288 to_unicode: BTreeMap::new(),
289 advances: BTreeMap::new(),
290 }
291 }
292
293 pub fn encode(&mut self, text: &str) -> Result<Vec<u8>> {
300 let face = ttf_parser::Face::parse(&self.font.data, 0)
301 .map_err(|e| ManipError::Other(format!("font became unparseable: {e}")))?;
302
303 let missing: Vec<char> = text
304 .chars()
305 .filter(|c| face.glyph_index(*c).is_none())
306 .collect();
307 if !missing.is_empty() {
308 let shown: String = missing
309 .iter()
310 .take(8)
311 .map(|c| format!("'{c}' (U+{:04X})", *c as u32))
312 .collect::<Vec<_>>()
313 .join(", ");
314 return Err(ManipError::Other(format!(
315 "font '{}' has no glyph for {shown}{}",
316 self.font.postscript_name,
317 if missing.len() > 8 {
318 format!(" and {} more", missing.len() - 8)
319 } else {
320 String::new()
321 }
322 )));
323 }
324
325 let mut bytes = Vec::with_capacity(text.len() * 2);
326 for ch in text.chars() {
327 let Some(old_gid) = face.glyph_index(ch) else {
329 continue;
330 };
331 let new_gid = self.remapper.remap(old_gid.0);
332
333 self.to_unicode
338 .entry(new_gid)
339 .or_insert_with(|| ch.to_string());
340 if let Some(adv) = face.glyph_hor_advance(old_gid) {
341 self.advances.insert(new_gid, adv);
342 }
343
344 bytes.extend_from_slice(&new_gid.to_be_bytes());
345 }
346 Ok(bytes)
347 }
348
349 #[must_use]
356 pub fn width_em(&self, text: &str) -> f64 {
357 let Ok(face) = ttf_parser::Face::parse(&self.font.data, 0) else {
358 return 0.0;
359 };
360 let upem = f64::from(self.font.units_per_em);
361 text.chars()
362 .filter_map(|c| face.glyph_index(c))
363 .filter_map(|g| face.glyph_hor_advance(g))
364 .map(|a| f64::from(a) / upem)
365 .sum()
366 }
367
368 #[must_use]
370 pub fn is_empty(&self) -> bool {
371 self.to_unicode.is_empty()
372 }
373
374 pub fn embed(self, doc: &mut Document, page_num: u32) -> Result<String> {
377 if self.is_empty() {
378 return Err(ManipError::Other(
379 "nothing encoded; no font to embed".to_string(),
380 ));
381 }
382
383 let subset = if self.font.variations.is_empty() {
384 subsetter::subset(&self.font.data, 0, &self.remapper)
385 } else {
386 let coords: Vec<(subsetter::Tag, f32)> = self
390 .font
391 .variations
392 .iter()
393 .map(|(tag, value)| (subsetter::Tag::new(tag), *value))
394 .collect();
395 subsetter::subset_with_variations(&self.font.data, 0, &coords, &self.remapper)
396 }
397 .map_err(|e| ManipError::Other(format!("font subsetting failed: {e}")))?;
398
399 let face = ttf_parser::Face::parse(&self.font.data, 0)
403 .map_err(|e| ManipError::Other(format!("font became unparseable: {e}")))?;
404
405 let upem = f64::from(self.font.units_per_em);
406 let to_pdf = |v: f64| (v * 1000.0 / upem).round() as i64;
407
408 let descriptor_id = self.write_descriptor(doc, &subset, &face, to_pdf)?;
409 let cid_font_id = self.write_cid_font(doc, descriptor_id, to_pdf);
410 let to_unicode_id = self.write_to_unicode(doc)?;
411
412 let base_font = self.subset_base_font_name();
413 let type0 = dictionary! {
414 "Type" => Object::Name(b"Font".to_vec()),
415 "Subtype" => Object::Name(b"Type0".to_vec()),
416 "BaseFont" => Object::Name(base_font.into_bytes()),
417 "Encoding" => Object::Name(b"Identity-H".to_vec()),
418 "DescendantFonts" => Object::Array(vec![Object::Reference(cid_font_id)]),
419 "ToUnicode" => Object::Reference(to_unicode_id),
420 };
421 let type0_id = doc.add_object(Object::Dictionary(type0));
422
423 register_page_font(doc, page_num, UNICODE_FONT_RESOURCE, type0_id).ok_or_else(|| {
424 ManipError::Other(format!(
425 "could not register the Unicode font in the resources of page {page_num}"
426 ))
427 })?;
428
429 Ok(UNICODE_FONT_RESOURCE.to_string())
430 }
431
432 fn subset_base_font_name(&self) -> String {
436 let tag: String = self.font.fingerprint[..6]
437 .iter()
438 .map(|b| char::from(b'A' + (b % 26)))
439 .collect();
440 let clean: String = self
441 .font
442 .postscript_name
443 .chars()
444 .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
445 .collect();
446 format!("{tag}+{clean}")
447 }
448
449 fn write_descriptor(
450 &self,
451 doc: &mut Document,
452 subset: &[u8],
453 face: &ttf_parser::Face,
454 to_pdf: impl Fn(f64) -> i64,
455 ) -> Result<ObjectId> {
456 let stream_dict = match self.font.outlines {
469 Outlines::TrueType => {
470 dictionary! { "Length1" => Object::Integer(subset.len() as i64) }
471 }
472 Outlines::Cff => {
473 dictionary! { "Subtype" => Object::Name(b"OpenType".to_vec()) }
474 }
475 };
476 let mut stream = Stream::new(stream_dict, subset.to_vec());
477 let _ = stream.compress();
480 let file_id = doc.add_object(Object::Stream(stream));
481
482 let bbox = face.global_bounding_box();
483 let mut descriptor = dictionary! {
484 "Type" => Object::Name(b"FontDescriptor".to_vec()),
485 "FontName" => Object::Name(self.subset_base_font_name().into_bytes()),
486 "Flags" => Object::Integer(4),
489 "FontBBox" => Object::Array(vec![
490 Object::Integer(to_pdf(f64::from(bbox.x_min))),
491 Object::Integer(to_pdf(f64::from(bbox.y_min))),
492 Object::Integer(to_pdf(f64::from(bbox.x_max))),
493 Object::Integer(to_pdf(f64::from(bbox.y_max))),
494 ]),
495 "ItalicAngle" => Object::Real(face.italic_angle()),
496 "Ascent" => Object::Integer(to_pdf(f64::from(face.ascender()))),
497 "Descent" => Object::Integer(to_pdf(f64::from(face.descender()))),
498 "CapHeight" => Object::Integer(
499 face.capital_height()
500 .map_or_else(|| to_pdf(f64::from(face.ascender())), |h| to_pdf(f64::from(h)))
501 ),
502 "StemV" => Object::Integer(80),
506 };
507 match self.font.outlines {
510 Outlines::TrueType => {
511 descriptor.set("FontFile2", Object::Reference(file_id));
512 }
513 Outlines::Cff => {
514 descriptor.set("FontFile3", Object::Reference(file_id));
515 }
516 }
517 Ok(doc.add_object(Object::Dictionary(descriptor)))
518 }
519
520 fn write_cid_font(
521 &self,
522 doc: &mut Document,
523 descriptor_id: ObjectId,
524 to_pdf: impl Fn(f64) -> i64,
525 ) -> ObjectId {
526 let subtype: &[u8] = match self.font.outlines {
530 Outlines::TrueType => b"CIDFontType2",
531 Outlines::Cff => b"CIDFontType0",
532 };
533 let mut cid_font = dictionary! {
534 "Type" => Object::Name(b"Font".to_vec()),
535 "Subtype" => Object::Name(subtype.to_vec()),
536 "BaseFont" => Object::Name(self.subset_base_font_name().into_bytes()),
537 "CIDSystemInfo" => dictionary! {
538 "Registry" => Object::string_literal("Adobe"),
539 "Ordering" => Object::string_literal("Identity"),
540 "Supplement" => Object::Integer(0),
541 },
542 "FontDescriptor" => Object::Reference(descriptor_id),
543
544 "DW" => Object::Integer(1000),
545 "W" => Object::Array(self.build_w_array(to_pdf)),
546 };
547
548 if self.font.outlines == Outlines::TrueType {
553 cid_font.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
554 }
555 doc.add_object(Object::Dictionary(cid_font))
556 }
557
558 fn build_w_array(&self, to_pdf: impl Fn(f64) -> i64) -> Vec<Object> {
561 let mut out = Vec::new();
562 let mut run_start: Option<u16> = None;
563 let mut run: Vec<Object> = Vec::new();
564 let mut prev: Option<u16> = None;
565
566 for (&gid, &adv) in &self.advances {
567 let contiguous = prev.is_some_and(|p| gid == p + 1);
568 if !contiguous {
569 if let Some(start) = run_start.take() {
570 out.push(Object::Integer(i64::from(start)));
571 out.push(Object::Array(std::mem::take(&mut run)));
572 }
573 run_start = Some(gid);
574 }
575 run.push(Object::Integer(to_pdf(f64::from(adv))));
576 prev = Some(gid);
577 }
578 if let Some(start) = run_start {
579 out.push(Object::Integer(i64::from(start)));
580 out.push(Object::Array(run));
581 }
582 out
583 }
584
585 fn write_to_unicode(&self, doc: &mut Document) -> Result<ObjectId> {
588 let mut cmap = String::from(
589 "/CIDInit /ProcSet findresource begin\n\
590 12 dict begin\n\
591 begincmap\n\
592 /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
593 /CMapName /Adobe-Identity-UCS def\n\
594 /CMapType 2 def\n\
595 1 begincodespacerange\n<0000> <FFFF>\n endcodespacerange\n",
596 );
597
598 let entries: Vec<(u16, &String)> = self.to_unicode.iter().map(|(g, s)| (*g, s)).collect();
600 for chunk in entries.chunks(100) {
601 cmap.push_str(&format!("{} beginbfchar\n", chunk.len()));
602 for (gid, text) in chunk {
603 let utf16: String = text
604 .encode_utf16()
605 .map(|u| format!("{u:04X}"))
606 .collect::<Vec<_>>()
607 .join("");
608 cmap.push_str(&format!("<{gid:04X}> <{utf16}>\n"));
609 }
610 cmap.push_str("endbfchar\n");
611 }
612
613 cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
614
615 let mut stream = Stream::new(Dictionary::new(), cmap.into_bytes());
616 let _ = stream.compress();
617 Ok(doc.add_object(Object::Stream(stream)))
618 }
619}
620
621fn register_page_font(
627 doc: &mut Document,
628 page_num: u32,
629 name: &str,
630 font_id: ObjectId,
631) -> Option<()> {
632 let pages = doc.get_pages();
633 let &page_id = pages.get(&page_num)?;
634
635 let resources_entry = doc
636 .get_object(page_id)
637 .ok()
638 .and_then(|o| o.as_dict().ok())
639 .and_then(|d| d.get(b"Resources").ok())
640 .cloned();
641
642 match resources_entry {
643 Some(Object::Reference(res_id)) => {
645 let font_entry = doc
646 .get_object(res_id)
647 .ok()
648 .and_then(|o| o.as_dict().ok())
649 .and_then(|d| d.get(b"Font").ok())
650 .cloned();
651
652 match font_entry {
653 Some(Object::Reference(font_dict_id)) => {
654 let dict = doc.get_object_mut(font_dict_id).ok()?.as_dict_mut().ok()?;
655 dict.set(name, Object::Reference(font_id));
656 }
657 Some(Object::Dictionary(mut d)) => {
658 d.set(name, Object::Reference(font_id));
659 let res = doc.get_object_mut(res_id).ok()?.as_dict_mut().ok()?;
660 res.set("Font", Object::Dictionary(d));
661 }
662 _ => {
663 let mut d = Dictionary::new();
664 d.set(name, Object::Reference(font_id));
665 let res = doc.get_object_mut(res_id).ok()?.as_dict_mut().ok()?;
666 res.set("Font", Object::Dictionary(d));
667 }
668 }
669 }
670 Some(Object::Dictionary(mut res)) => match res.get(b"Font").ok().cloned() {
672 Some(Object::Reference(font_dict_id)) => {
673 let dict = doc.get_object_mut(font_dict_id).ok()?.as_dict_mut().ok()?;
674 dict.set(name, Object::Reference(font_id));
675 }
676 Some(Object::Dictionary(mut d)) => {
677 d.set(name, Object::Reference(font_id));
678 res.set("Font", Object::Dictionary(d));
679 let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
680 page.set("Resources", Object::Dictionary(res));
681 }
682 _ => {
683 let mut d = Dictionary::new();
684 d.set(name, Object::Reference(font_id));
685 res.set("Font", Object::Dictionary(d));
686 let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
687 page.set("Resources", Object::Dictionary(res));
688 }
689 },
690 _ => {
692 let mut fonts = Dictionary::new();
693 fonts.set(name, Object::Reference(font_id));
694 let mut res = Dictionary::new();
695 res.set("Font", Object::Dictionary(fonts));
696 let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
697 page.set("Resources", Object::Dictionary(res));
698 }
699 }
700
701 Some(())
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 fn host_font() -> Option<Vec<u8>> {
711 for path in [
712 "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
713 "/System/Library/Fonts/Supplemental/Arial.ttf",
714 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
715 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
716 ] {
717 if let Ok(data) = std::fs::read(path) {
718 return Some(data);
719 }
720 }
721 None
722 }
723
724 #[test]
725 fn rejects_a_non_font() {
726 let err = UnicodeFont::from_bytes(b"this is not a font".to_vec()).unwrap_err();
727 assert!(err.to_string().contains("not a usable font program"));
728 }
729
730 #[test]
731 fn reports_the_characters_a_font_cannot_write() {
732 let Some(data) = host_font() else { return };
733 let font = UnicodeFont::from_bytes(data).unwrap();
734 assert!(font.covers("Hello"));
736 let missing = font.missing_chars("A\u{E000}B");
738 assert_eq!(missing, vec!['\u{E000}']);
739 }
740
741 #[test]
742 fn encodes_two_bytes_per_glyph_and_registers_them() {
743 let Some(data) = host_font() else { return };
744 let font = UnicodeFont::from_bytes(data).unwrap();
745 let mut enc = UnicodeEncoder::new(font);
746
747 let bytes = enc.encode("AB").unwrap();
748 assert_eq!(bytes.len(), 4, "Identity-H is 2 bytes per glyph");
749 assert!(!enc.is_empty());
750
751 let again = enc.encode("A").unwrap();
754 assert_eq!(&bytes[0..2], &again[..]);
755 }
756
757 #[test]
758 fn refuses_characters_the_font_lacks_instead_of_writing_blanks() {
759 let Some(data) = host_font() else { return };
760 let font = UnicodeFont::from_bytes(data).unwrap();
761 let mut enc = UnicodeEncoder::new(font);
762 let err = enc.encode("A\u{E000}").unwrap_err().to_string();
763 assert!(
764 err.contains("U+E000"),
765 "error should name the character: {err}"
766 );
767 }
768
769 #[test]
770 fn a_repeated_character_maps_back_to_one_character() {
771 let Some(data) = host_font() else { return };
776 let font = UnicodeFont::from_bytes(data).unwrap();
777 let mut enc = UnicodeEncoder::new(font);
778 enc.encode("aaa").unwrap();
779 enc.encode("a").unwrap();
780
781 let mapped = enc
782 .to_unicode
783 .values()
784 .find(|s| s.starts_with('a'))
785 .expect("the glyph for 'a' should be registered");
786 assert_eq!(
787 mapped, "a",
788 "one glyph must map back to exactly one character, got {mapped:?}"
789 );
790 }
791
792 #[test]
793 fn a_variable_font_is_aimed_at_regular_not_its_own_default() {
794 let Some(data) = host_font() else { return };
799 let font = UnicodeFont::from_bytes(data).unwrap();
800
801 if !font.is_variable() {
802 assert!(font.variations.is_empty());
804 return;
805 }
806 for (axis, value) in &font.variations {
807 if axis == &WGHT {
808 assert!(
809 (*value - REGULAR_WEIGHT).abs() < f32::EPSILON,
810 "a variable font should be pinned to Regular, got {value}"
811 );
812 }
813 }
814 }
815
816 #[test]
817 fn an_explicit_variation_overrides_the_automatic_one() {
818 let Some(data) = host_font() else { return };
819 let font = UnicodeFont::from_bytes(data)
820 .unwrap()
821 .with_variation(b"wght", 700.0);
822 let weight = font
823 .variations
824 .iter()
825 .find(|(a, _)| a == &WGHT)
826 .map(|(_, v)| *v);
827 assert_eq!(weight, Some(700.0), "caller's weight must win");
828 }
829
830 #[test]
831 fn w_array_coalesces_consecutive_glyphs() {
832 let Some(data) = host_font() else { return };
833 let font = UnicodeFont::from_bytes(data).unwrap();
834 let mut enc = UnicodeEncoder::new(font);
835 enc.encode("Hello world").unwrap();
836 let w = enc.build_w_array(|v| v.round() as i64);
837 assert_eq!(w.len() % 2, 0);
839 assert!(matches!(w.first(), Some(Object::Integer(_))));
840 assert!(matches!(w.get(1), Some(Object::Array(_))));
841 }
842
843 #[test]
844 fn subset_tag_is_six_uppercase_letters_and_stable() {
845 let Some(data) = host_font() else { return };
846 let font = UnicodeFont::from_bytes(data).unwrap();
847 let a = UnicodeEncoder::new(font.clone()).subset_base_font_name();
848 let b = UnicodeEncoder::new(font).subset_base_font_name();
849 assert_eq!(a, b, "same font must give the same tag across runs");
850 let tag = a.split('+').next().unwrap();
851 assert_eq!(tag.len(), 6);
852 assert!(tag.chars().all(|c| c.is_ascii_uppercase()));
853 }
854}