1use std::collections::HashMap;
8
9use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref, Str};
10use ratex_font::FontId;
11use ratex_types::color::Color;
12use ratex_types::display_item::{DisplayItem, DisplayList};
13use ratex_types::path_command::PathCommand;
14
15use crate::fonts::{self, EmbeddedFont};
16
17#[derive(Debug, Clone)]
24pub struct PdfOptions {
25 pub font_size: f64,
27 pub padding: f64,
29 pub stroke_width: f64,
31 pub font_dir: String,
34}
35
36impl Default for PdfOptions {
37 fn default() -> Self {
40 Self {
41 font_size: 40.0,
42 padding: 10.0,
43 stroke_width: 1.5,
44 font_dir: String::new(),
45 }
46 }
47}
48
49#[derive(Debug)]
51pub enum PdfError {
52 Font(String),
53 Render(String),
54}
55
56impl std::fmt::Display for PdfError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 PdfError::Font(s) => write!(f, "Font error: {s}"),
60 PdfError::Render(s) => write!(f, "Render error: {s}"),
61 }
62 }
63}
64
65impl std::error::Error for PdfError {}
66
67pub fn render_to_pdf(
69 display_list: &DisplayList,
70 options: &PdfOptions,
71) -> Result<Vec<u8>, PdfError> {
72 let em = options.font_size;
73 let pad = options.padding;
74 let sw = options.stroke_width;
75
76 let total_h = display_list.height + display_list.depth;
77 let page_w = display_list.width * em + 2.0 * pad;
78 let page_h = total_h * em + 2.0 * pad;
79
80 let font_data = fonts::load_all_fonts(&options.font_dir).map_err(PdfError::Font)?;
82
83 let usages = fonts::collect_glyph_usage(&display_list.items, &font_data);
85
86 let mut pdf = Pdf::new();
88 let mut alloc = Ref::new(1);
89
90 let catalog_ref = alloc.bump();
91 let pages_ref = alloc.bump();
92 let page_ref = alloc.bump();
93 let content_ref = alloc.bump();
94
95 let embedded = fonts::embed_fonts(&mut pdf, &mut alloc, &usages, &font_data)
97 .map_err(PdfError::Font)?;
98
99
100 let font_index: HashMap<FontId, usize> = embedded
102 .iter()
103 .enumerate()
104 .map(|(i, ef)| (ef.font_id, i))
105 .collect();
106
107 let content_bytes = build_content_stream(
109 &display_list.items,
110 &embedded,
111 &font_index,
112 &font_data,
113 em,
114 pad,
115 page_h,
116 sw,
117 );
118
119 let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&content_bytes, 6);
121
122 let mut stream = pdf.stream(content_ref, &compressed);
124 stream.filter(Filter::FlateDecode);
125 stream.pair(Name(b"Length1"), content_bytes.len() as i32);
126 stream.finish();
127
128 let mut page = pdf.page(page_ref);
130 page.parent(pages_ref);
131 page.media_box(Rect::new(0.0, 0.0, page_w as f32, page_h as f32));
132 page.contents(content_ref);
133
134 let mut resources = page.resources();
136 let mut font_dict = resources.fonts();
137 for ef in &embedded {
138 font_dict.pair(Name(ef.res_name.as_bytes()), ef.type0_ref);
139 }
140 font_dict.finish();
141 resources.finish();
142 page.finish();
143
144 let mut pages = pdf.pages(pages_ref);
146 pages.count(1);
147 pages.kids([page_ref]);
148 pages.finish();
149
150 pdf.catalog(catalog_ref).pages(pages_ref);
152
153 Ok(pdf.finish())
154}
155
156#[allow(clippy::too_many_arguments)]
161fn build_content_stream(
162 items: &[DisplayItem],
163 embedded: &[EmbeddedFont],
164 font_index: &HashMap<FontId, usize>,
165 font_data: &fonts::RawFontData,
166 em: f64,
167 pad: f64,
168 page_h: f64,
169 stroke_width: f64,
170) -> Vec<u8> {
171 let mut content = Content::new();
172
173 for item in items {
174 match item {
175 DisplayItem::GlyphPath {
176 x,
177 y,
178 scale,
179 font,
180 char_code,
181 color,
182 ..
183 } => {
184 emit_glyph(
185 &mut content,
186 *x * em + pad,
187 *y * em + pad,
188 font,
189 *char_code,
190 *scale,
191 color,
192 em,
193 page_h,
194 embedded,
195 font_index,
196 font_data,
197 );
198 }
199 DisplayItem::Line {
200 x,
201 y,
202 width,
203 thickness,
204 color,
205 dashed,
206 } => {
207 emit_line(
208 &mut content,
209 &LineParams {
210 x: *x * em + pad,
211 y: *y * em + pad,
212 width: *width * em,
213 thickness: *thickness * em,
214 color: *color,
215 dashed: *dashed,
216 page_h,
217 },
218 );
219 }
220 DisplayItem::Rect {
221 x,
222 y,
223 width,
224 height,
225 color,
226 } => {
227 emit_rect(
228 &mut content,
229 *x * em + pad,
230 *y * em + pad,
231 *width * em,
232 *height * em,
233 color,
234 page_h,
235 );
236 }
237 DisplayItem::Path {
238 x,
239 y,
240 commands,
241 fill,
242 color,
243 } => {
244 emit_path(
245 &mut content,
246 *x * em + pad,
247 *y * em + pad,
248 commands,
249 *fill,
250 color,
251 em,
252 stroke_width,
253 page_h,
254 );
255 }
256 }
257 }
258
259 content.finish().into_vec()
260}
261
262#[inline]
264fn flip_y(y: f64, page_h: f64) -> f32 {
265 (page_h - y) as f32
266}
267
268#[allow(clippy::too_many_arguments)]
273fn emit_glyph(
274 content: &mut Content,
275 px: f64,
276 py: f64,
277 font_name: &str,
278 char_code: u32,
279 scale: f64,
280 color: &Color,
281 em: f64,
282 page_h: f64,
283 embedded: &[EmbeddedFont],
284 font_index: &HashMap<FontId, usize>,
285 font_data: &fonts::RawFontData,
286) {
287 let font_id = FontId::parse(font_name).unwrap_or(FontId::MainRegular);
288
289 let actual_fid = if font_data.contains_key(&font_id) {
291 font_id
292 } else {
293 FontId::MainRegular
294 };
295
296 let bytes = match font_data.get(&actual_fid) {
297 Some(b) => b,
298 None => return,
299 };
300
301 let gid = match fonts::resolve_glyph_id(bytes, font_id, char_code) {
302 Some(g) => g,
303 None => return,
304 };
305
306 let ef_idx = match font_index.get(&actual_fid) {
307 Some(&i) => i,
308 None => return,
309 };
310 let ef = &embedded[ef_idx];
311
312 let new_cid = match ef.remapper.get(gid) {
313 Some(c) => c,
314 None => return,
315 };
316
317 let glyph_em = (scale * em) as f32;
318 let pdf_x = px as f32;
319 let pdf_y = flip_y(py, page_h);
320
321 let cid_bytes = [(new_cid >> 8) as u8, (new_cid & 0xFF) as u8];
323
324 set_fill_rgb(content, color);
325 content.begin_text();
326 content.set_font(Name(ef.res_name.as_bytes()), glyph_em);
327 content.set_text_matrix([1.0, 0.0, 0.0, 1.0, pdf_x, pdf_y]);
328 content.show(Str(&cid_bytes));
329 content.end_text();
330}
331
332struct LineParams {
337 x: f64,
338 y: f64,
339 width: f64,
340 thickness: f64,
341 color: Color,
342 dashed: bool,
343 page_h: f64,
344}
345
346fn emit_line(content: &mut Content, line: &LineParams) {
347 let t = line.thickness.max(0.5);
348
349 set_fill_rgb(content, &line.color);
350
351 if line.dashed {
352 let dash_len = (4.0 * t).max(1.0);
353 let gap_len = (4.0 * t).max(1.0);
354 let period = dash_len + gap_len;
355 let top = line.y - t / 2.0;
356 let mut cur_x = line.x;
357 while cur_x < line.x + line.width {
358 let seg_w = dash_len.min(line.x + line.width - cur_x).max(0.5);
359 let pdf_x = cur_x as f32;
360 let pdf_y = flip_y(top + t, line.page_h); content.rect(pdf_x, pdf_y, seg_w as f32, t as f32);
362 cur_x += period;
363 }
364 content.fill_nonzero();
365 } else {
366 let top = line.y - t / 2.0;
367 let pdf_x = line.x as f32;
368 let pdf_y = flip_y(top + t, line.page_h);
369 content.rect(pdf_x, pdf_y, line.width as f32, t as f32);
370 content.fill_nonzero();
371 }
372}
373
374fn emit_rect(
379 content: &mut Content,
380 x: f64,
381 y: f64,
382 width: f64,
383 height: f64,
384 color: &Color,
385 page_h: f64,
386) {
387 let w = width.max(0.5);
388 let h = height.max(0.5);
389
390 set_fill_rgb(content, color);
391 let pdf_x = x as f32;
392 let pdf_y = flip_y(y + h, page_h); content.rect(pdf_x, pdf_y, w as f32, h as f32);
394 content.fill_nonzero();
395}
396
397#[allow(clippy::too_many_arguments)]
402fn emit_path(
403 content: &mut Content,
404 ox: f64,
405 oy: f64,
406 commands: &[PathCommand],
407 fill: bool,
408 color: &Color,
409 em: f64,
410 stroke_width: f64,
411 page_h: f64,
412) {
413 if fill {
414 let mut start = 0;
416 for i in 1..commands.len() {
417 if matches!(commands[i], PathCommand::MoveTo { .. }) {
418 emit_path_segment(content, ox, oy, &commands[start..i], true, color, em, stroke_width, page_h);
419 start = i;
420 }
421 }
422 emit_path_segment(content, ox, oy, &commands[start..], true, color, em, stroke_width, page_h);
423 } else {
424 emit_path_segment(content, ox, oy, commands, false, color, em, stroke_width, page_h);
425 }
426}
427
428#[allow(clippy::too_many_arguments)]
429fn emit_path_segment(
430 content: &mut Content,
431 ox: f64,
432 oy: f64,
433 commands: &[PathCommand],
434 fill: bool,
435 color: &Color,
436 em: f64,
437 stroke_width: f64,
438 page_h: f64,
439) {
440 if commands.is_empty() {
441 return;
442 }
443
444 let mut cur = (0.0f32, 0.0f32);
446
447 for cmd in commands {
448 match cmd {
449 PathCommand::MoveTo { x, y } => {
450 let px = (ox + x * em) as f32;
451 let py = flip_y(oy + y * em, page_h);
452 content.move_to(px, py);
453 cur = (px, py);
454 }
455 PathCommand::LineTo { x, y } => {
456 let px = (ox + x * em) as f32;
457 let py = flip_y(oy + y * em, page_h);
458 content.line_to(px, py);
459 cur = (px, py);
460 }
461 PathCommand::CubicTo { x1, y1, x2, y2, x, y } => {
462 let end_x = (ox + x * em) as f32;
463 let end_y = flip_y(oy + y * em, page_h);
464 content.cubic_to(
465 (ox + x1 * em) as f32,
466 flip_y(oy + y1 * em, page_h),
467 (ox + x2 * em) as f32,
468 flip_y(oy + y2 * em, page_h),
469 end_x,
470 end_y,
471 );
472 cur = (end_x, end_y);
473 }
474 PathCommand::QuadTo { x1, y1, x, y } => {
475 let qx = (ox + x1 * em) as f32;
478 let qy = flip_y(oy + y1 * em, page_h);
479 let end_x = (ox + x * em) as f32;
480 let end_y = flip_y(oy + y * em, page_h);
481 let cp1_x = cur.0 + 2.0 / 3.0 * (qx - cur.0);
482 let cp1_y = cur.1 + 2.0 / 3.0 * (qy - cur.1);
483 let cp2_x = end_x + 2.0 / 3.0 * (qx - end_x);
484 let cp2_y = end_y + 2.0 / 3.0 * (qy - end_y);
485 content.cubic_to(cp1_x, cp1_y, cp2_x, cp2_y, end_x, end_y);
486 cur = (end_x, end_y);
487 }
488 PathCommand::Close => {
489 content.close_path();
490 }
491 }
492 }
493
494 if fill {
495 set_fill_rgb(content, color);
496 content.fill_even_odd();
497 } else {
498 set_stroke_rgb(content, color);
499 content.set_line_width(stroke_width as f32);
500 content.stroke();
501 }
502}
503
504fn set_fill_rgb(content: &mut Content, color: &Color) {
509 content.set_fill_rgb(color.r, color.g, color.b);
510}
511
512fn set_stroke_rgb(content: &mut Content, color: &Color) {
513 content.set_stroke_rgb(color.r, color.g, color.b);
514}