1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/// Supported input document formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub enum Format {
Docx,
Pptx,
Xlsx,
}
impl Format {
/// Detect format from file extension.
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_ascii_lowercase().as_str() {
"docx" => Some(Self::Docx),
"pptx" => Some(Self::Pptx),
"xlsx" => Some(Self::Xlsx),
_ => None,
}
}
}
/// A range of slide numbers (1-indexed) for PPTX conversion.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SlideRange {
/// Start slide number (1-indexed, inclusive).
pub start: u32,
/// End slide number (1-indexed, inclusive).
pub end: u32,
}
impl SlideRange {
/// Create a new slide range (1-indexed, inclusive on both ends).
pub fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
/// Check if a 1-indexed slide number is within this range.
pub fn contains(&self, slide_number: u32) -> bool {
slide_number >= self.start && slide_number <= self.end
}
/// Parse a slide range string like "1-5" or "3".
pub fn parse(s: &str) -> Result<Self, String> {
if let Some((start_str, end_str)) = s.split_once('-') {
let start: u32 = start_str
.trim()
.parse()
.map_err(|_| format!("invalid start number: {start_str}"))?;
let end: u32 = end_str
.trim()
.parse()
.map_err(|_| format!("invalid end number: {end_str}"))?;
if start == 0 || end == 0 {
return Err("slide numbers must be >= 1".to_string());
}
if start > end {
return Err(format!("start ({start}) must be <= end ({end})"));
}
Ok(Self::new(start, end))
} else {
let n: u32 = s
.trim()
.parse()
.map_err(|_| format!("invalid slide number: {s}"))?;
if n == 0 {
return Err("slide number must be >= 1".to_string());
}
Ok(Self::new(n, n))
}
}
}
/// PDF standard to enforce compliance with.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub enum PdfStandard {
/// PDF/A-2b for archival purposes.
PdfA2b,
}
/// Paper size for output PDF.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub enum PaperSize {
/// A4: 595.28pt × 841.89pt (210mm × 297mm).
A4,
/// US Letter: 612pt × 792pt (8.5in × 11in).
Letter,
/// US Legal: 612pt × 1008pt (8.5in × 14in).
Legal,
/// Custom dimensions in points.
Custom { width: f64, height: f64 },
}
impl PaperSize {
/// Returns (width, height) in points.
pub fn dimensions(&self) -> (f64, f64) {
use crate::defaults;
match self {
Self::A4 => (defaults::A4_WIDTH_PT, defaults::A4_HEIGHT_PT),
Self::Letter => (defaults::LETTER_WIDTH_PT, defaults::LETTER_HEIGHT_PT),
Self::Legal => (defaults::LEGAL_WIDTH_PT, defaults::LEGAL_HEIGHT_PT),
Self::Custom { width, height } => (*width, *height),
}
}
/// Parse a paper size string (case-insensitive): "a4", "letter", "legal".
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"a4" => Ok(Self::A4),
"letter" => Ok(Self::Letter),
"legal" => Ok(Self::Legal),
_ => Err(format!(
"unknown paper size: {s}; expected one of: a4, letter, legal"
)),
}
}
}
/// Options controlling the conversion process.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct ConvertOptions {
/// Filter XLSX sheets by name. Only sheets whose names are in this list
/// will be included. If `None`, all sheets are included.
pub sheet_names: Option<Vec<String>>,
/// Filter PPTX slides by range (1-indexed). If `None`, all slides are included.
pub slide_range: Option<SlideRange>,
/// PDF standard to enforce. If `None`, produces a standard PDF 1.7.
pub pdf_standard: Option<PdfStandard>,
/// Override paper size for the output PDF. If `None`, uses the source document's size.
pub paper_size: Option<PaperSize>,
/// Additional font directories to search for fonts.
#[cfg_attr(feature = "typescript", ts(type = "Array<string>"))]
pub font_paths: Vec<std::path::PathBuf>,
/// Force landscape orientation. If `Some(true)`, swaps width/height so width > height.
/// If `Some(false)`, forces portrait. If `None`, uses source document orientation.
pub landscape: Option<bool>,
/// Enable tagged PDF output with document structure tags (H1-H6, P, Table, Figure).
/// When `true`, the output PDF includes accessibility tags that map document
/// structure for screen readers and assistive technologies.
pub tagged: bool,
/// Enable PDF/UA (Universal Accessibility) compliance. Implies `tagged: true`.
/// Combines tagged PDF with the PDF/UA-1 standard for full accessibility compliance.
pub pdf_ua: bool,
/// Enable streaming mode for large file processing.
/// In streaming mode, XLSX files are processed in chunks of rows to bound memory usage.
/// Each chunk is compiled independently and the resulting PDFs are merged.
/// Requires the `pdf-ops` feature for PDF merging.
pub streaming: bool,
/// Chunk size (in rows) for streaming mode. Defaults to 1000 if `None`.
/// Only used when `streaming` is `true`.
pub streaming_chunk_size: Option<usize>,
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;
#[cfg(all(test, feature = "typescript"))]
#[path = "config_ts_tests.rs"]
mod ts_tests;