#[cfg(feature = "fonts")]
pub mod truetype;
pub mod kern_tables;
#[cfg(feature = "fonts")]
pub use truetype::{EmbeddedFont, ShapedGlyph};
use crate::objects::{PdfDict, PdfName, PdfObject};
use pdf_canvas::{BuiltinFont, FontSource};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardFont {
Courier,
CourierBold,
CourierOblique,
CourierBoldOblique,
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Symbol,
ZapfDingbats,
}
impl StandardFont {
pub fn pdf_name(&self) -> &'static str {
match self {
StandardFont::Courier => "Courier",
StandardFont::CourierBold => "Courier-Bold",
StandardFont::CourierOblique => "Courier-Oblique",
StandardFont::CourierBoldOblique => "Courier-BoldOblique",
StandardFont::Helvetica => "Helvetica",
StandardFont::HelveticaBold => "Helvetica-Bold",
StandardFont::HelveticaOblique => "Helvetica-Oblique",
StandardFont::HelveticaBoldOblique => "Helvetica-BoldOblique",
StandardFont::TimesRoman => "Times-Roman",
StandardFont::TimesBold => "Times-Bold",
StandardFont::TimesItalic => "Times-Italic",
StandardFont::TimesBoldItalic => "Times-BoldItalic",
StandardFont::Symbol => "Symbol",
StandardFont::ZapfDingbats => "ZapfDingbats",
}
}
pub fn to_dict(&self) -> PdfDict {
let mut dict = PdfDict::new();
dict.set("Type", PdfObject::Name(PdfName::font()));
dict.set("Subtype", PdfObject::Name(PdfName::new("Type1")));
dict.set("BaseFont", PdfObject::Name(PdfName::new(self.pdf_name())));
if !matches!(self, StandardFont::Symbol | StandardFont::ZapfDingbats) {
dict.set("Encoding", PdfObject::Name(PdfName::new("WinAnsiEncoding")));
}
dict
}
pub fn from_name(name: &str) -> Option<StandardFont> {
match name {
"Courier" => Some(StandardFont::Courier),
"Courier-Bold" => Some(StandardFont::CourierBold),
"Courier-Oblique" => Some(StandardFont::CourierOblique),
"Courier-BoldOblique" => Some(StandardFont::CourierBoldOblique),
"Helvetica" => Some(StandardFont::Helvetica),
"Helvetica-Bold" => Some(StandardFont::HelveticaBold),
"Helvetica-Oblique" => Some(StandardFont::HelveticaOblique),
"Helvetica-BoldOblique" => Some(StandardFont::HelveticaBoldOblique),
"Times-Roman" | "Times" => Some(StandardFont::TimesRoman),
"Times-Bold" => Some(StandardFont::TimesBold),
"Times-Italic" => Some(StandardFont::TimesItalic),
"Times-BoldItalic" => Some(StandardFont::TimesBoldItalic),
"Symbol" => Some(StandardFont::Symbol),
"ZapfDingbats" => Some(StandardFont::ZapfDingbats),
_ => None,
}
}
}
pub struct FontMetrics {
pub avg_width: i32,
pub ascender: i32,
pub descender: i32,
pub cap_height: i32,
pub x_height: i32,
pub line_gap: i32,
}
impl StandardFont {
fn as_builtin_font(&self) -> BuiltinFont {
match self {
StandardFont::Courier => BuiltinFont::Courier,
StandardFont::CourierBold => BuiltinFont::Courier_Bold,
StandardFont::CourierOblique => BuiltinFont::Courier_Oblique,
StandardFont::CourierBoldOblique => BuiltinFont::Courier_BoldOblique,
StandardFont::Helvetica => BuiltinFont::Helvetica,
StandardFont::HelveticaBold => BuiltinFont::Helvetica_Bold,
StandardFont::HelveticaOblique => BuiltinFont::Helvetica_Oblique,
StandardFont::HelveticaBoldOblique => BuiltinFont::Helvetica_BoldOblique,
StandardFont::TimesRoman => BuiltinFont::Times_Roman,
StandardFont::TimesBold => BuiltinFont::Times_Bold,
StandardFont::TimesItalic => BuiltinFont::Times_Italic,
StandardFont::TimesBoldItalic => BuiltinFont::Times_BoldItalic,
StandardFont::Symbol => BuiltinFont::Symbol,
StandardFont::ZapfDingbats => BuiltinFont::ZapfDingbats,
}
}
pub fn string_width(&self, text: &str) -> i32 {
self.as_builtin_font().get_width_raw(text) as i32
}
pub fn metrics(&self) -> FontMetrics {
match self {
StandardFont::Courier | StandardFont::CourierOblique => FontMetrics {
avg_width: 600,
ascender: 629,
descender: -157,
cap_height: 562,
x_height: 426,
line_gap: 269,
},
StandardFont::CourierBold | StandardFont::CourierBoldOblique => FontMetrics {
avg_width: 600,
ascender: 629,
descender: -157,
cap_height: 562,
x_height: 426,
line_gap: 265,
},
StandardFont::Helvetica | StandardFont::HelveticaOblique => FontMetrics {
avg_width: 500,
ascender: 718,
descender: -207,
cap_height: 718,
x_height: 523,
line_gap: 231,
},
StandardFont::HelveticaBold | StandardFont::HelveticaBoldOblique => FontMetrics {
avg_width: 500,
ascender: 718,
descender: -207,
cap_height: 718,
x_height: 523,
line_gap: 265,
},
StandardFont::TimesRoman => FontMetrics {
avg_width: 500,
ascender: 683,
descender: -217,
cap_height: 662,
x_height: 450,
line_gap: 216,
},
StandardFont::TimesBold => FontMetrics {
avg_width: 500,
ascender: 683,
descender: -217,
cap_height: 662,
x_height: 450,
line_gap: 253,
},
StandardFont::TimesItalic => FontMetrics {
avg_width: 500,
ascender: 683,
descender: -217,
cap_height: 662,
x_height: 450,
line_gap: 200,
},
StandardFont::TimesBoldItalic => FontMetrics {
avg_width: 500,
ascender: 683,
descender: -217,
cap_height: 662,
x_height: 450,
line_gap: 239,
},
StandardFont::Symbol => FontMetrics {
avg_width: 500,
ascender: 1010,
descender: -293,
cap_height: 1010,
x_height: 500,
line_gap: 200,
},
StandardFont::ZapfDingbats => FontMetrics {
avg_width: 500,
ascender: 820,
descender: -143,
cap_height: 820,
x_height: 500,
line_gap: 200,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_font_name() {
assert_eq!(StandardFont::Helvetica.pdf_name(), "Helvetica");
assert_eq!(StandardFont::TimesRoman.pdf_name(), "Times-Roman");
}
#[test]
fn test_font_dict() {
let dict = StandardFont::Helvetica.to_dict();
assert_eq!(dict.get_type(), Some("Font"));
assert_eq!(dict.get_name("Subtype").map(|n| n.as_str()), Some("Type1"));
}
#[test]
fn test_from_name() {
assert_eq!(
StandardFont::from_name("Helvetica"),
Some(StandardFont::Helvetica)
);
assert_eq!(StandardFont::from_name("Unknown"), None);
}
#[test]
fn test_string_width() {
assert_eq!(StandardFont::Helvetica.string_width("Hello World"), 5167);
assert_eq!(StandardFont::Courier.string_width("ABCD"), 2400);
assert_eq!(StandardFont::Courier.string_width("abcd"), 2400);
}
#[test]
fn test_kerning_width_matches_prawn() {
use super::kern_tables;
let font = StandardFont::Helvetica;
let text = "The quick brown fox jumps over the lazy dog.";
let raw = font.string_width(text) as f64; let kern = kern_tables::total_kern_adjustment(&font, text) as f64;
let width_9pt = (raw + kern) * 9.0 / 1000.0;
assert!(
(width_9pt - 179.361).abs() < 0.01,
"kerned width at 9pt: {}, expected ~179.361",
width_9pt
);
}
#[test]
fn test_text_box_line_widths() {
use super::kern_tables;
let font = StandardFont::Helvetica;
let size = 9.0;
#[cfg(feature = "prawn-compat")]
let lines: [(&str, f64); 5] = [
(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
219.078,
),
(
"Sed do eiusmod tempor incididunt ut labore et dolore",
211.104,
),
(
"magna aliqua. Ut enim ad minim veniam, quis nostrud",
214.488,
),
(
"exercitation ullamco laboris. Duis aute irure dolor in",
203.661,
),
("reprehenderit in voluptate velit esse cillum.", 169.749),
];
#[cfg(not(feature = "prawn-compat"))]
let lines: [(&str, f64); 5] = [
(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
219.078,
),
(
"Sed do eiusmod tempor incididunt ut labore et dolore",
211.104,
),
(
"magna aliqua. Ut enim ad minim veniam, quis nostrud",
213.948,
),
(
"exercitation ullamco laboris. Duis aute irure dolor in",
203.121,
),
("reprehenderit in voluptate velit esse cillum.", 169.749),
];
for (text, expected_width) in &lines {
let raw = font.string_width(text) as f64;
let kern = kern_tables::total_kern_adjustment(&font, text) as f64;
let width = (raw + kern) * size / 1000.0;
assert!(
(width - expected_width).abs() < 0.01,
"width mismatch for \"{}\": got={:.3}, expected={:.3}",
text,
width,
expected_width
);
}
}
}