feat(pdftract-5f92): implement Type3 font loader

Implemented Type3Font struct and loader with:
- /CharProcs: HashMap of glyph name -> stream reference (strips "/" prefix)
- /FirstChar, /LastChar: character code range
- /Widths: per-code advance widths in glyph space
- /FontMatrix: 3x3 transform from glyph to text space (default [0.001 0 0 0.001 0 0])
- /Resources: optional resource dict for nested content streams
- /Encoding: code -> glyph name mapping (FontEncoding)

Key features:
- advance_for() applies FontMatrix[0] to scale glyph space to text space
- Missing /Widths defaults to all-zero with FONT_PARSE_FAILED diagnostic
- Widths length mismatch emits FONT_TYPE3_WIDTHS_LENGTH_MISMATCH
- Missing /CharProcs returns empty map (malformed but valid)
- Arbitrary glyph names supported (not limited to AGL)

Added FontType3WidthsLengthMismatch to diagnostics.rs severity() method.

Acceptance criteria:
- PASS: Valid Type3 font loads with all fields populated
- PASS: /FontMatrix [0.001 0 0 0.001 0 0]: width 500 -> 0.5 text-units
- PASS: /FontMatrix [1 0 0 1 0 0]: width 500 -> 500 text-units
- PASS: Missing /Widths defaults to all-zero with diagnostic
- PASS: Code outside [FirstChar, LastChar] returns advance 0, no panic

All 13 Type3 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-24 01:06:59 -04:00
parent bf37f0f05f
commit ece0442587
3 changed files with 688 additions and 0 deletions

View file

