1use std::{ffi::OsStr, fs, io::BufRead, path::Path};
4
5use ignore::Walk;
6use printpdf::{
7 FontId, Op, PdfDocument, PdfPage, Px, RawImage, TextItem, XObjectRotation, XObjectTransform,
8 color,
9};
10use syntect::{
11 easy::HighlightFile,
12 highlighting::{Color, Style, Theme},
13 parsing::SyntaxSet,
14};
15
16use crate::{dimensions::Dimensions, helpers::init_page, text_manipulation::TextWrapper};
17
18pub struct HighlighterConfig {
22 syntax_set: SyntaxSet,
23 theme: Theme,
24 max_line_len_to_highlight: usize,
25}
26impl HighlighterConfig {
27 pub fn new(syntax_set: SyntaxSet, theme: Theme) -> Self {
29 Self {
30 syntax_set,
31 theme,
32 max_line_len_to_highlight: 20_000,
33 }
34 }
35}
36
37pub struct CodeToPdf {
41 current_page_contents: Vec<Op>,
42 doc: PdfDocument,
43 font_id: FontId,
44 page_dimensions: Dimensions,
45 text_wrapper: TextWrapper,
46 processed_file_count: usize,
47}
48impl CodeToPdf {
49 pub fn new(
51 doc: PdfDocument,
52 font_id: FontId,
53 page_dimensions: Dimensions,
54 text_wrapper: TextWrapper,
55 ) -> Self {
56 Self {
57 current_page_contents: vec![],
58 doc,
59 font_id,
60 page_dimensions,
61 text_wrapper,
62 processed_file_count: 0,
63 }
64 }
65 fn save_page(&mut self) {
67 let contents = std::mem::take(&mut self.current_page_contents);
68 let page = PdfPage::new(
69 self.page_dimensions.width,
70 self.page_dimensions.height,
71 contents,
72 );
73 self.doc.pages.push(page);
74 }
75
76 fn init_page(&mut self, path: &Path) {
78 debug_assert_eq!(self.current_page_contents.len(), 0);
80
81 init_page(
82 &mut self.current_page_contents,
83 &self.page_dimensions,
84 self.font_id.clone(),
85 self.text_wrapper.font_size(),
86 path,
87 &mut self.text_wrapper,
88 );
89 }
90 fn max_line_count(&self) -> u32 {
92 let max_height = self.page_dimensions.max_text_height();
93 ((max_height).into_pt().0 / (self.text_wrapper.font_size() * 1.2)).floor() as u32
94 }
95 fn increment_line_count(&mut self, line_count: &mut u32, path: &Path) -> bool {
98 *line_count += 1;
99 if *line_count > self.max_line_count() {
100 self.save_page();
101 self.init_page(path);
102 *line_count = 0;
103 true
104 } else {
105 false
106 }
107 }
108 fn generate_highlighted_pages(
110 &mut self,
111 highlighter: &mut HighlightFile,
112 path: &Path,
113 highlighter_config: &HighlighterConfig,
114 ) {
115 let mut line = String::new();
116 let mut line_count = 0;
117 self.init_page(path);
118 let mut has_added_text = false;
119 while highlighter.reader.read_line(&mut line).unwrap_or(0) > 0 {
120 has_added_text = true;
121 let mut line_width = 0.0;
123 let regions: Vec<(Style, &str)> =
124 if line.len() < highlighter_config.max_line_len_to_highlight {
125 highlighter
126 .highlight_lines
127 .highlight_line(&line, &highlighter_config.syntax_set)
128 .unwrap()
129 } else {
130 vec![(
131 Style {
132 foreground: Color::BLACK,
133 background: Color::WHITE,
134 font_style: syntect::highlighting::FontStyle::default(),
135 },
136 &line,
137 )]
138 };
139 for (style, text) in regions {
140 line_width += self.text_wrapper.get_width(text);
141 if line_width > self.page_dimensions.max_text_width().into_pt().0 {
143 self.increment_line_count(&mut line_count, path);
144 self.current_page_contents.push(Op::AddLineBreak);
145 line_width = 0.0;
146 }
147 let text_style = style.foreground;
148 self.current_page_contents.push(Op::SetFillColor {
150 col: color::Color::Rgb(color::Rgb {
151 r: (text_style.r as f32) / 255.0,
152 g: (text_style.g as f32) / 255.0,
153 b: (text_style.b as f32) / 255.0,
154 icc_profile: None,
155 }),
156 });
157 let lines = self
158 .text_wrapper
159 .split_into_lines(text, self.page_dimensions.max_text_width());
160 match lines.len() {
162 1 => {
163 self.current_page_contents.push(Op::WriteText {
164 items: vec![TextItem::Text(text.to_owned())],
165 font: self.font_id.clone(),
166 });
167 }
168 _ => {
170 let mut first = true;
171 for l in lines {
172 if !first {
173 self.current_page_contents.push(Op::AddLineBreak);
174 }
175 first = false;
176 self.current_page_contents.push(Op::WriteText {
177 items: vec![TextItem::Text(l)],
178 font: self.font_id.clone(),
179 });
180 self.increment_line_count(&mut line_count, path);
181 }
182 }
183 }
184 }
185
186 if !self.increment_line_count(&mut line_count, path) {
189 self.current_page_contents.push(Op::AddLineBreak);
190 }
191 line.clear();
192 }
193 if has_added_text {
195 self.save_page();
196 } else {
197 self.current_page_contents.clear()
198 }
199 }
200
201 fn generate_image_page(&mut self, path: &Path) {
203 let bytes = if let Ok(b) = fs::read(path) {
204 b
205 } else {
206 return;
207 };
208 let image = if let Ok(img) = RawImage::decode_from_bytes(&bytes, &mut vec![]) {
209 img
210 } else {
211 return;
212 };
213 self.init_page(path);
214 let image_id = self.doc.add_image(&image);
215 let pg_x_dpi = self.page_dimensions.width.into_pt().into_px(300.0).0;
216 let pg_y_dpi = self.page_dimensions.height.into_pt().into_px(300.0).0;
217
218 let x_scaling = pg_x_dpi as f32 / image.width as f32;
219 let y_scaling = pg_y_dpi as f32 / image.height as f32;
220
221 let scale = f32::min(x_scaling, y_scaling);
222 let rotation = if image.width > (image.height as f32 * 1.25) as usize {
224 Some(XObjectRotation {
225 angle_ccw_degrees: -90.0,
226 rotation_center_x: Px(((image.width as f32 * scale) / 2.0) as usize),
227 rotation_center_y: Px(((image.height as f32 * scale) / 2.0) as usize),
228 })
229 } else {
230 None
231 };
232 self.current_page_contents.push(Op::UseXobject {
233 id: image_id.clone(),
234 transform: XObjectTransform {
235 scale_x: Some(scale),
236 scale_y: Some(scale),
237 rotate: rotation,
238 ..Default::default()
239 },
240 });
241 self.save_page();
242 }
243 pub fn process_file(
245 &mut self,
246 file: &Path,
247 highlighter_config: &HighlighterConfig,
248 ) -> Result<(), Box<dyn std::error::Error>> {
249 println!("Generating pages for {}", file.display());
250 self.processed_file_count += 1;
251 match file.extension().and_then(OsStr::to_str) {
252 Some("jpg" | "jpeg" | "png" | "ico" | "bmp" | "webp") => {
253 self.generate_image_page(file);
254 Ok(())
255 }
256 _ => {
257 let mut highlighter = HighlightFile::new(
258 file,
259 &highlighter_config.syntax_set,
260 &highlighter_config.theme,
261 )?;
262
263 self.generate_highlighted_pages(&mut highlighter, file, highlighter_config);
264
265 Ok(())
266 }
267 }
268 }
269 pub fn process_files(&mut self, walker: Walk, highlighter_config: HighlighterConfig) {
271 dbg!(self.max_line_count());
272 for result in walker {
273 match result {
274 Ok(entry) => {
275 if entry.file_type().is_some_and(|f| f.is_file()) {
276 if let Err(err) = self.process_file(entry.path(), &highlighter_config) {
277 println!("ERROR: {}", err)
278 }
279 }
280 }
281 Err(err) => println!("ERROR: {}", err),
282 }
283 }
284 }
285
286 pub fn document(self) -> PdfDocument {
288 self.doc
289 }
290
291 pub fn processed_file_count(&self) -> usize {
293 self.processed_file_count
294 }
295}