1mod cli;
7mod converter;
8mod preprocess;
9mod tex_engine;
10mod tex_format;
11mod validation;
12
13pub use cli::Args;
14pub use tex_engine::TexEngine;
15pub use tex_format::TexFormat;
16
17use converter::PdfConverter;
18use preprocess::preprocess_svg;
19use std::fs::File;
20use std::io::Write;
21use usvg::{Options, Tree};
22use validation::{analyze_tree, TextFontRequirement};
23
24pub fn render_output(args: &Args) -> Result<String, String> {
28 if !args.fallback_dpi.is_finite() || args.fallback_dpi <= 0.0 {
29 return Err(format!(
30 "Invalid --fallback-dpi value '{}': expected a positive number.",
31 args.fallback_dpi
32 ));
33 }
34
35 if let Some(font_size) = args.font_size {
36 if !font_size.is_finite() || font_size <= 0.0 {
37 return Err(format!(
38 "Invalid --font-size value '{}': expected a positive number.",
39 font_size
40 ));
41 }
42 }
43
44 let mut opt = Options::default();
45 if let Some(font_family) = &args.font_family {
46 opt.font_family = font_family.clone();
47 }
48 if let Some(font_size) = args.font_size {
49 opt.font_size = font_size;
50 }
51 if let Some(family) = &args.serif_family {
52 opt.fontdb_mut().set_serif_family(family.clone());
53 }
54 if let Some(family) = &args.sans_serif_family {
55 opt.fontdb_mut().set_sans_serif_family(family.clone());
56 }
57 if let Some(family) = &args.cursive_family {
58 opt.fontdb_mut().set_cursive_family(family.clone());
59 }
60 if let Some(family) = &args.fantasy_family {
61 opt.fontdb_mut().set_fantasy_family(family.clone());
62 }
63 if let Some(family) = &args.monospace_family {
64 opt.fontdb_mut().set_monospace_family(family.clone());
65 }
66 if !args.no_system_fonts {
67 opt.fontdb_mut().load_system_fonts();
68 }
69
70 for font_file in &args.font_files {
71 opt.fontdb_mut()
72 .load_font_file(font_file)
73 .map_err(|e| format!("Error loading font file '{}': {}", font_file.display(), e))?;
74 }
75
76 for font_dir in &args.font_dirs {
77 opt.fontdb_mut().load_fonts_dir(font_dir);
78 }
79
80 let svg_data = std::fs::read(&args.input)
81 .map_err(|e| format!("Error reading input file '{}': {}", args.input, e))?;
82 let svg_data = preprocess_svg(&svg_data);
83
84 let tree = Tree::from_data(&svg_data, &opt).map_err(|e| format!("Error parsing SVG: {}", e))?;
85 let analysis = analyze_tree(&tree, args.embed_images);
86
87 if analysis.has_text_nodes {
88 if args.report_fonts {
89 report_text_fonts(&analysis.text_font_requirements);
90 }
91
92 if !args.no_system_fonts {
93 let requested = analysis
94 .text_font_requirements
95 .iter()
96 .map(TextFontRequirement::summary)
97 .collect::<Vec<_>>();
98 eprintln!(
99 "Warning: Text conversion currently depends on system fonts. Requested text fonts: {}. \
100Use --no-system-fonts together with --font-file/--font-dir and explicit default font families for reproducible output.",
101 if requested.is_empty() {
102 "(none)".to_string()
103 } else {
104 requested.join("; ")
105 }
106 );
107 }
108
109 let missing_named_families = analysis.missing_named_font_families(tree.fontdb());
110 if args.strict_fonts {
111 if !args.no_system_fonts {
112 return Err(
113 "--strict-fonts requires --no-system-fonts so text rendering does not depend on host font discovery."
114 .to_string(),
115 );
116 }
117
118 if !missing_named_families.is_empty() {
119 return Err(format!(
120 "Missing named fonts required by SVG text: {}. Load them with --font-file/--font-dir or change the default font families.",
121 missing_named_families.join(", ")
122 ));
123 }
124 } else if !missing_named_families.is_empty() {
125 eprintln!(
126 "Warning: Some named fonts requested by SVG text were not found in the loaded font database: {}. Fallback shaping may change layout.",
127 missing_named_families.join(", ")
128 );
129 }
130 }
131
132 let size = tree.size();
133 let mut converter = PdfConverter::new(
134 size,
135 args.embed_images,
136 args.fallback_dpi,
137 args.engine,
138 args.tex_format,
139 );
140 if !analysis.unsupported_features.is_empty() && args.strict {
143 return Err(format!(
144 "Unsupported SVG features detected: {}. Re-run without --strict to rasterize only the unsupported subtrees.",
145 analysis.unsupported_features.join(", ")
146 ));
147 }
148
149 if !analysis.unsupported_features.is_empty() {
150 eprintln!(
151 "Info: Hybrid rendering enabled for unsupported SVG features: {}",
152 analysis.unsupported_features.join(", ")
153 );
154 }
155
156 match converter.convert(&tree) {
157 Ok(()) => {}
158 Err(err) if !analysis.unsupported_features.is_empty() => {
159 eprintln!(
160 "Info: Hybrid rendering failed ({}); falling back to full-document rasterization.",
161 err
162 );
163 converter.rasterize_tree(&tree)?;
164 }
165 Err(err) => return Err(err),
166 }
167
168 Ok(if args.tex {
169 converter.generate_latex()
170 } else {
171 converter.generate_pdf_literal()
172 })
173}
174
175pub fn run(args: Args) -> Result<(), String> {
177 let output_content = render_output(&args)?;
178
179 if let Some(output_path) = args.output {
180 let mut file = File::create(&output_path)
181 .map_err(|e| format!("Error creating output file '{}': {}", output_path, e))?;
182
183 file.write_all(output_content.as_bytes())
184 .map_err(|e| format!("Error writing to output file: {}", e))?;
185
186 eprintln!("Output written to: {}", output_path);
187 } else {
188 print!("{}", output_content);
189 }
190
191 Ok(())
192}
193
194fn report_text_fonts(requirements: &[TextFontRequirement]) {
195 if requirements.is_empty() {
196 eprintln!("Info: No text font requirements were detected.");
197 return;
198 }
199
200 eprintln!("Text font requirements:");
201 for requirement in requirements {
202 eprintln!(" - {}", requirement.summary());
203 }
204}