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:
parent
bf37f0f05f
commit
ece0442587
3 changed files with 688 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
675
crates/pdftract-core/src/font/type3.rs
Normal file
675
crates/pdftract-core/src/font/type3.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue