use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct FontManager {
fonts: HashMap<String, FontInfo>,
next_font_id: u32,
}
impl FontManager {
pub fn new() -> Self {
let mut manager = Self {
fonts: HashMap::new(),
next_font_id: 1,
};
manager.register_base14_fonts();
manager
}
fn register_base14_fonts(&mut self) {
self.register_font(FontInfo::base14(
"Helvetica",
FontFamily::Helvetica,
FontWeight::Normal,
false,
));
self.register_font(FontInfo::base14(
"Helvetica-Bold",
FontFamily::Helvetica,
FontWeight::Bold,
false,
));
self.register_font(FontInfo::base14(
"Helvetica-Oblique",
FontFamily::Helvetica,
FontWeight::Normal,
true,
));
self.register_font(FontInfo::base14(
"Helvetica-BoldOblique",
FontFamily::Helvetica,
FontWeight::Bold,
true,
));
self.register_font(FontInfo::base14(
"Times-Roman",
FontFamily::Times,
FontWeight::Normal,
false,
));
self.register_font(FontInfo::base14(
"Times-Bold",
FontFamily::Times,
FontWeight::Bold,
false,
));
self.register_font(FontInfo::base14(
"Times-Italic",
FontFamily::Times,
FontWeight::Normal,
true,
));
self.register_font(FontInfo::base14(
"Times-BoldItalic",
FontFamily::Times,
FontWeight::Bold,
true,
));
self.register_font(FontInfo::base14(
"Courier",
FontFamily::Courier,
FontWeight::Normal,
false,
));
self.register_font(FontInfo::base14(
"Courier-Bold",
FontFamily::Courier,
FontWeight::Bold,
false,
));
self.register_font(FontInfo::base14(
"Courier-Oblique",
FontFamily::Courier,
FontWeight::Normal,
true,
));
self.register_font(FontInfo::base14(
"Courier-BoldOblique",
FontFamily::Courier,
FontWeight::Bold,
true,
));
self.register_font(FontInfo::base14_symbol("Symbol"));
self.register_font(FontInfo::base14_symbol("ZapfDingbats"));
}
fn register_font(&mut self, font: FontInfo) {
self.fonts.insert(font.name.clone(), font);
}
pub fn get_font(&self, name: &str) -> Option<&FontInfo> {
self.fonts.get(name)
}
pub fn get_font_or_default(&self, name: &str) -> &FontInfo {
self.fonts.get(name).unwrap_or_else(|| {
self.fonts
.get("Helvetica")
.expect("Helvetica must be registered")
})
}
pub fn text_width(&self, text: &str, font_name: &str, font_size: f32) -> f32 {
let font = self.get_font_or_default(font_name);
font.text_width(text, font_size)
}
pub fn char_width(&self, ch: char, font_name: &str, font_size: f32) -> f32 {
let font = self.get_font_or_default(font_name);
font.char_width(ch) * font_size / 1000.0
}
pub fn next_font_resource_id(&mut self) -> String {
let id = format!("F{}", self.next_font_id);
self.next_font_id += 1;
id
}
pub fn is_base14(&self, name: &str) -> bool {
self.fonts.get(name).map(|f| f.is_base14).unwrap_or(false)
}
pub fn font_names(&self) -> Vec<&str> {
self.fonts.keys().map(|s| s.as_str()).collect()
}
pub fn select_font(&self, family: FontFamily, weight: FontWeight, italic: bool) -> &str {
(match (family, weight, italic) {
(FontFamily::Helvetica, FontWeight::Normal, false) => "Helvetica",
(FontFamily::Helvetica, FontWeight::Bold, false) => "Helvetica-Bold",
(FontFamily::Helvetica, FontWeight::Normal, true) => "Helvetica-Oblique",
(FontFamily::Helvetica, FontWeight::Bold, true) => "Helvetica-BoldOblique",
(FontFamily::Times, FontWeight::Normal, false) => "Times-Roman",
(FontFamily::Times, FontWeight::Bold, false) => "Times-Bold",
(FontFamily::Times, FontWeight::Normal, true) => "Times-Italic",
(FontFamily::Times, FontWeight::Bold, true) => "Times-BoldItalic",
(FontFamily::Courier, FontWeight::Normal, false) => "Courier",
(FontFamily::Courier, FontWeight::Bold, false) => "Courier-Bold",
(FontFamily::Courier, FontWeight::Normal, true) => "Courier-Oblique",
(FontFamily::Courier, FontWeight::Bold, true) => "Courier-BoldOblique",
(FontFamily::Symbol, _, _) => "Symbol",
(FontFamily::ZapfDingbats, _, _) => "ZapfDingbats",
}) as _
}
}
impl Default for FontManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FontFamily {
Helvetica,
Times,
Courier,
Symbol,
ZapfDingbats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontWeight {
#[default]
Normal,
Bold,
}
#[derive(Debug, Clone)]
pub struct FontInfo {
pub name: String,
pub family: FontFamily,
pub weight: FontWeight,
pub italic: bool,
pub is_base14: bool,
widths: FontWidths,
pub ascender: f32,
pub descender: f32,
pub line_gap: f32,
pub cap_height: f32,
pub x_height: f32,
}
impl FontInfo {
fn base14(name: &str, family: FontFamily, weight: FontWeight, italic: bool) -> Self {
let widths = FontWidths::for_base14(name);
let metrics = base14_metrics(name);
Self {
name: name.to_string(),
family,
weight,
italic,
is_base14: true,
widths,
ascender: metrics.0,
descender: metrics.1,
line_gap: metrics.2,
cap_height: metrics.3,
x_height: metrics.4,
}
}
fn base14_symbol(name: &str) -> Self {
Self {
name: name.to_string(),
family: if name == "Symbol" {
FontFamily::Symbol
} else {
FontFamily::ZapfDingbats
},
weight: FontWeight::Normal,
italic: false,
is_base14: true,
widths: FontWidths::Symbol,
ascender: 800.0,
descender: -200.0,
line_gap: 0.0,
cap_height: 700.0,
x_height: 500.0,
}
}
pub fn text_width(&self, text: &str, font_size: f32) -> f32 {
let width_units: f32 = text.chars().map(|c| self.char_width(c)).sum();
width_units * font_size / 1000.0
}
pub fn char_width(&self, ch: char) -> f32 {
self.widths.width_for_char(ch)
}
pub fn line_height(&self, font_size: f32) -> f32 {
(self.ascender - self.descender + self.line_gap) * font_size / 1000.0
}
pub fn line_spacing_factor(&self) -> f32 {
1.2 }
}
#[derive(Debug, Clone)]
enum FontWidths {
Proportional(HashMap<char, f32>),
Monospace(f32),
Symbol,
}
impl FontWidths {
fn for_base14(name: &str) -> Self {
match name {
"Courier" | "Courier-Bold" | "Courier-Oblique" | "Courier-BoldOblique" => {
FontWidths::Monospace(600.0)
},
"Symbol" | "ZapfDingbats" => FontWidths::Symbol,
_ => FontWidths::Proportional(get_base14_widths(name)),
}
}
fn width_for_char(&self, ch: char) -> f32 {
match self {
FontWidths::Proportional(widths) => {
*widths.get(&ch).unwrap_or(&500.0) },
FontWidths::Monospace(width) => *width,
FontWidths::Symbol => 500.0,
}
}
}
fn base14_metrics(name: &str) -> (f32, f32, f32, f32, f32) {
match name {
"Helvetica" | "Helvetica-Oblique" => (718.0, -207.0, 0.0, 718.0, 523.0),
"Helvetica-Bold" | "Helvetica-BoldOblique" => (718.0, -207.0, 0.0, 718.0, 532.0),
"Times-Roman" | "Times-Italic" => (683.0, -217.0, 0.0, 662.0, 450.0),
"Times-Bold" | "Times-BoldItalic" => (676.0, -205.0, 0.0, 676.0, 461.0),
"Courier" | "Courier-Oblique" => (629.0, -157.0, 0.0, 562.0, 426.0),
"Courier-Bold" | "Courier-BoldOblique" => (626.0, -142.0, 0.0, 562.0, 439.0),
_ => (750.0, -250.0, 0.0, 700.0, 500.0), }
}
fn get_base14_widths(name: &str) -> HashMap<char, f32> {
let mut widths = HashMap::new();
let (space_w, period_w, comma_w, hyphen_w, colon_w) = match name {
"Helvetica" | "Helvetica-Oblique" => (278.0, 278.0, 278.0, 333.0, 278.0),
"Helvetica-Bold" | "Helvetica-BoldOblique" => (278.0, 278.0, 278.0, 333.0, 333.0),
"Times-Roman" | "Times-Italic" => (250.0, 250.0, 250.0, 333.0, 278.0),
"Times-Bold" | "Times-BoldItalic" => (250.0, 250.0, 250.0, 333.0, 333.0),
_ => (250.0, 250.0, 250.0, 333.0, 278.0),
};
widths.insert(' ', space_w);
widths.insert('.', period_w);
widths.insert(',', comma_w);
widths.insert('-', hyphen_w);
widths.insert(':', colon_w);
widths.insert(';', 278.0);
widths.insert('!', 333.0);
widths.insert('?', 500.0);
widths.insert('\'', 222.0);
widths.insert('"', 400.0);
widths.insert('(', 333.0);
widths.insert(')', 333.0);
widths.insert('[', 333.0);
widths.insert(']', 333.0);
widths.insert('{', 333.0);
widths.insert('}', 333.0);
widths.insert('/', 278.0);
widths.insert('\\', 278.0);
widths.insert('@', 800.0);
widths.insert('#', 556.0);
widths.insert('$', 556.0);
widths.insert('%', 889.0);
widths.insert('^', 500.0);
widths.insert('&', 722.0);
widths.insert('*', 389.0);
widths.insert('+', 584.0);
widths.insert('=', 584.0);
widths.insert('<', 584.0);
widths.insert('>', 584.0);
widths.insert('|', 280.0);
widths.insert('`', 333.0);
widths.insert('~', 584.0);
widths.insert('_', 556.0);
for digit in '0'..='9' {
widths.insert(digit, 556.0);
}
let uppercase_widths = match name {
"Helvetica" | "Helvetica-Oblique" => [
('A', 722.0),
('B', 722.0),
('C', 722.0),
('D', 722.0),
('E', 667.0),
('F', 611.0),
('G', 778.0),
('H', 722.0),
('I', 278.0),
('J', 556.0),
('K', 722.0),
('L', 611.0),
('M', 833.0),
('N', 722.0),
('O', 778.0),
('P', 667.0),
('Q', 778.0),
('R', 722.0),
('S', 667.0),
('T', 611.0),
('U', 722.0),
('V', 667.0),
('W', 944.0),
('X', 667.0),
('Y', 667.0),
('Z', 611.0),
],
"Helvetica-Bold" | "Helvetica-BoldOblique" => [
('A', 722.0),
('B', 722.0),
('C', 722.0),
('D', 722.0),
('E', 667.0),
('F', 611.0),
('G', 778.0),
('H', 722.0),
('I', 278.0),
('J', 556.0),
('K', 722.0),
('L', 611.0),
('M', 833.0),
('N', 722.0),
('O', 778.0),
('P', 667.0),
('Q', 778.0),
('R', 722.0),
('S', 667.0),
('T', 611.0),
('U', 722.0),
('V', 667.0),
('W', 944.0),
('X', 667.0),
('Y', 667.0),
('Z', 611.0),
],
"Times-Roman" | "Times-Italic" => [
('A', 722.0),
('B', 667.0),
('C', 667.0),
('D', 722.0),
('E', 611.0),
('F', 556.0),
('G', 722.0),
('H', 722.0),
('I', 333.0),
('J', 389.0),
('K', 722.0),
('L', 611.0),
('M', 889.0),
('N', 722.0),
('O', 722.0),
('P', 556.0),
('Q', 722.0),
('R', 667.0),
('S', 556.0),
('T', 611.0),
('U', 722.0),
('V', 722.0),
('W', 944.0),
('X', 722.0),
('Y', 722.0),
('Z', 611.0),
],
"Times-Bold" | "Times-BoldItalic" => [
('A', 722.0),
('B', 667.0),
('C', 722.0),
('D', 722.0),
('E', 667.0),
('F', 611.0),
('G', 778.0),
('H', 778.0),
('I', 389.0),
('J', 500.0),
('K', 778.0),
('L', 667.0),
('M', 944.0),
('N', 722.0),
('O', 778.0),
('P', 611.0),
('Q', 778.0),
('R', 722.0),
('S', 556.0),
('T', 667.0),
('U', 722.0),
('V', 722.0),
('W', 1000.0),
('X', 722.0),
('Y', 722.0),
('Z', 667.0),
],
_ => [
('A', 722.0),
('B', 667.0),
('C', 667.0),
('D', 722.0),
('E', 611.0),
('F', 556.0),
('G', 722.0),
('H', 722.0),
('I', 333.0),
('J', 389.0),
('K', 722.0),
('L', 611.0),
('M', 889.0),
('N', 722.0),
('O', 722.0),
('P', 556.0),
('Q', 722.0),
('R', 667.0),
('S', 556.0),
('T', 611.0),
('U', 722.0),
('V', 722.0),
('W', 944.0),
('X', 722.0),
('Y', 722.0),
('Z', 611.0),
],
};
for (ch, w) in uppercase_widths {
widths.insert(ch, w);
}
let lowercase_widths = match name {
"Helvetica" | "Helvetica-Oblique" => [
('a', 556.0),
('b', 611.0),
('c', 556.0),
('d', 611.0),
('e', 556.0),
('f', 278.0),
('g', 611.0),
('h', 611.0),
('i', 222.0),
('j', 222.0),
('k', 556.0),
('l', 222.0),
('m', 833.0),
('n', 611.0),
('o', 611.0),
('p', 611.0),
('q', 611.0),
('r', 389.0),
('s', 556.0),
('t', 333.0),
('u', 611.0),
('v', 556.0),
('w', 778.0),
('x', 556.0),
('y', 556.0),
('z', 500.0),
],
"Helvetica-Bold" | "Helvetica-BoldOblique" => [
('a', 556.0),
('b', 611.0),
('c', 556.0),
('d', 611.0),
('e', 556.0),
('f', 333.0),
('g', 611.0),
('h', 611.0),
('i', 278.0),
('j', 278.0),
('k', 556.0),
('l', 278.0),
('m', 889.0),
('n', 611.0),
('o', 611.0),
('p', 611.0),
('q', 611.0),
('r', 389.0),
('s', 556.0),
('t', 333.0),
('u', 611.0),
('v', 556.0),
('w', 778.0),
('x', 556.0),
('y', 556.0),
('z', 500.0),
],
"Times-Roman" | "Times-Italic" => [
('a', 444.0),
('b', 500.0),
('c', 444.0),
('d', 500.0),
('e', 444.0),
('f', 333.0),
('g', 500.0),
('h', 500.0),
('i', 278.0),
('j', 278.0),
('k', 500.0),
('l', 278.0),
('m', 778.0),
('n', 500.0),
('o', 500.0),
('p', 500.0),
('q', 500.0),
('r', 333.0),
('s', 389.0),
('t', 278.0),
('u', 500.0),
('v', 500.0),
('w', 722.0),
('x', 500.0),
('y', 500.0),
('z', 444.0),
],
"Times-Bold" | "Times-BoldItalic" => [
('a', 500.0),
('b', 556.0),
('c', 444.0),
('d', 556.0),
('e', 444.0),
('f', 333.0),
('g', 500.0),
('h', 556.0),
('i', 278.0),
('j', 333.0),
('k', 556.0),
('l', 278.0),
('m', 833.0),
('n', 556.0),
('o', 500.0),
('p', 556.0),
('q', 556.0),
('r', 444.0),
('s', 389.0),
('t', 333.0),
('u', 556.0),
('v', 500.0),
('w', 722.0),
('x', 500.0),
('y', 500.0),
('z', 444.0),
],
_ => [
('a', 444.0),
('b', 500.0),
('c', 444.0),
('d', 500.0),
('e', 444.0),
('f', 333.0),
('g', 500.0),
('h', 500.0),
('i', 278.0),
('j', 278.0),
('k', 500.0),
('l', 278.0),
('m', 778.0),
('n', 500.0),
('o', 500.0),
('p', 500.0),
('q', 500.0),
('r', 333.0),
('s', 389.0),
('t', 278.0),
('u', 500.0),
('v', 500.0),
('w', 722.0),
('x', 500.0),
('y', 500.0),
('z', 444.0),
],
};
for (ch, w) in lowercase_widths {
widths.insert(ch, w);
}
widths
}
#[derive(Debug)]
pub struct TextLayout {
font_manager: FontManager,
}
impl TextLayout {
pub fn new() -> Self {
Self {
font_manager: FontManager::new(),
}
}
pub fn with_font_manager(font_manager: FontManager) -> Self {
Self { font_manager }
}
pub fn wrap_text(
&self,
text: &str,
font_name: &str,
font_size: f32,
max_width: f32,
) -> Vec<(String, f32)> {
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0.0;
let space_width = self.font_manager.char_width(' ', font_name, font_size);
for word in text.split_whitespace() {
let word_width = self.font_manager.text_width(word, font_name, font_size);
if current_line.is_empty() {
current_line = word.to_string();
current_width = word_width;
} else if current_width + space_width + word_width <= max_width {
current_line.push(' ');
current_line.push_str(word);
current_width += space_width + word_width;
} else {
lines.push((current_line, current_width));
current_line = word.to_string();
current_width = word_width;
}
}
if !current_line.is_empty() {
lines.push((current_line, current_width));
}
if lines.is_empty() {
lines.push((String::new(), 0.0));
}
lines
}
pub fn text_bounds(
&self,
text: &str,
font_name: &str,
font_size: f32,
max_width: f32,
) -> (f32, f32) {
let lines = self.wrap_text(text, font_name, font_size, max_width);
let font = self.font_manager.get_font_or_default(font_name);
let line_height = font.line_height(font_size) * font.line_spacing_factor();
let max_line_width = lines.iter().map(|(_, w)| *w).fold(0.0_f32, f32::max);
let total_height = lines.len() as f32 * line_height;
(max_line_width, total_height)
}
pub fn font_manager(&self) -> &FontManager {
&self.font_manager
}
}
impl Default for TextLayout {
fn default() -> Self {
Self::new()
}
}
use crate::fonts::{FontSubsetter, TrueTypeFont, UnicodeEncoder};
#[derive(Debug)]
pub struct EmbeddedFont {
pub name: String,
subset_name: Option<String>,
font_data: Arc<Vec<u8>>,
subsetter: FontSubsetter,
#[allow(dead_code)]
encoder: UnicodeEncoder,
glyph_lookup: HashMap<u32, u16>,
glyph_widths: HashMap<u16, u16>,
pub ascender: i32,
pub descender: i32,
pub cap_height: i32,
pub x_height: i32,
pub bbox: (i32, i32, i32, i32),
pub flags: u32,
pub stem_v: i16,
pub italic_angle: f32,
#[allow(dead_code)]
units_per_em: u16,
}
impl EmbeddedFont {
pub fn from_data(name: Option<String>, data: Vec<u8>) -> Result<Self, String> {
let font =
TrueTypeFont::parse(&data).map_err(|e| format!("Failed to parse font: {}", e))?;
let font_name = name.unwrap_or_else(|| {
font.postscript_name()
.unwrap_or_else(|| "Unknown".to_string())
});
let metrics = crate::fonts::FontMetrics::from_font(&font);
let mut glyph_lookup = HashMap::new();
let mut glyph_widths = HashMap::new();
for codepoint in font.supported_codepoints() {
if let Some(gid) = font.glyph_id(codepoint) {
glyph_lookup.insert(codepoint, gid);
glyph_widths.insert(gid, font.glyph_width(gid));
}
}
Ok(Self {
name: font_name,
subset_name: None,
font_data: Arc::new(data),
subsetter: FontSubsetter::new(),
encoder: UnicodeEncoder::new(),
glyph_lookup,
glyph_widths,
ascender: metrics.pdf_ascender(),
descender: metrics.pdf_descender(),
cap_height: metrics.pdf_cap_height(),
x_height: metrics.to_pdf_units(metrics.x_height),
bbox: metrics.pdf_bbox(),
flags: metrics.flags,
stem_v: metrics.stem_v,
italic_angle: metrics.italic_angle,
units_per_em: metrics.units_per_em,
})
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, String> {
let data =
std::fs::read(path.as_ref()).map_err(|e| format!("Failed to read font file: {}", e))?;
Self::from_data(None, data)
}
pub fn glyph_id(&self, codepoint: u32) -> Option<u16> {
self.glyph_lookup.get(&codepoint).copied()
}
pub fn glyph_width(&self, gid: u16) -> u16 {
self.glyph_widths.get(&gid).copied().unwrap_or(500)
}
pub fn char_width(&self, codepoint: u32) -> u16 {
self.glyph_id(codepoint)
.map(|gid| self.glyph_width(gid))
.unwrap_or(500)
}
pub fn text_width(&self, text: &str, font_size: f32) -> f32 {
let width_units: f32 = text.chars().map(|c| self.char_width(c as u32) as f32).sum();
width_units * font_size / 1000.0
}
pub fn use_string(&mut self, text: &str) {
for ch in text.chars() {
let codepoint = ch as u32;
if let Some(gid) = self.glyph_id(codepoint) {
self.subsetter.use_char(codepoint, gid);
}
}
}
pub fn encode_string(&mut self, text: &str) -> String {
self.use_string(text);
let mut hex = String::with_capacity(text.len() * 4 + 2);
hex.push('<');
for ch in text.chars() {
let glyph_id = self.glyph_id(ch as u32).unwrap_or(0);
hex.push_str(&format!("{:04X}", glyph_id));
}
hex.push('>');
hex
}
pub fn subset_name(&mut self) -> &str {
if self.subset_name.is_none() {
self.subset_name = Some(self.subsetter.subset_font_name(&self.name));
}
self.subset_name.as_ref().unwrap()
}
pub fn font_data(&self) -> &[u8] {
&self.font_data
}
pub fn subset_stats(&self) -> (usize, usize) {
(self.subsetter.char_count(), self.subsetter.glyph_count())
}
pub fn is_used(&self) -> bool {
!self.subsetter.is_empty()
}
pub fn generate_widths_array(&self) -> String {
let mut result = String::from("[");
let used_glyphs = self.subsetter.used_glyphs();
let glyphs: Vec<_> = used_glyphs.iter().copied().collect();
let mut i = 0;
while i < glyphs.len() {
let start = glyphs[i];
let mut widths = vec![self.glyph_width(start)];
while i + 1 < glyphs.len() && glyphs[i + 1] == glyphs[i] + 1 {
i += 1;
widths.push(self.glyph_width(glyphs[i]));
}
result.push_str(&format!("{} [", start));
for (j, w) in widths.iter().enumerate() {
if j > 0 {
result.push(' ');
}
result.push_str(&w.to_string());
}
result.push(']');
i += 1;
}
result.push(']');
result
}
pub fn generate_tounicode_cmap(&self) -> String {
let used_chars = self.subsetter.used_chars();
let mut cmap = String::new();
cmap.push_str("/CIDInit /ProcSet findresource begin\n");
cmap.push_str("12 dict begin\n");
cmap.push_str("begincmap\n");
cmap.push_str("/CIDSystemInfo <<\n");
cmap.push_str(" /Registry (Adobe)\n");
cmap.push_str(" /Ordering (UCS)\n");
cmap.push_str(" /Supplement 0\n");
cmap.push_str(">> def\n");
cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
cmap.push_str("/CMapType 2 def\n");
cmap.push_str("1 begincodespacerange\n");
cmap.push_str("<0000> <FFFF>\n");
cmap.push_str("endcodespacerange\n");
let mut mappings: Vec<(u16, u32)> = used_chars
.iter()
.map(|(&unicode, &gid)| (gid, unicode))
.collect();
mappings.sort_by_key(|&(gid, _)| gid);
let chunks: Vec<_> = mappings.chunks(100).collect();
for chunk in chunks {
cmap.push_str(&format!("{} beginbfchar\n", chunk.len()));
for &(gid, unicode) in chunk {
if unicode <= 0xFFFF {
cmap.push_str(&format!("<{:04X}> <{:04X}>\n", gid, unicode));
} else {
let high = ((unicode - 0x10000) >> 10) + 0xD800;
let low = ((unicode - 0x10000) & 0x3FF) + 0xDC00;
cmap.push_str(&format!("<{:04X}> <{:04X}{:04X}>\n", gid, high, low));
}
}
cmap.push_str("endbfchar\n");
}
cmap.push_str("endcmap\n");
cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
cmap.push_str("end\n");
cmap.push_str("end\n");
cmap
}
}
#[derive(Debug, Default)]
pub struct EmbeddedFontManager {
fonts: HashMap<String, EmbeddedFont>,
resource_ids: HashMap<String, String>,
next_id: u32,
}
impl EmbeddedFontManager {
pub fn new() -> Self {
Self {
fonts: HashMap::new(),
resource_ids: HashMap::new(),
next_id: 1,
}
}
pub fn register(&mut self, name: impl Into<String>, font: EmbeddedFont) -> String {
let name = name.into();
let resource_id = format!("F{}", self.next_id);
self.next_id += 1;
self.resource_ids.insert(name.clone(), resource_id.clone());
self.fonts.insert(name, font);
resource_id
}
pub fn register_from_file(
&mut self,
name: impl Into<String>,
path: impl AsRef<std::path::Path>,
) -> Result<String, String> {
let font = EmbeddedFont::from_file(path)?;
Ok(self.register(name, font))
}
pub fn get(&self, name: &str) -> Option<&EmbeddedFont> {
self.fonts.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut EmbeddedFont> {
self.fonts.get_mut(name)
}
pub fn resource_id(&self, name: &str) -> Option<&str> {
self.resource_ids.get(name).map(|s| s.as_str())
}
pub fn fonts(&self) -> impl Iterator<Item = (&str, &EmbeddedFont)> {
self.fonts.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn fonts_with_ids(&self) -> impl Iterator<Item = (&str, &str, &EmbeddedFont)> {
self.fonts.iter().filter_map(|(name, font)| {
self.resource_ids
.get(name)
.map(|id| (name.as_str(), id.as_str(), font))
})
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_empty(&self) -> bool {
self.fonts.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_manager_creation() {
let manager = FontManager::new();
assert!(manager.get_font("Helvetica").is_some());
assert!(manager.get_font("Times-Roman").is_some());
assert!(manager.get_font("Courier").is_some());
}
#[test]
fn test_base14_fonts() {
let manager = FontManager::new();
assert!(manager.is_base14("Helvetica"));
assert!(manager.is_base14("Helvetica-Bold"));
assert!(manager.is_base14("Times-Roman"));
assert!(manager.is_base14("Courier"));
assert!(!manager.is_base14("Arial")); }
#[test]
fn test_text_width_calculation() {
let manager = FontManager::new();
let width = manager.text_width("Hello", "Helvetica", 12.0);
assert!(width > 0.0);
assert!(width < 100.0); }
#[test]
fn test_monospace_consistency() {
let manager = FontManager::new();
let w1 = manager.char_width('i', "Courier", 12.0);
let w2 = manager.char_width('m', "Courier", 12.0);
let w3 = manager.char_width('W', "Courier", 12.0);
assert!((w1 - w2).abs() < 0.001);
assert!((w2 - w3).abs() < 0.001);
}
#[test]
fn test_proportional_variance() {
let manager = FontManager::new();
let w_i = manager.char_width('i', "Helvetica", 12.0);
let w_w = manager.char_width('W', "Helvetica", 12.0);
assert!(w_i < w_w);
}
#[test]
fn test_font_selection() {
let manager = FontManager::new();
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Normal, false),
"Helvetica"
);
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Bold, false),
"Helvetica-Bold"
);
assert_eq!(
manager.select_font(FontFamily::Times, FontWeight::Normal, true),
"Times-Italic"
);
assert_eq!(
manager.select_font(FontFamily::Courier, FontWeight::Bold, true),
"Courier-BoldOblique"
);
}
#[test]
fn test_font_metrics() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
assert!(font.ascender > 0.0);
assert!(font.descender < 0.0);
assert!(font.cap_height > 0.0);
assert!(font.x_height > 0.0);
assert!(font.x_height < font.cap_height);
}
#[test]
fn test_line_height() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
let line_height = font.line_height(12.0);
assert!(line_height > 10.0);
assert!(line_height < 15.0);
let visual_line_height = line_height * font.line_spacing_factor();
assert!(visual_line_height > 12.0); }
#[test]
fn test_text_layout_wrap() {
let layout = TextLayout::new();
let text = "The quick brown fox jumps over the lazy dog";
let lines = layout.wrap_text(text, "Helvetica", 12.0, 100.0);
assert!(lines.len() > 1); for (line, width) in &lines {
assert!(!line.is_empty() || lines.len() == 1);
assert!(*width <= 100.0 || line.split_whitespace().count() == 1);
}
}
#[test]
fn test_text_bounds() {
let layout = TextLayout::new();
let text = "Hello World";
let (width, height) = layout.text_bounds(text, "Helvetica", 12.0, 1000.0);
assert!(width > 0.0);
assert!(height > 0.0);
}
#[test]
fn test_empty_text() {
let layout = TextLayout::new();
let lines = layout.wrap_text("", "Helvetica", 12.0, 100.0);
assert_eq!(lines.len(), 1);
assert!(lines[0].0.is_empty());
}
#[test]
fn test_font_manager_default() {
let manager = FontManager::default();
assert!(manager.get_font("Helvetica").is_some());
assert!(manager.get_font("Symbol").is_some());
assert!(manager.get_font("ZapfDingbats").is_some());
}
#[test]
fn test_all_base14_fonts_registered() {
let manager = FontManager::new();
let names = manager.font_names();
assert!(names.len() >= 14);
for name in &[
"Helvetica",
"Helvetica-Bold",
"Helvetica-Oblique",
"Helvetica-BoldOblique",
"Times-Roman",
"Times-Bold",
"Times-Italic",
"Times-BoldItalic",
"Courier",
"Courier-Bold",
"Courier-Oblique",
"Courier-BoldOblique",
"Symbol",
"ZapfDingbats",
] {
assert!(manager.get_font(name).is_some(), "Missing font: {}", name);
assert!(manager.is_base14(name), "Not base14: {}", name);
}
}
#[test]
fn test_get_font_or_default_existing() {
let manager = FontManager::new();
let font = manager.get_font_or_default("Times-Roman");
assert_eq!(font.name, "Times-Roman");
}
#[test]
fn test_get_font_or_default_fallback() {
let manager = FontManager::new();
let font = manager.get_font_or_default("NonExistentFont");
assert_eq!(font.name, "Helvetica");
}
#[test]
fn test_get_font_none() {
let manager = FontManager::new();
assert!(manager.get_font("FakeFont").is_none());
}
#[test]
fn test_next_font_resource_id() {
let mut manager = FontManager::new();
assert_eq!(manager.next_font_resource_id(), "F1");
assert_eq!(manager.next_font_resource_id(), "F2");
assert_eq!(manager.next_font_resource_id(), "F3");
}
#[test]
fn test_text_width_nonexistent_font_fallback() {
let manager = FontManager::new();
let width = manager.text_width("Hello", "FakeFont", 12.0);
let helvetica_width = manager.text_width("Hello", "Helvetica", 12.0);
assert!((width - helvetica_width).abs() < 0.001);
}
#[test]
fn test_char_width_nonexistent_font_fallback() {
let manager = FontManager::new();
let width = manager.char_width('A', "NonExistent", 12.0);
let helvetica_width = manager.char_width('A', "Helvetica", 12.0);
assert!((width - helvetica_width).abs() < 0.001);
}
#[test]
fn test_select_font_all_combinations() {
let manager = FontManager::new();
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Normal, false),
"Helvetica"
);
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Bold, false),
"Helvetica-Bold"
);
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Normal, true),
"Helvetica-Oblique"
);
assert_eq!(
manager.select_font(FontFamily::Helvetica, FontWeight::Bold, true),
"Helvetica-BoldOblique"
);
assert_eq!(
manager.select_font(FontFamily::Times, FontWeight::Normal, false),
"Times-Roman"
);
assert_eq!(manager.select_font(FontFamily::Times, FontWeight::Bold, false), "Times-Bold");
assert_eq!(
manager.select_font(FontFamily::Times, FontWeight::Normal, true),
"Times-Italic"
);
assert_eq!(
manager.select_font(FontFamily::Times, FontWeight::Bold, true),
"Times-BoldItalic"
);
assert_eq!(manager.select_font(FontFamily::Courier, FontWeight::Normal, false), "Courier");
assert_eq!(
manager.select_font(FontFamily::Courier, FontWeight::Bold, false),
"Courier-Bold"
);
assert_eq!(
manager.select_font(FontFamily::Courier, FontWeight::Normal, true),
"Courier-Oblique"
);
assert_eq!(
manager.select_font(FontFamily::Courier, FontWeight::Bold, true),
"Courier-BoldOblique"
);
assert_eq!(manager.select_font(FontFamily::Symbol, FontWeight::Normal, false), "Symbol");
assert_eq!(manager.select_font(FontFamily::Symbol, FontWeight::Bold, true), "Symbol");
assert_eq!(
manager.select_font(FontFamily::ZapfDingbats, FontWeight::Normal, false),
"ZapfDingbats"
);
assert_eq!(
manager.select_font(FontFamily::ZapfDingbats, FontWeight::Bold, true),
"ZapfDingbats"
);
}
#[test]
fn test_font_info_family_properties() {
let manager = FontManager::new();
let helv = manager.get_font("Helvetica").unwrap();
assert_eq!(helv.family, FontFamily::Helvetica);
assert_eq!(helv.weight, FontWeight::Normal);
assert!(!helv.italic);
assert!(helv.is_base14);
let helv_bold = manager.get_font("Helvetica-Bold").unwrap();
assert_eq!(helv_bold.weight, FontWeight::Bold);
assert!(!helv_bold.italic);
let helv_obl = manager.get_font("Helvetica-Oblique").unwrap();
assert_eq!(helv_obl.weight, FontWeight::Normal);
assert!(helv_obl.italic);
let helv_bo = manager.get_font("Helvetica-BoldOblique").unwrap();
assert_eq!(helv_bo.weight, FontWeight::Bold);
assert!(helv_bo.italic);
}
#[test]
fn test_times_font_properties() {
let manager = FontManager::new();
let tr = manager.get_font("Times-Roman").unwrap();
assert_eq!(tr.family, FontFamily::Times);
assert_eq!(tr.weight, FontWeight::Normal);
assert!(!tr.italic);
let tb = manager.get_font("Times-Bold").unwrap();
assert_eq!(tb.weight, FontWeight::Bold);
let ti = manager.get_font("Times-Italic").unwrap();
assert!(ti.italic);
let tbi = manager.get_font("Times-BoldItalic").unwrap();
assert_eq!(tbi.weight, FontWeight::Bold);
assert!(tbi.italic);
}
#[test]
fn test_courier_font_properties() {
let manager = FontManager::new();
let c = manager.get_font("Courier").unwrap();
assert_eq!(c.family, FontFamily::Courier);
let cb = manager.get_font("Courier-Bold").unwrap();
assert_eq!(cb.weight, FontWeight::Bold);
let co = manager.get_font("Courier-Oblique").unwrap();
assert!(co.italic);
let cbo = manager.get_font("Courier-BoldOblique").unwrap();
assert_eq!(cbo.weight, FontWeight::Bold);
assert!(cbo.italic);
}
#[test]
fn test_symbol_font_properties() {
let manager = FontManager::new();
let sym = manager.get_font("Symbol").unwrap();
assert_eq!(sym.family, FontFamily::Symbol);
assert!(sym.is_base14);
let zd = manager.get_font("ZapfDingbats").unwrap();
assert_eq!(zd.family, FontFamily::ZapfDingbats);
assert!(zd.is_base14);
}
#[test]
fn test_symbol_font_char_width() {
let manager = FontManager::new();
let sym = manager.get_font("Symbol").unwrap();
assert!((sym.char_width('A') - 500.0).abs() < 0.001);
assert!((sym.char_width('z') - 500.0).abs() < 0.001);
}
#[test]
fn test_courier_font_metrics() {
let manager = FontManager::new();
let courier = manager.get_font("Courier").unwrap();
assert_eq!(courier.ascender, 629.0);
assert_eq!(courier.descender, -157.0);
let courier_bold = manager.get_font("Courier-Bold").unwrap();
assert_eq!(courier_bold.ascender, 626.0);
}
#[test]
fn test_helvetica_bold_metrics() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica-Bold").unwrap();
assert_eq!(font.ascender, 718.0);
assert_eq!(font.descender, -207.0);
assert_eq!(font.x_height, 532.0);
}
#[test]
fn test_times_metrics() {
let manager = FontManager::new();
let font = manager.get_font("Times-Roman").unwrap();
assert_eq!(font.ascender, 683.0);
assert_eq!(font.descender, -217.0);
let bold = manager.get_font("Times-Bold").unwrap();
assert_eq!(bold.ascender, 676.0);
}
#[test]
fn test_font_info_line_spacing_factor() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
assert!((font.line_spacing_factor() - 1.2).abs() < 0.001);
}
#[test]
fn test_font_info_text_width_empty() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
assert!((font.text_width("", 12.0)).abs() < 0.001);
}
#[test]
fn test_font_info_char_width_unknown_char() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
let width = font.char_width('\u{FFFF}');
assert!((width - 500.0).abs() < 0.001);
}
#[test]
fn test_font_widths_proportional_known_chars() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
let space_w = font.char_width(' ');
assert!((space_w - 278.0).abs() < 0.001);
let a_w = font.char_width('A');
assert!((a_w - 722.0).abs() < 0.001);
let a_lower_w = font.char_width('a');
assert!((a_lower_w - 556.0).abs() < 0.001);
}
#[test]
fn test_font_widths_times_chars() {
let manager = FontManager::new();
let font = manager.get_font("Times-Roman").unwrap();
let space_w = font.char_width(' ');
assert!((space_w - 250.0).abs() < 0.001);
let i_w = font.char_width('I');
assert!((i_w - 333.0).abs() < 0.001);
}
#[test]
fn test_font_widths_times_bold_chars() {
let manager = FontManager::new();
let font = manager.get_font("Times-Bold").unwrap();
let w_w = font.char_width('W');
assert!((w_w - 1000.0).abs() < 0.001);
}
#[test]
fn test_helvetica_bold_widths() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica-Bold").unwrap();
let f_w = font.char_width('f');
assert!((f_w - 333.0).abs() < 0.001);
}
#[test]
fn test_digit_widths() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
for digit in '0'..='9' {
let w = font.char_width(digit);
assert!((w - 556.0).abs() < 0.001, "Digit {} has width {}", digit, w);
}
}
#[test]
fn test_punctuation_widths() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
assert!(font.char_width('@') > 700.0); assert!(font.char_width('!') > 200.0);
assert!(font.char_width('.') > 200.0);
}
#[test]
fn test_text_width_scaling() {
let manager = FontManager::new();
let w12 = manager.text_width("Hello", "Helvetica", 12.0);
let w24 = manager.text_width("Hello", "Helvetica", 24.0);
assert!((w24 - 2.0 * w12).abs() < 0.01);
}
#[test]
fn test_line_height_scaling() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica").unwrap();
let lh12 = font.line_height(12.0);
let lh24 = font.line_height(24.0);
assert!((lh24 - 2.0 * lh12).abs() < 0.01);
}
#[test]
fn test_text_layout_default() {
let layout = TextLayout::default();
let lines = layout.wrap_text("Hello", "Helvetica", 12.0, 1000.0);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].0, "Hello");
}
#[test]
fn test_text_layout_with_font_manager() {
let fm = FontManager::new();
let layout = TextLayout::with_font_manager(fm);
let lines = layout.wrap_text("Test", "Helvetica", 12.0, 1000.0);
assert_eq!(lines.len(), 1);
}
#[test]
fn test_text_layout_font_manager_accessor() {
let layout = TextLayout::new();
let fm = layout.font_manager();
assert!(fm.get_font("Helvetica").is_some());
}
#[test]
fn test_text_layout_single_long_word() {
let layout = TextLayout::new();
let lines = layout.wrap_text("Superlongword", "Helvetica", 12.0, 10.0);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].0, "Superlongword");
}
#[test]
fn test_text_layout_wrap_exact_fit() {
let layout = TextLayout::new();
let lines = layout.wrap_text("A B", "Helvetica", 12.0, 1000.0);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].0, "A B");
}
#[test]
fn test_text_bounds_multiline() {
let layout = TextLayout::new();
let (width, height) = layout.text_bounds("Hello World", "Helvetica", 12.0, 30.0);
assert!(width > 0.0);
assert!(height > 0.0);
}
#[test]
fn test_text_bounds_empty() {
let layout = TextLayout::new();
let (_width, height) = layout.text_bounds("", "Helvetica", 12.0, 100.0);
assert!(height > 0.0);
}
#[test]
fn test_font_weight_default() {
let weight = FontWeight::default();
assert_eq!(weight, FontWeight::Normal);
}
#[test]
fn test_embedded_font_manager_new() {
let mgr = EmbeddedFontManager::new();
assert!(mgr.is_empty());
assert_eq!(mgr.len(), 0);
}
#[test]
fn test_embedded_font_manager_default() {
let mgr = EmbeddedFontManager::default();
assert!(mgr.is_empty());
assert_eq!(mgr.len(), 0);
}
#[test]
fn test_base14_metrics_default_branch() {
let metrics = base14_metrics("Unknown");
assert_eq!(metrics.0, 750.0);
assert_eq!(metrics.1, -250.0);
}
#[test]
fn test_font_widths_for_base14_symbol() {
let widths = FontWidths::for_base14("Symbol");
assert!((widths.width_for_char('A') - 500.0).abs() < 0.001);
}
#[test]
fn test_font_widths_for_base14_unknown() {
let widths = FontWidths::for_base14("UnknownFont");
let w = widths.width_for_char('A');
assert!(w > 0.0);
}
#[test]
fn test_courier_monospace_all_chars_same() {
let manager = FontManager::new();
let font = manager.get_font("Courier").unwrap();
let width_a = font.char_width('A');
let width_z = font.char_width('z');
let width_at = font.char_width('@');
assert!((width_a - 600.0).abs() < 0.001);
assert!((width_z - 600.0).abs() < 0.001);
assert!((width_at - 600.0).abs() < 0.001);
}
#[test]
fn test_courier_bold_monospace() {
let manager = FontManager::new();
let font = manager.get_font("Courier-Bold").unwrap();
assert!((font.char_width('A') - 600.0).abs() < 0.001);
}
#[test]
fn test_courier_oblique_monospace() {
let manager = FontManager::new();
let font = manager.get_font("Courier-Oblique").unwrap();
assert!((font.char_width('X') - 600.0).abs() < 0.001);
}
#[test]
fn test_courier_boldoblique_monospace() {
let manager = FontManager::new();
let font = manager.get_font("Courier-BoldOblique").unwrap();
assert!((font.char_width('M') - 600.0).abs() < 0.001);
}
#[test]
fn test_helvetica_oblique_widths() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica-Oblique").unwrap();
let a_w = font.char_width('A');
assert!((a_w - 722.0).abs() < 0.001);
}
#[test]
fn test_times_italic_widths() {
let manager = FontManager::new();
let font = manager.get_font("Times-Italic").unwrap();
let space_w = font.char_width(' ');
assert!((space_w - 250.0).abs() < 0.001);
}
#[test]
fn test_times_bolditalic_widths() {
let manager = FontManager::new();
let font = manager.get_font("Times-BoldItalic").unwrap();
let a_w = font.char_width('a');
assert!((a_w - 500.0).abs() < 0.001);
}
#[test]
fn test_helvetica_boldoblique_widths() {
let manager = FontManager::new();
let font = manager.get_font("Helvetica-BoldOblique").unwrap();
let w = font.char_width(':');
assert!((w - 333.0).abs() < 0.001);
}
}