use crate::page_template::PageTemplate;
use crate::units::Pt;
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub resource_limits: ResourceLimits,
pub missing_glyph_policy: MissingGlyphPolicy,
pub deterministic_mode: bool,
pub page_width: Pt,
pub page_height: Pt,
pub dpi_downsampling_threshold: f64,
pub page_template: Option<PageTemplate>,
pub hyphenation: HyphenationConfig,
pub page_sections: Vec<PageSectionConfig>,
pub footnote: crate::node::FootnoteConfig,
pub toc: TocConfig,
pub metadata: PdfMetadata,
pub output_intent: Option<OutputIntentConfig>,
}
#[derive(Debug, Clone, Default)]
pub struct PdfMetadata {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub creator: Option<String>,
pub producer: Option<String>,
pub custom: Vec<(String, String)>,
pub creation_date: Option<(u16, u8, u8)>,
pub modification_date: Option<(u16, u8, u8)>,
}
#[derive(Debug, Clone)]
pub struct OutputIntentConfig {
pub condition_identifier: String,
pub condition: String,
pub registry_name: String,
pub icc_profile: Option<Vec<u8>>,
}
impl OutputIntentConfig {
#[must_use]
pub fn srgb() -> Self {
Self {
condition_identifier: "sRGB IEC61966-2.1".to_string(),
condition: "sRGB".to_string(),
registry_name: "http://www.color.org".to_string(),
icc_profile: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TocConfig {
pub enabled: bool,
pub max_level: u8,
pub dot_leaders: bool,
pub title: Option<String>,
pub indent_per_level: Pt,
pub entry_width: Option<Pt>,
}
impl Default for TocConfig {
fn default() -> Self {
Self {
enabled: false,
max_level: 3,
dot_leaders: true,
title: Some("Contents".to_string()),
indent_per_level: Pt::new(15.0),
entry_width: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PageSectionConfig {
pub start_element_id: String,
pub width: Pt,
pub height: Pt,
pub margins: Option<crate::page_template::PageMargins>,
pub header: Option<crate::tree::StyledTree>,
pub footer: Option<crate::tree::StyledTree>,
pub page_number_style: Option<PageNumberStyle>,
pub restart_numbering: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PageNumberStyle {
#[default]
Arabic,
RomanLower,
RomanUpper,
AlphaLower,
None,
}
impl PageNumberStyle {
#[must_use]
pub fn format(&self, n: u32) -> String {
match self {
Self::Arabic => n.to_string(),
Self::RomanLower => to_roman(n).to_lowercase(),
Self::RomanUpper => to_roman(n),
Self::AlphaLower => format_alpha_lower(n),
Self::None => String::new(),
}
}
}
fn to_roman(mut n: u32) -> String {
const TABLE: [(u32, &str); 13] = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
if n == 0 {
return "0".to_string();
}
let mut result = String::new();
for &(value, symbol) in &TABLE {
while n >= value {
result.push_str(symbol);
n -= value;
}
}
result
}
fn format_alpha_lower(n: u32) -> String {
if n == 0 {
return String::new();
}
let mut result = String::new();
let mut val = n - 1;
loop {
result.insert(0, (b'a' + (val % 26) as u8) as char);
if val < 26 {
break;
}
val = val / 26 - 1;
}
result
}
#[derive(Debug, Clone)]
pub struct HyphenationConfig {
pub enabled: bool,
pub min_word_length: u32,
pub min_left: u32,
pub min_right: u32,
}
impl Default for HyphenationConfig {
fn default() -> Self {
Self {
enabled: false,
min_word_length: 5,
min_left: 2,
min_right: 2,
}
}
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
resource_limits: ResourceLimits::default(),
missing_glyph_policy: MissingGlyphPolicy::default(),
deterministic_mode: false,
page_width: Pt::new(595.276), page_height: Pt::new(841.890), dpi_downsampling_threshold: 300.0,
page_template: None,
hyphenation: HyphenationConfig::default(),
page_sections: Vec::new(),
footnote: crate::node::FootnoteConfig::default(),
toc: TocConfig::default(),
metadata: PdfMetadata::default(),
output_intent: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_node_count: u32,
pub max_tree_depth: u32,
pub max_text_bytes: u64,
pub max_svg_path_count: u32,
pub max_pages: u32,
pub page_time_budget_ms: u64,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_node_count: 1_000_000,
max_tree_depth: 256,
max_text_bytes: 100 * 1024 * 1024, max_svg_path_count: 50_000,
max_pages: 10_000,
page_time_budget_ms: 500,
}
}
}
impl ResourceLimits {
#[must_use]
pub fn unlimited() -> Self {
Self {
max_node_count: u32::MAX,
max_tree_depth: u32::MAX,
max_text_bytes: u64::MAX,
max_svg_path_count: u32::MAX,
max_pages: u32::MAX,
page_time_budget_ms: u64::MAX,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MissingGlyphPolicy {
#[default]
Fallback,
Fail,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_limits_are_sane() {
let limits = ResourceLimits::default();
assert_eq!(limits.max_node_count, 1_000_000);
assert_eq!(limits.max_tree_depth, 256);
assert_eq!(limits.max_text_bytes, 100 * 1024 * 1024);
assert_eq!(limits.max_svg_path_count, 50_000);
assert_eq!(limits.max_pages, 10_000);
assert_eq!(limits.page_time_budget_ms, 500);
}
#[test]
fn unlimited_is_max() {
let limits = ResourceLimits::unlimited();
assert_eq!(limits.max_node_count, u32::MAX);
assert_eq!(limits.max_text_bytes, u64::MAX);
}
#[test]
fn default_config() {
let config = RenderConfig::default();
assert_eq!(config.missing_glyph_policy, MissingGlyphPolicy::Fallback);
assert!(!config.deterministic_mode);
assert!((config.dpi_downsampling_threshold - 300.0).abs() < f64::EPSILON);
}
#[test]
fn missing_glyph_policy_default_is_fallback() {
assert_eq!(MissingGlyphPolicy::default(), MissingGlyphPolicy::Fallback);
}
}