@ -575,6 +575,14 @@ pub enum DiagCode {
/// Phase origin: 2.2
FontEncodingDifferenceOutOfRange,
/// Type3 font /Widths array length mismatch
///
/// Emitted when a Type3 font's /Widths array length does not match
/// LastChar - FirstChar + 1. The array is clamped or padded with zeros.
///
/// Phase origin: 2.4
FontType3WidthsLengthMismatch,
/// Malformed byte sequence in CJK encoding fallback
///
/// Emitted when a CJK byte encoding (Shift-JIS, GB18030, Big5, or EUC-KR)
@ -861,6 +869,7 @@ impl DiagCode {
| DiagCode::FontInvalidCmap
| DiagCode::FontParseFailed
| DiagCode::FontUnsupported
| DiagCode::FontType3WidthsLengthMismatch
| DiagCode::FontCidtogidmapTruncated
| DiagCode::FontEncodingDifferenceOutOfRange => "FONT",
@ -962,6 +971,7 @@ impl DiagCode {
DiagCode::FontUnsupported => "FONT_UNSUPPORTED",
DiagCode::FontCidtogidmapTruncated => "FONT_CIDTOGIDMAP_TRUNCATED",
DiagCode::FontEncodingDifferenceOutOfRange => "ENCODING_DIFFERENCE_OUT_OF_RANGE",
DiagCode::FontType3WidthsLengthMismatch => "FONT_TYPE3_WIDTHS_LENGTH_MISMATCH",
#[cfg(feature = "cjk")]
DiagCode::CjkDecodeMalformed => "CJK_DECODE_MALFORMED",
DiagCode::OcrJbig2Unsupported => "OCR_JBIG2_UNSUPPORTED",
@ -1045,6 +1055,7 @@ impl DiagCode {
| DiagCode::FontInvalidCmap
| DiagCode::FontParseFailed
| DiagCode::FontUnsupported
| DiagCode::FontType3WidthsLengthMismatch
| DiagCode::FontCidtogidmapTruncated
| DiagCode::FontEncodingDifferenceOutOfRange
| DiagCode::OcrJbig2Unsupported

View file

@ -6,6 +6,7 @@
pub mod std14;
pub mod embedded;
pub mod type0;
pub mod type3;
pub mod cmap;
pub mod encoding;
pub mod agl;
@ -18,6 +19,7 @@ pub mod cjk_encoding;
pub use embedded::{EmbeddedFont, FontMetrics, EmptyFontMetrics, GlyphBbox};
pub use type0::{Type0Font, DescendantCIDFont, CIDToGIDMap};
pub use type3::Type3Font;
pub use cmap::{ToUnicodeMap, parse_to_unicode, parse_to_unicode_with_diags};
pub use encoding::{NamedEncoding, DifferencesOverlay, FontEncoding};
pub use agl::{unicode_for_glyph_name, unicode_for_glyph_name_multi};

View file

@ -0,0 +1,675 @@
//! Type 3 font loader.
//!
//! This module implements loading of Type 3 fonts, which are PDF fonts defined
//! by content stream glyphs rather than font programs. Type 3 fonts have:
//! - /CharProcs: dictionary of glyph name -> content stream
//! - /Widths: array of advance widths per character code
//! - /FontMatrix: transform from glyph space to text space
//! - /Resources: resource dictionary for glyph streams
//! - /Encoding: code -> glyph name mapping
use std::collections::HashMap;
use std::sync::Arc;
use crate::diagnostics::{Diagnostic, DiagCode};
use crate::font::encoding::FontEncoding;
use crate::graphics_state::Matrix3x3;
use crate::parser::object::types::{ObjRef, PdfDict, PdfObject};
/// Type 3 font data.
///
/// Type 3 fonts are defined by content stream glyphs rather than font programs.
/// Each glyph is a PDF content stream that draws the glyph shape.
#[derive(Clone, Debug)]
pub struct Type3Font {
/// /CharProcs dictionary: glyph name -> stream object reference.
///
/// These are content streams that draw each glyph. The streams are
/// fetched lazily on first rasterization.
pub char_procs: HashMap<Arc<str>, ObjRef>,
/// /FirstChar: first character code in /Widths array.
pub first_char: u8,
/// /LastChar: last character code in /Widths array.
pub last_char: u8,
/// /Widths array: advance widths in glyph space.
///
/// Length should equal `last_char - first_char + 1`. Widths are
/// in glyph space and must be transformed by /FontMatrix to get
/// text space units.
pub widths: Vec<f64>,
/// /FontMatrix: 3x3 transform from glyph space to text space.
///
/// Default is `[0.001 0 0 0.001 0 0]` (1/1000 scale). Per PDF spec,
/// this matrix is applied during glyph execution.
pub font_matrix: Matrix3x3,
/// /Resources: resource dictionary for glyph content streams.
///
/// Defaults to the page's resource dictionary if absent. This
/// contains form XObjects and fonts referenced by glyph streams.
pub resources: Option<Arc<PdfDict>>,
/// /Encoding: code -> glyph name mapping.
///
/// Uses the same encoding structure as Type1 fonts (named encoding
/// + /Differences overlay).
pub encoding: FontEncoding,
/// Diagnostics emitted during loading.
pub diagnostics: Vec<Diagnostic>,
}
impl Type3Font {
/// Load a Type 3 font from its dictionary.
///
/// # Arguments
///
/// * `font_dict` - The Type 3 font dictionary from the resource dictionary
///
/// # Returns
///
/// A `Type3Font` with all fields populated. Missing fields are handled
/// gracefully with defaults and diagnostics.
pub fn load(font_dict: &PdfDict) -> Self {
let mut diagnostics = Vec::new();
// Parse /CharProcs (dictionary of glyph name -> stream reference)
let char_procs = Self::load_char_procs(font_dict, &mut diagnostics);
// Parse /FirstChar and /LastChar
let (first_char, last_char) = Self::load_char_range(font_dict, &mut diagnostics);
// Parse /Widths array
let widths = Self::load_widths(font_dict, first_char, last_char, &mut diagnostics);
// Parse /FontMatrix (default to [0.001 0 0 0.001 0 0])
let font_matrix = Self::load_font_matrix(font_dict, &mut diagnostics);
// Parse /Resources (optional, defaults to None)
let resources = Self::load_resources(font_dict);
// Parse /Encoding (defaults to StandardEncoding)
let encoding = FontEncoding::parse_from_font(font_dict, None, &mut diagnostics);
Self {
char_procs,
first_char,
last_char,
widths,
font_matrix,
resources,
encoding,
diagnostics,
}
}
/// Load /CharProcs dictionary.
///
/// Maps glyph names to content stream object references. Returns empty
/// map if /CharProcs is missing (malformed but seen in the wild).
fn load_char_procs(
font_dict: &PdfDict,
diagnostics: &mut Vec<Diagnostic>,
) -> HashMap<Arc<str>, ObjRef> {
let mut char_procs = HashMap::new();
let char_procs_obj = match font_dict.get("/CharProcs") {
Some(obj) => obj,
None => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"Type3 font missing /CharProcs dictionary; treating as zero-glyph font",
));
return char_procs;
}
};
let char_procs_dict = match char_procs_obj {
PdfObject::Dict(d) => d.as_ref(),
PdfObject::Ref(_) => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"/CharProcs is indirect reference; not supported, treating as zero-glyph font",
));
return char_procs;
}
_ => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"/CharProcs is not a dictionary; treating as zero-glyph font",
));
return char_procs;
}
};
// Parse each entry: glyph name -> stream reference
for (key, value) in char_procs_dict.iter() {
// Strip leading "/" from glyph name (PDF name syntax vs actual name)
let glyph_name = if key.starts_with('/') {
Arc::from(&key[1..])
} else {
Arc::clone(key)
};
let obj_ref = match value {
PdfObject::Ref(r) => *r,
PdfObject::Stream(_) => {
diagnostics.push(Diagnostic::with_dynamic_no_offset(
DiagCode::FontParseFailed,
format!("/CharProcs entry '{}' is direct stream, not reference; skipping", glyph_name),
));
continue;
}
_ => {
diagnostics.push(Diagnostic::with_dynamic_no_offset(
DiagCode::FontParseFailed,
format!("/CharProcs entry '{}' is not a stream reference; skipping", glyph_name),
));
continue;
}
};
char_procs.insert(glyph_name, obj_ref);
}
char_procs
}
/// Load /FirstChar and /LastChar.
///
/// Defaults to (0, 0) if missing.
fn load_char_range(
font_dict: &PdfDict,
_diagnostics: &mut Vec<Diagnostic>,
) -> (u8, u8) {
let first_char = font_dict
.get("/FirstChar")
.and_then(|obj| obj.as_int())
.map(|i| i.clamp(0, 255) as u8)
.unwrap_or(0);
let last_char = font_dict
.get("/LastChar")
.and_then(|obj| obj.as_int())
.map(|i| i.clamp(0, 255) as u8)
.unwrap_or(0);
(first_char, last_char)
}
/// Load /Widths array.
///
/// Length should equal `last_char - first_char + 1`. On mismatch,
/// emits diagnostic and clamps/pads.
///
/// Missing /Widths defaults to all-zero.
fn load_widths(
font_dict: &PdfDict,
first_char: u8,
last_char: u8,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<f64> {
let expected_len = if last_char >= first_char {
(last_char - first_char + 1) as usize
} else {
0
};
let widths_obj = match font_dict.get("/Widths") {
Some(obj) => obj,
None => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"Type3 font missing /Widths array; defaulting to all-zero",
));
return vec![0.0; expected_len.max(1)];
}
};
let widths_array = match widths_obj {
PdfObject::Array(arr) => arr.as_ref(),
PdfObject::Ref(_) => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"/Widths is indirect reference; not supported, defaulting to all-zero",
));
return vec![0.0; expected_len.max(1)];
}
_ => {
diagnostics.push(Diagnostic::with_static_no_offset(
DiagCode::FontParseFailed,
"/Widths is not an array; defaulting to all-zero",
));
return vec![0.0; expected_len.max(1)];
}
};
// Parse widths as f64
let mut widths: Vec<f64> = widths_array
.iter()
.filter_map(|obj| obj.as_real().or(obj.as_int().map(|i| i as f64)))
.collect();
// Validate length
if widths.len() != expected_len {
diagnostics.push(Diagnostic::with_dynamic_no_offset(
DiagCode::FontType3WidthsLengthMismatch,
format!(
"/Widths length {} does not match LastChar-FirstChar+1 ({}); clamping/padding",
widths.len(),
expected_len
),
));
// Clamp or pad to expected length
if widths.len() > expected_len {
widths.truncate(expected_len);
} else if expected_len > 0 {
while widths.len() < expected_len {
widths.push(0.0);
}
}
}
widths
}
/// Load /FontMatrix.
///
/// Defaults to `[0.001 0 0 0.001 0 0]` if missing (the Type 3 default per spec).
fn load_font_matrix(
font_dict: &PdfDict,
_diagnostics: &mut Vec<Diagnostic>,
) -> Matrix3x3 {
let default_matrix = Matrix3x3::from_pdf_array([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
let matrix_obj = match font_dict.get("/FontMatrix") {
Some(obj) => obj,
None => return default_matrix,
};
let matrix_array = match matrix_obj {
PdfObject::Array(arr) => arr.as_ref(),
PdfObject::Ref(_) => return default_matrix,
_ => return default_matrix,
};
// Parse 6-element array [a b c d e f]
let mut values = [0.0f64; 6];
for (i, elem) in matrix_array.iter().enumerate() {
if i >= 6 {
break;
}
values[i] = elem.as_real().or(elem.as_int().map(|i| i as f64)).unwrap_or(0.0);
}
Matrix3x3::from_pdf_array(values)
}
/// Load /Resources.
///
/// Returns None if /Resources is missing (will default to page resources).
fn load_resources(font_dict: &PdfDict) -> Option<Arc<PdfDict>> {
match font_dict.get("/Resources") {
Some(PdfObject::Dict(d)) => {
// Convert Box<IndexMap> to Arc<IndexMap> by dereferencing
Some(Arc::new((**d).clone()))
}
Some(PdfObject::Ref(_)) => None, // Indirect reference - would need resolution
_ => None,
}
}
/// Get the advance width for a character code in text space units.
///
/// Returns 0 for codes outside [first_char, last_char].
///
/// The advance width is transformed from glyph space to text space
/// by the /FontMatrix: `text_space_width = glyph_space_width * font_matrix.a`
pub fn advance_for(&self, code: u8) -> f64 {
if code < self.first_char || code > self.last_char {
return 0.0;
}
let idx = (code - self.first_char) as usize;
let glyph_space_width = self.widths.get(idx).copied().unwrap_or(0.0);
// Apply FontMatrix[0] (the 'a' coefficient) to scale to text space
// For standard FontMatrix [0.001 0 0 0.001 0 0], this scales by 0.001
glyph_space_width * self.font_matrix.a
}
/// Get the glyph content stream reference for a glyph name.
///
/// Returns None if the glyph name is not in /CharProcs.
pub fn char_proc(&self, glyph_name: &str) -> Option<ObjRef> {
self.char_procs.get(glyph_name).copied()
}
/// Get the number of glyphs in /CharProcs.
pub fn glyph_count(&self) -> usize {
self.char_procs.len()
}
/// Check if this font has a glyph with the given name.
pub fn has_glyph(&self, glyph_name: &str) -> bool {
self.char_procs.contains_key(glyph_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::object::types::intern;
#[test]
fn test_type3_load_minimal() {
// Create a minimal Type3 font dict
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/Subtype"), PdfObject::Name(intern("/Type3")));
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(5));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(500),
PdfObject::Integer(600),
PdfObject::Integer(700),
PdfObject::Integer(800),
PdfObject::Integer(900),
PdfObject::Integer(1000),
])),
);
let font = Type3Font::load(&font_dict);
assert_eq!(font.first_char, 0);
assert_eq!(font.last_char, 5);
assert_eq!(font.widths.len(), 6);
assert_eq!(font.widths[0], 500.0);
assert_eq!(font.widths[5], 1000.0);
// Default FontMatrix
assert_eq!(font.font_matrix.a, 0.001);
// No /CharProcs
assert_eq!(font.glyph_count(), 0);
}
#[test]
fn test_type3_with_char_procs() {
// Create /CharProcs dictionary
let mut char_procs_dict = PdfDict::new();
char_procs_dict.insert(
intern("/A"),
PdfObject::Ref(ObjRef::new(10, 0)),
);
char_procs_dict.insert(
intern("/B"),
PdfObject::Ref(ObjRef::new(11, 0)),
);
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/CharProcs"), PdfObject::Dict(Box::new(char_procs_dict)));
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(1));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(500),
PdfObject::Integer(600),
])),
);
let font = Type3Font::load(&font_dict);
assert_eq!(font.glyph_count(), 2);
assert!(font.has_glyph("A"));
assert!(font.has_glyph("B"));
assert!(!font.has_glyph("C"));
assert_eq!(font.char_proc("A"), Some(ObjRef::new(10, 0)));
assert_eq!(font.char_proc("B"), Some(ObjRef::new(11, 0)));
assert_eq!(font.char_proc("C"), None);
}
#[test]
fn test_advance_for_with_standard_font_matrix() {
// Test with default FontMatrix [0.001 0 0 0.001 0 0]
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(32));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(33));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(500), // code 32
PdfObject::Integer(1000), // code 33
])),
);
let font = Type3Font::load(&font_dict);
// Width 500 * 0.001 = 0.5 text units
assert_eq!(font.advance_for(32), 0.5);
// Width 1000 * 0.001 = 1.0 text units
assert_eq!(font.advance_for(33), 1.0);
}
#[test]
fn test_advance_for_with_identity_font_matrix() {
// Test with identity FontMatrix [1 0 0 1 0 0]
let mut font_dict = PdfDict::new();
font_dict.insert(
intern("/FontMatrix"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(1),
PdfObject::Integer(0),
PdfObject::Integer(0),
PdfObject::Integer(1),
PdfObject::Integer(0),
PdfObject::Integer(0),
])),
);
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(32));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(32));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![PdfObject::Integer(500)])),
);
let font = Type3Font::load(&font_dict);
// Width 500 * 1.0 = 500 text units (no scaling)
assert_eq!(font.advance_for(32), 500.0);
}
#[test]
fn test_advance_for_out_of_range() {
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(32));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(126));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![PdfObject::Integer(500)])),
);
let font = Type3Font::load(&font_dict);
// Before range
assert_eq!(font.advance_for(31), 0.0);
// After range
assert_eq!(font.advance_for(127), 0.0);
}
#[test]
fn test_widths_length_mismatch() {
// /Widths has 3 elements but FirstChar=0, LastChar=5 (expected 6)
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(5));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(500),
PdfObject::Integer(600),
PdfObject::Integer(700),
])),
);
let font = Type3Font::load(&font_dict);
// Should emit diagnostic and pad with zeros
assert_eq!(font.widths.len(), 6);
assert_eq!(font.widths[0], 500.0);
assert_eq!(font.widths[1], 600.0);
assert_eq!(font.widths[2], 700.0);
assert_eq!(font.widths[3], 0.0); // Padded
assert_eq!(font.widths[4], 0.0); // Padded
assert_eq!(font.widths[5], 0.0); // Padded
assert!(font.diagnostics.iter().any(|d| d.code == DiagCode::FontType3WidthsLengthMismatch));
}
#[test]
fn test_widths_too_long() {
// /Widths has 10 elements but FirstChar=0, LastChar=2 (expected 3)
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(2));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![
PdfObject::Integer(500),
PdfObject::Integer(600),
PdfObject::Integer(700),
PdfObject::Integer(800),
PdfObject::Integer(900),
PdfObject::Integer(1000),
PdfObject::Integer(1100),
PdfObject::Integer(1200),
PdfObject::Integer(1300),
PdfObject::Integer(1400),
])),
);
let font = Type3Font::load(&font_dict);
// Should emit diagnostic and truncate
assert_eq!(font.widths.len(), 3);
assert_eq!(font.widths[0], 500.0);
assert_eq!(font.widths[1], 600.0);
assert_eq!(font.widths[2], 700.0);
assert!(font.diagnostics.iter().any(|d| d.code == DiagCode::FontType3WidthsLengthMismatch));
}
#[test]
fn test_missing_widths() {
// No /Widths array
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(5));
let font = Type3Font::load(&font_dict);
// Should default to all-zero
assert_eq!(font.widths.len(), 6);
assert!(font.widths.iter().all(|&w| w == 0.0));
assert!(font.diagnostics.iter().any(|d| d.code == DiagCode::FontParseFailed));
}
#[test]
fn test_missing_char_procs() {
// No /CharProcs dictionary
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(0));
let font = Type3Font::load(&font_dict);
// Should have empty char_procs
assert_eq!(font.glyph_count(), 0);
assert!(font.diagnostics.iter().any(|d| d.code == DiagCode::FontParseFailed));
}
#[test]
fn test_custom_font_matrix() {
// Test custom FontMatrix
let mut font_dict = PdfDict::new();
font_dict.insert(
intern("/FontMatrix"),
PdfObject::Array(Box::new(vec![
PdfObject::Real(0.002),
PdfObject::Integer(0),
PdfObject::Integer(0),
PdfObject::Real(0.002),
PdfObject::Integer(0),
PdfObject::Integer(0),
])),
);
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(0));
font_dict.insert(
intern("/Widths"),
PdfObject::Array(Box::new(vec![PdfObject::Integer(500)])),
);
let font = Type3Font::load(&font_dict);
assert_eq!(font.font_matrix.a, 0.002);
// Width 500 * 0.002 = 1.0 text units
assert_eq!(font.advance_for(0), 1.0);
}
#[test]
fn test_with_resources() {
// Test with /Resources dictionary
let mut resources = PdfDict::new();
resources.insert(intern("/Font"), PdfObject::Array(Box::new(vec![])));
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/Resources"), PdfObject::Dict(Box::new(resources)));
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(0));
let font = Type3Font::load(&font_dict);
assert!(font.resources.is_some());
}
#[test]
fn test_arbitrary_glyph_names() {
// Type3 fonts can have arbitrary glyph names
let mut char_procs_dict = PdfDict::new();
char_procs_dict.insert(
intern("/CustomGlyph1"),
PdfObject::Ref(ObjRef::new(10, 0)),
);
char_procs_dict.insert(
intern("/MySpecialGlyph"),
PdfObject::Ref(ObjRef::new(11, 0)),
);
let mut font_dict = PdfDict::new();
font_dict.insert(intern("/CharProcs"), PdfObject::Dict(Box::new(char_procs_dict)));
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(0));
let font = Type3Font::load(&font_dict);
assert!(font.has_glyph("CustomGlyph1"));
assert!(font.has_glyph("MySpecialGlyph"));
}
#[test]
fn test_encoding_parse() {
// Test that encoding is parsed
let mut font_dict = PdfDict::new();
font_dict.insert(
intern("/Encoding"),
PdfObject::Name(intern("/WinAnsiEncoding")),
);
font_dict.insert(intern("/FirstChar"), PdfObject::Integer(0));
font_dict.insert(intern("/LastChar"), PdfObject::Integer(0));
let font = Type3Font::load(&font_dict);
assert_eq!(font.encoding.base_encoding(), Some(crate::font::encoding::NamedEncoding::WinAnsi));
}
}