1use std::collections::HashMap;
4use std::fmt::Write as FmtWrite;
5
6use justpdf_core::color::{Color as PdfColor, ColorSpace};
7use justpdf_core::content::{ContentOp, Operand, parse_content_stream};
8use justpdf_core::font::{FontInfo, ToUnicodeCMap, parse_font_info};
9use justpdf_core::image;
10use justpdf_core::object::{PdfDict, PdfObject};
11use justpdf_core::page::PageInfo;
12use justpdf_core::PdfDocument;
13
14use crate::error::{RenderError, Result};
15use crate::graphics_state::{
16 GraphicsState, LineCap, LineJoin, Matrix, PdfBlendMode,
17};
18
19struct ResolvedFont {
21 info: FontInfo,
22 cmap: Option<ToUnicodeCMap>,
23 #[allow(dead_code)]
24 font_data: Option<Vec<u8>>,
25}
26
27pub struct SvgRenderer<'a> {
29 doc: &'a PdfDocument,
30 state: GraphicsState,
31 state_stack: Vec<GraphicsState>,
32 fonts: HashMap<Vec<u8>, ResolvedFont>,
33 page_transform: Matrix,
35 path_data: Option<String>,
37 elements: Vec<String>,
39 defs: Vec<String>,
41 id_counter: u32,
43 active_clip_id: Option<String>,
45 clip_id_stack: Vec<Option<String>>,
47 xobject_depth: u32,
49 page_width: f64,
51 page_height: f64,
52}
53
54impl<'a> SvgRenderer<'a> {
55 pub fn new(
56 doc: &'a PdfDocument,
57 page_transform: Matrix,
58 page_width: f64,
59 page_height: f64,
60 ) -> Self {
61 Self {
62 doc,
63 state: GraphicsState::default(),
64 state_stack: Vec::new(),
65 fonts: HashMap::new(),
66 page_transform,
67 path_data: None,
68 elements: Vec::new(),
69 defs: Vec::new(),
70 id_counter: 0,
71 active_clip_id: None,
72 clip_id_stack: Vec::new(),
73 xobject_depth: 0,
74 page_width,
75 page_height,
76 }
77 }
78
79 fn next_id(&mut self, prefix: &str) -> String {
80 self.id_counter += 1;
81 format!("{}{}", prefix, self.id_counter)
82 }
83
84 pub fn render_page(mut self, page: &PageInfo) -> Result<String> {
86 let _ = self.resolve_page_fonts(page);
87
88 let content_data = self.get_page_content(page)?;
89 if !content_data.is_empty() {
90 let ops = parse_content_stream(&content_data).map_err(RenderError::Core)?;
91 self.execute_ops(&ops, page)?;
92 }
93
94 Ok(self.build_svg())
95 }
96
97 fn build_svg(&self) -> String {
98 let mut svg = String::new();
99 let _ = write!(
100 svg,
101 r#"<?xml version="1.0" encoding="UTF-8"?>
102<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 {} {}" width="{}" height="{}">"#,
103 self.page_width, self.page_height, self.page_width, self.page_height
104 );
105
106 let _ = write!(
108 svg,
109 r#"
110<rect width="{}" height="{}" fill="white"/>"#,
111 self.page_width, self.page_height
112 );
113
114 if !self.defs.is_empty() {
115 svg.push_str("\n<defs>");
116 for d in &self.defs {
117 svg.push('\n');
118 svg.push_str(d);
119 }
120 svg.push_str("\n</defs>");
121 }
122
123 for el in &self.elements {
124 svg.push('\n');
125 svg.push_str(el);
126 }
127
128 svg.push_str("\n</svg>\n");
129 svg
130 }
131
132 fn resolve_page_fonts(&mut self, page: &PageInfo) -> Result<()> {
137 let resources_obj = match &page.resources_ref {
138 Some(obj) => self.resolve_object(obj)?,
139 None => return Ok(()),
140 };
141
142 let resources_dict = match &resources_obj {
143 PdfObject::Dict(d) => d.clone(),
144 _ => return Ok(()),
145 };
146
147 let font_dict_obj = match resources_dict.get(b"Font") {
148 Some(PdfObject::Dict(d)) => PdfObject::Dict(d.clone()),
149 Some(PdfObject::Reference(r)) => {
150 let r = r.clone();
151 self.doc.resolve(&r)?
152 }
153 _ => return Ok(()),
154 };
155
156 if let PdfObject::Dict(font_dict) = &font_dict_obj {
157 for (name, val) in font_dict.iter() {
158 let font_obj = match val {
159 PdfObject::Reference(r) => {
160 let r = r.clone();
161 self.doc.resolve(&r)?
162 }
163 other => other.clone(),
164 };
165
166 if let PdfObject::Dict(fd) = &font_obj {
167 let mut info = parse_font_info(fd);
168
169 let cmap = if let Some(PdfObject::Reference(tu_ref)) = fd.get(b"ToUnicode") {
170 let tu_ref = tu_ref.clone();
171 if let Ok(tu_obj) = self.doc.resolve(&tu_ref) {
172 if let PdfObject::Stream { dict, data } = tu_obj {
173 let decoded = self.doc.decode_stream(&dict, &data).ok();
174 decoded.map(|d| ToUnicodeCMap::parse(&d))
175 } else {
176 None
177 }
178 } else {
179 None
180 }
181 } else {
182 None
183 };
184
185 if info.subtype == b"Type0" {
187 if let Some(PdfObject::Array(descendants)) = fd.get(b"DescendantFonts") {
188 if let Some(desc_ref) = descendants.first() {
189 let desc_obj = match desc_ref {
190 PdfObject::Reference(r) => {
191 let r = r.clone();
192 self.doc.resolve(&r)?
193 }
194 other => other.clone(),
195 };
196 if let PdfObject::Dict(cid_dict) = &desc_obj {
197 let cid_info = parse_font_info(cid_dict);
198 info.widths = cid_info.widths;
199 }
200 }
201 }
202 }
203
204 self.fonts.insert(
205 name.clone(),
206 ResolvedFont {
207 info,
208 cmap,
209 font_data: None, },
211 );
212 }
213 }
214 }
215
216 Ok(())
217 }
218
219 fn resolve_object(&mut self, obj: &PdfObject) -> Result<PdfObject> {
220 match obj {
221 PdfObject::Reference(r) => {
222 let r = r.clone();
223 Ok(self.doc.resolve(&r)?)
224 }
225 other => Ok(other.clone()),
226 }
227 }
228
229 fn get_page_content(&mut self, page: &PageInfo) -> Result<Vec<u8>> {
230 let contents = match &page.contents_ref {
231 Some(c) => c.clone(),
232 None => return Ok(Vec::new()),
233 };
234
235 match &contents {
236 PdfObject::Reference(r) => {
237 let r = r.clone();
238 let obj = self.doc.resolve(&r)?;
239 match obj {
240 PdfObject::Stream { dict, data } => {
241 Ok(self.doc.decode_stream(&dict, &data).unwrap_or_default())
242 }
243 PdfObject::Array(arr) => self.concat_content_streams(&arr),
244 _ => Ok(Vec::new()),
245 }
246 }
247 PdfObject::Array(arr) => {
248 let arr = arr.clone();
249 self.concat_content_streams(&arr)
250 }
251 PdfObject::Stream { dict, data } => {
252 Ok(self.doc.decode_stream(dict, data).unwrap_or_default())
253 }
254 _ => Ok(Vec::new()),
255 }
256 }
257
258 fn concat_content_streams(&mut self, arr: &[PdfObject]) -> Result<Vec<u8>> {
259 let mut combined = Vec::new();
260 for item in arr {
261 let obj = match item {
262 PdfObject::Reference(r) => {
263 let r = r.clone();
264 self.doc.resolve(&r)?
265 }
266 other => other.clone(),
267 };
268 if let PdfObject::Stream { dict, data } = obj {
269 if let Ok(decoded) = self.doc.decode_stream(&dict, &data) {
270 combined.extend_from_slice(&decoded);
271 combined.push(b' ');
272 }
273 }
274 }
275 Ok(combined)
276 }
277
278 fn execute_ops(&mut self, ops: &[ContentOp], page: &PageInfo) -> Result<()> {
283 for op in ops {
284 self.execute_op(op, page)?;
285 }
286 Ok(())
287 }
288
289 fn execute_op(&mut self, op: &ContentOp, page: &PageInfo) -> Result<()> {
290 let operator = op.operator_str();
291 let operands = &op.operands;
292
293 match operator {
294 "q" => {
296 self.state_stack.push(self.state.clone());
297 self.clip_id_stack.push(self.active_clip_id.clone());
298 }
299 "Q" => {
300 if let Some(s) = self.state_stack.pop() {
301 self.state = s;
302 }
303 if let Some(clip_id) = self.clip_id_stack.pop() {
304 self.active_clip_id = clip_id;
305 }
306 }
307 "cm" => {
308 if operands.len() >= 6 {
309 let m = Matrix {
310 a: f(operands, 0),
311 b: f(operands, 1),
312 c: f(operands, 2),
313 d: f(operands, 3),
314 e: f(operands, 4),
315 f: f(operands, 5),
316 };
317 self.state.ctm = m.concat(&self.state.ctm);
318 }
319 }
320
321 "w" => {
323 if let Some(v) = operands.first().and_then(|o| o.as_f64()) {
324 self.state.line_width = v;
325 }
326 }
327 "J" => {
328 if let Some(v) = operands.first().and_then(|o| o.as_i64()) {
329 self.state.line_cap = match v {
330 1 => LineCap::Round,
331 2 => LineCap::Square,
332 _ => LineCap::Butt,
333 };
334 }
335 }
336 "j" => {
337 if let Some(v) = operands.first().and_then(|o| o.as_i64()) {
338 self.state.line_join = match v {
339 1 => LineJoin::Round,
340 2 => LineJoin::Bevel,
341 _ => LineJoin::Miter,
342 };
343 }
344 }
345 "M" => {
346 if let Some(v) = operands.first().and_then(|o| o.as_f64()) {
347 self.state.miter_limit = v;
348 }
349 }
350 "d" => {
351 if operands.len() >= 2 {
352 if let Some(arr) = operands[0].as_array() {
353 self.state.dash_pattern =
354 arr.iter().filter_map(|o| o.as_f64()).collect();
355 }
356 self.state.dash_phase = f(operands, 1);
357 }
358 }
359
360 "gs" => {
362 if let Some(name) = operands.first().and_then(|o| o.as_name()) {
363 let _ = self.apply_extgstate(name, page);
364 }
365 }
366
367 "m" => {
369 let pd = self.path_data.get_or_insert_with(String::new);
370 let _ = write!(pd, "M{} {} ", fmt_f(f(operands, 0)), fmt_f(f(operands, 1)));
371 }
372 "l" => {
373 if let Some(pd) = &mut self.path_data {
374 let _ = write!(pd, "L{} {} ", fmt_f(f(operands, 0)), fmt_f(f(operands, 1)));
375 }
376 }
377 "c" => {
378 if let Some(pd) = &mut self.path_data {
379 let _ = write!(
380 pd,
381 "C{} {} {} {} {} {} ",
382 fmt_f(f(operands, 0)),
383 fmt_f(f(operands, 1)),
384 fmt_f(f(operands, 2)),
385 fmt_f(f(operands, 3)),
386 fmt_f(f(operands, 4)),
387 fmt_f(f(operands, 5)),
388 );
389 }
390 }
391 "v" => {
392 if let Some(pd) = &mut self.path_data {
393 let _ = write!(
395 pd,
396 "C{} {} {} {} {} {} ",
397 fmt_f(f(operands, 0)),
398 fmt_f(f(operands, 1)),
399 fmt_f(f(operands, 0)),
400 fmt_f(f(operands, 1)),
401 fmt_f(f(operands, 2)),
402 fmt_f(f(operands, 3)),
403 );
404 }
405 }
406 "y" => {
407 if let Some(pd) = &mut self.path_data {
408 let _ = write!(
410 pd,
411 "C{} {} {} {} {} {} ",
412 fmt_f(f(operands, 0)),
413 fmt_f(f(operands, 1)),
414 fmt_f(f(operands, 2)),
415 fmt_f(f(operands, 3)),
416 fmt_f(f(operands, 2)),
417 fmt_f(f(operands, 3)),
418 );
419 }
420 }
421 "h" => {
422 if let Some(pd) = &mut self.path_data {
423 pd.push_str("Z ");
424 }
425 }
426 "re" => {
427 if operands.len() >= 4 {
428 let x = f(operands, 0);
429 let y = f(operands, 1);
430 let w = f(operands, 2);
431 let h = f(operands, 3);
432 let pd = self.path_data.get_or_insert_with(String::new);
433 let _ = write!(
434 pd,
435 "M{} {} L{} {} L{} {} L{} {} Z ",
436 fmt_f(x), fmt_f(y),
437 fmt_f(x + w), fmt_f(y),
438 fmt_f(x + w), fmt_f(y + h),
439 fmt_f(x), fmt_f(y + h),
440 );
441 }
442 }
443
444 "S" => {
446 self.stroke_current_path();
447 }
448 "s" => {
449 if let Some(pd) = &mut self.path_data {
450 pd.push_str("Z ");
451 }
452 self.stroke_current_path();
453 }
454 "f" | "F" => {
455 self.fill_current_path("nonzero");
456 }
457 "f*" => {
458 self.fill_current_path("evenodd");
459 }
460 "B" => {
461 self.fill_and_stroke_path("nonzero");
462 }
463 "B*" => {
464 self.fill_and_stroke_path("evenodd");
465 }
466 "b" => {
467 if let Some(pd) = &mut self.path_data {
468 pd.push_str("Z ");
469 }
470 self.fill_and_stroke_path("nonzero");
471 }
472 "b*" => {
473 if let Some(pd) = &mut self.path_data {
474 pd.push_str("Z ");
475 }
476 self.fill_and_stroke_path("evenodd");
477 }
478 "n" => {
479 self.path_data = None;
480 }
481
482 "W" => {
484 self.apply_clip("nonzero");
485 }
486 "W*" => {
487 self.apply_clip("evenodd");
488 }
489
490 "CS" => {
492 if let Some(name) = operands.first().and_then(|o| o.as_name()) {
493 self.state.stroke_cs = cs_from_name(name);
494 if name != b"Pattern" {
495 self.state.stroke_pattern_name = None;
496 }
497 }
498 }
499 "cs" => {
500 if let Some(name) = operands.first().and_then(|o| o.as_name()) {
501 self.state.fill_cs = cs_from_name(name);
502 if name != b"Pattern" {
503 self.state.fill_pattern_name = None;
504 }
505 }
506 }
507 "SC" | "SCN" => {
508 let last_is_name = operands.last().and_then(|o| o.as_name());
509 if last_is_name.is_some() {
510 self.state.stroke_pattern_name = last_is_name.map(|n| n.to_vec());
511 let comps: Vec<f64> = operands.iter().filter_map(|o| o.as_f64()).collect();
512 if !comps.is_empty() {
513 self.state.stroke_color = PdfColor { components: comps };
514 }
515 } else {
516 let comps: Vec<f64> = operands.iter().filter_map(|o| o.as_f64()).collect();
517 if !comps.is_empty() {
518 self.state.stroke_color = PdfColor { components: comps };
519 }
520 }
521 }
522 "sc" | "scn" => {
523 let last_is_name = operands.last().and_then(|o| o.as_name());
524 if last_is_name.is_some() {
525 self.state.fill_pattern_name = last_is_name.map(|n| n.to_vec());
526 let comps: Vec<f64> = operands.iter().filter_map(|o| o.as_f64()).collect();
527 if !comps.is_empty() {
528 self.state.fill_color = PdfColor { components: comps };
529 }
530 } else {
531 let comps: Vec<f64> = operands.iter().filter_map(|o| o.as_f64()).collect();
532 if !comps.is_empty() {
533 self.state.fill_color = PdfColor { components: comps };
534 }
535 }
536 }
537 "G" => {
538 self.state.stroke_cs = ColorSpace::DeviceGray;
539 self.state.stroke_color = PdfColor::gray(f(operands, 0));
540 }
541 "g" => {
542 self.state.fill_cs = ColorSpace::DeviceGray;
543 self.state.fill_color = PdfColor::gray(f(operands, 0));
544 }
545 "RG" => {
546 self.state.stroke_cs = ColorSpace::DeviceRGB;
547 self.state.stroke_color =
548 PdfColor::rgb(f(operands, 0), f(operands, 1), f(operands, 2));
549 }
550 "rg" => {
551 self.state.fill_cs = ColorSpace::DeviceRGB;
552 self.state.fill_color =
553 PdfColor::rgb(f(operands, 0), f(operands, 1), f(operands, 2));
554 }
555 "K" => {
556 self.state.stroke_cs = ColorSpace::DeviceCMYK;
557 self.state.stroke_color = PdfColor::cmyk(
558 f(operands, 0),
559 f(operands, 1),
560 f(operands, 2),
561 f(operands, 3),
562 );
563 }
564 "k" => {
565 self.state.fill_cs = ColorSpace::DeviceCMYK;
566 self.state.fill_color = PdfColor::cmyk(
567 f(operands, 0),
568 f(operands, 1),
569 f(operands, 2),
570 f(operands, 3),
571 );
572 }
573
574 "BT" => {
576 self.state.text_matrix = Matrix::identity();
577 self.state.text_line_matrix = Matrix::identity();
578 }
579 "ET" => {}
580 "Tc" => {
581 self.state.text.char_spacing = f(operands, 0);
582 }
583 "Tw" => {
584 self.state.text.word_spacing = f(operands, 0);
585 }
586 "Tz" => {
587 self.state.text.horiz_scaling = f(operands, 0) / 100.0;
588 }
589 "TL" => {
590 self.state.text.leading = f(operands, 0);
591 }
592 "Tf" => {
593 if let Some(name) = operands.first().and_then(|o| o.as_name()) {
594 self.state.text.font_name = name.to_vec();
595 }
596 if operands.len() > 1 {
597 self.state.text.font_size = f(operands, 1);
598 }
599 }
600 "Tr" => {
601 self.state.text.render_mode =
602 operands.first().and_then(|o| o.as_i64()).unwrap_or(0);
603 }
604 "Ts" => {
605 self.state.text.text_rise = f(operands, 0);
606 }
607 "Td" => {
608 let tx = f(operands, 0);
609 let ty = f(operands, 1);
610 let t = Matrix::translate(tx, ty);
611 self.state.text_line_matrix = t.concat(&self.state.text_line_matrix);
612 self.state.text_matrix = self.state.text_line_matrix;
613 }
614 "TD" => {
615 let tx = f(operands, 0);
616 let ty = f(operands, 1);
617 self.state.text.leading = -ty;
618 let t = Matrix::translate(tx, ty);
619 self.state.text_line_matrix = t.concat(&self.state.text_line_matrix);
620 self.state.text_matrix = self.state.text_line_matrix;
621 }
622 "Tm" => {
623 if operands.len() >= 6 {
624 let m = Matrix {
625 a: f(operands, 0),
626 b: f(operands, 1),
627 c: f(operands, 2),
628 d: f(operands, 3),
629 e: f(operands, 4),
630 f: f(operands, 5),
631 };
632 self.state.text_matrix = m;
633 self.state.text_line_matrix = m;
634 }
635 }
636 "T*" => {
637 let leading = self.state.text.leading;
638 let t = Matrix::translate(0.0, -leading);
639 self.state.text_line_matrix = t.concat(&self.state.text_line_matrix);
640 self.state.text_matrix = self.state.text_line_matrix;
641 }
642 "Tj" => {
643 if let Some(s) = operands.first().and_then(|o| o.as_str()) {
644 self.render_text_string(s);
645 }
646 }
647 "TJ" => {
648 if let Some(arr) = operands.first().and_then(|o| o.as_array()) {
649 for item in arr {
650 match item {
651 Operand::String(s) => {
652 self.render_text_string(s);
653 }
654 Operand::Integer(n) => {
655 self.adjust_text_position(*n as f64);
656 }
657 Operand::Real(n) => {
658 self.adjust_text_position(*n);
659 }
660 _ => {}
661 }
662 }
663 }
664 }
665 "'" => {
666 let leading = self.state.text.leading;
667 let t = Matrix::translate(0.0, -leading);
668 self.state.text_line_matrix = t.concat(&self.state.text_line_matrix);
669 self.state.text_matrix = self.state.text_line_matrix;
670 if let Some(s) = operands.first().and_then(|o| o.as_str()) {
671 self.render_text_string(s);
672 }
673 }
674 "\"" => {
675 if operands.len() >= 3 {
676 self.state.text.word_spacing = f(operands, 0);
677 self.state.text.char_spacing = f(operands, 1);
678 let leading = self.state.text.leading;
679 let t = Matrix::translate(0.0, -leading);
680 self.state.text_line_matrix = t.concat(&self.state.text_line_matrix);
681 self.state.text_matrix = self.state.text_line_matrix;
682 if let Some(s) = operands.get(2).and_then(|o| o.as_str()) {
683 self.render_text_string(s);
684 }
685 }
686 }
687
688 "Do" => {
690 if let Some(name) = operands.first().and_then(|o| o.as_name()) {
691 let _ = self.do_xobject(name, page);
692 }
693 }
694
695 "BI" => {}
697
698 "BMC" | "BDC" | "EMC" | "MP" | "DP" => {}
700
701 "sh" => {}
703
704 "d0" | "d1" => {}
706
707 "BX" | "EX" => {}
709
710 _ => {}
711 }
712
713 Ok(())
714 }
715
716 fn effective_transform(&self) -> Matrix {
721 self.state.ctm.concat(&self.page_transform)
722 }
723
724 fn svg_transform_attr(&self) -> String {
725 let m = self.effective_transform();
726 format!(
727 "transform=\"matrix({},{},{},{},{},{})\"",
728 fmt_f(m.a), fmt_f(m.b), fmt_f(m.c), fmt_f(m.d), fmt_f(m.e), fmt_f(m.f)
729 )
730 }
731
732 fn fill_color_svg(&self) -> String {
733 let rgb = self.state.fill_color.to_rgb(&self.state.fill_cs);
734 format!(
735 "rgb({},{},{})",
736 (rgb[0].clamp(0.0, 1.0) * 255.0).round() as u8,
737 (rgb[1].clamp(0.0, 1.0) * 255.0).round() as u8,
738 (rgb[2].clamp(0.0, 1.0) * 255.0).round() as u8,
739 )
740 }
741
742 fn stroke_color_svg(&self) -> String {
743 let rgb = self.state.stroke_color.to_rgb(&self.state.stroke_cs);
744 format!(
745 "rgb({},{},{})",
746 (rgb[0].clamp(0.0, 1.0) * 255.0).round() as u8,
747 (rgb[1].clamp(0.0, 1.0) * 255.0).round() as u8,
748 (rgb[2].clamp(0.0, 1.0) * 255.0).round() as u8,
749 )
750 }
751
752 fn opacity_attrs(&self, for_fill: bool, for_stroke: bool) -> String {
753 let mut attrs = String::new();
754 if for_fill && self.state.fill_alpha < 1.0 {
755 let _ = write!(attrs, " fill-opacity=\"{}\"", fmt_f(self.state.fill_alpha));
756 }
757 if for_stroke && self.state.stroke_alpha < 1.0 {
758 let _ = write!(attrs, " stroke-opacity=\"{}\"", fmt_f(self.state.stroke_alpha));
759 }
760 attrs
761 }
762
763 fn blend_mode_attr(&self) -> String {
764 let bm = match self.state.blend_mode {
765 PdfBlendMode::Normal => return String::new(),
766 PdfBlendMode::Multiply => "multiply",
767 PdfBlendMode::Screen => "screen",
768 PdfBlendMode::Overlay => "overlay",
769 PdfBlendMode::Darken => "darken",
770 PdfBlendMode::Lighten => "lighten",
771 PdfBlendMode::ColorDodge => "color-dodge",
772 PdfBlendMode::ColorBurn => "color-burn",
773 PdfBlendMode::HardLight => "hard-light",
774 PdfBlendMode::SoftLight => "soft-light",
775 PdfBlendMode::Difference => "difference",
776 PdfBlendMode::Exclusion => "exclusion",
777 PdfBlendMode::Hue => "hue",
778 PdfBlendMode::Saturation => "saturation",
779 PdfBlendMode::Color => "color",
780 PdfBlendMode::Luminosity => "luminosity",
781 };
782 format!(" style=\"mix-blend-mode:{}\"", bm)
783 }
784
785 fn clip_attr(&self) -> String {
786 match &self.active_clip_id {
787 Some(id) => format!(" clip-path=\"url(#{})\"", id),
788 None => String::new(),
789 }
790 }
791
792 fn stroke_attrs(&self) -> String {
793 let mut attrs = String::new();
794 let _ = write!(attrs, " stroke-width=\"{}\"", fmt_f(self.state.line_width));
795 match self.state.line_cap {
796 LineCap::Butt => {}
797 LineCap::Round => attrs.push_str(" stroke-linecap=\"round\""),
798 LineCap::Square => attrs.push_str(" stroke-linecap=\"square\""),
799 }
800 match self.state.line_join {
801 LineJoin::Miter => {}
802 LineJoin::Round => attrs.push_str(" stroke-linejoin=\"round\""),
803 LineJoin::Bevel => attrs.push_str(" stroke-linejoin=\"bevel\""),
804 }
805 if self.state.miter_limit != 4.0 {
806 let _ = write!(attrs, " stroke-miterlimit=\"{}\"", fmt_f(self.state.miter_limit));
807 }
808 if !self.state.dash_pattern.is_empty() {
809 let dashes: Vec<String> = self.state.dash_pattern.iter().map(|d| fmt_f(*d)).collect();
810 let _ = write!(attrs, " stroke-dasharray=\"{}\"", dashes.join(","));
811 if self.state.dash_phase != 0.0 {
812 let _ = write!(attrs, " stroke-dashoffset=\"{}\"", fmt_f(self.state.dash_phase));
813 }
814 }
815 attrs
816 }
817
818 fn fill_current_path(&mut self, fill_rule: &str) {
819 if let Some(pd) = self.path_data.take() {
820 let transform = self.svg_transform_attr();
821 let fill = self.fill_color_svg();
822 let opacity = self.opacity_attrs(true, false);
823 let clip = self.clip_attr();
824 let bm = self.blend_mode_attr();
825 let rule = if fill_rule == "evenodd" {
826 " fill-rule=\"evenodd\""
827 } else {
828 ""
829 };
830 self.elements.push(format!(
831 "<path d=\"{}\" fill=\"{}\"{} stroke=\"none\" {}{}{}{}/>",
832 pd.trim(), fill, rule, transform, opacity, clip, bm,
833 ));
834 }
835 }
836
837 fn stroke_current_path(&mut self) {
838 if let Some(pd) = self.path_data.take() {
839 let transform = self.svg_transform_attr();
840 let stroke = self.stroke_color_svg();
841 let opacity = self.opacity_attrs(false, true);
842 let clip = self.clip_attr();
843 let bm = self.blend_mode_attr();
844 let stroke_attrs = self.stroke_attrs();
845 self.elements.push(format!(
846 "<path d=\"{}\" fill=\"none\" stroke=\"{}\"{}{}{}{}{}/>",
847 pd.trim(), stroke, stroke_attrs, transform, opacity, clip, bm,
848 ));
849 }
850 }
851
852 fn fill_and_stroke_path(&mut self, fill_rule: &str) {
853 if let Some(pd) = self.path_data.take() {
854 let transform = self.svg_transform_attr();
855 let fill = self.fill_color_svg();
856 let stroke = self.stroke_color_svg();
857 let opacity = self.opacity_attrs(true, true);
858 let clip = self.clip_attr();
859 let bm = self.blend_mode_attr();
860 let stroke_attrs = self.stroke_attrs();
861 let rule = if fill_rule == "evenodd" {
862 " fill-rule=\"evenodd\""
863 } else {
864 ""
865 };
866 self.elements.push(format!(
867 "<path d=\"{}\" fill=\"{}\"{} stroke=\"{}\"{}{}{}{}{}/>",
868 pd.trim(), fill, rule, stroke, stroke_attrs, transform, opacity, clip, bm,
869 ));
870 }
871 }
872
873 fn apply_clip(&mut self, clip_rule: &str) {
874 if let Some(pd) = self.path_data.clone() {
875 let clip_id = self.next_id("clip");
876 let transform = self.svg_transform_attr();
877 let rule = if clip_rule == "evenodd" {
878 " clip-rule=\"evenodd\""
879 } else {
880 ""
881 };
882 self.defs.push(format!(
883 "<clipPath id=\"{}\"><path d=\"{}\"{} {}/></clipPath>",
884 clip_id, pd.trim(), rule, transform,
885 ));
886 self.active_clip_id = Some(clip_id);
887 }
888 }
889
890 fn render_text_string(&mut self, string_bytes: &[u8]) {
895 let font_name = self.state.text.font_name.clone();
896 let font = match self.fonts.get(&font_name) {
897 Some(f) => f,
898 None => return,
899 };
900
901 let font_size = self.state.text.font_size;
902 let horiz_scaling = self.state.text.horiz_scaling;
903 let char_spacing = self.state.text.char_spacing;
904 let word_spacing = self.state.text.word_spacing;
905 let text_rise = self.state.text.text_rise;
906 let render_mode = self.state.text.render_mode;
907 let is_cid = font.info.subtype == b"Type0";
908
909 let char_codes: Vec<u32> = if is_cid {
911 string_bytes
912 .chunks(2)
913 .map(|c| {
914 if c.len() == 2 {
915 ((c[0] as u32) << 8) | (c[1] as u32)
916 } else {
917 c[0] as u32
918 }
919 })
920 .collect()
921 } else {
922 string_bytes.iter().map(|b| *b as u32).collect()
923 };
924
925 let cmap = font.cmap.as_ref();
927
928 let widths: Vec<f64> = char_codes.iter().map(|code| font.info.widths.get_width(*code)).collect();
930
931 let font_family = extract_font_family(&font.info);
933
934 for (i, code) in char_codes.iter().enumerate() {
935 let width = widths[i];
936 let w0 = width / 1000.0;
937
938 if render_mode != 3 {
939 let text_char = if let Some(cm) = cmap {
941 cm.lookup(*code)
942 } else if *code < 128 {
943 char::from_u32(*code).map(|c| c.to_string())
945 } else {
946 None
947 };
948
949 if let Some(text) = text_char {
950 let trm = Matrix {
952 a: font_size * horiz_scaling,
953 b: 0.0,
954 c: 0.0,
955 d: font_size,
956 e: 0.0,
957 f: text_rise,
958 }
959 .concat(&self.state.text_matrix)
960 .concat(&self.state.ctm)
961 .concat(&self.page_transform);
962
963 let fill_color = self.fill_color_svg();
964 let opacity = self.opacity_attrs(true, false);
965 let clip = self.clip_attr();
966 let bm = self.blend_mode_attr();
967
968 let effective_size = trm.font_size_scale();
970
971 let x = trm.e;
973 let y = trm.f;
974
975 let escaped = xml_escape(&text);
976 self.elements.push(format!(
977 "<text x=\"{}\" y=\"{}\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\"{}{}{}>{}</text>",
978 fmt_f(x), fmt_f(y), xml_escape(&font_family), fmt_f(effective_size),
979 fill_color, opacity, clip, bm, escaped,
980 ));
981 } else {
982 let glyph_width = w0 * font_size;
984 if glyph_width.abs() > 0.001 {
985 let trm = self
986 .state
987 .text_matrix
988 .concat(&self.state.ctm)
989 .concat(&self.page_transform);
990
991 let fill_color = self.fill_color_svg();
992 let opacity = self.opacity_attrs(true, false);
993 let clip = self.clip_attr();
994
995 let rx = 0.0_f64;
996 let ry = text_rise - font_size * 0.2;
997 let rw = glyph_width;
998 let rh = font_size * 0.8;
999
1000 let pd = format!(
1001 "M{} {} L{} {} L{} {} L{} {} Z",
1002 fmt_f(rx), fmt_f(ry),
1003 fmt_f(rx + rw), fmt_f(ry),
1004 fmt_f(rx + rw), fmt_f(ry + rh),
1005 fmt_f(rx), fmt_f(ry + rh),
1006 );
1007
1008 self.elements.push(format!(
1009 "<path d=\"{}\" fill=\"{}\" stroke=\"none\" transform=\"matrix({},{},{},{},{},{})\"{}{}/>",
1010 pd, fill_color,
1011 fmt_f(trm.a), fmt_f(trm.b), fmt_f(trm.c), fmt_f(trm.d), fmt_f(trm.e), fmt_f(trm.f),
1012 opacity, clip,
1013 ));
1014 }
1015 }
1016 }
1017
1018 let tx = (w0 * font_size + char_spacing) * horiz_scaling;
1020 let tx = if *code == 32 {
1021 tx + word_spacing * horiz_scaling
1022 } else {
1023 tx
1024 };
1025
1026 let advance = Matrix::translate(tx, 0.0);
1027 self.state.text_matrix = advance.concat(&self.state.text_matrix);
1028 }
1029 }
1030
1031 fn adjust_text_position(&mut self, amount: f64) {
1032 let font_size = self.state.text.font_size;
1033 let horiz_scaling = self.state.text.horiz_scaling;
1034 let tx = -amount / 1000.0 * font_size * horiz_scaling;
1035 let advance = Matrix::translate(tx, 0.0);
1036 self.state.text_matrix = advance.concat(&self.state.text_matrix);
1037 }
1038
1039 fn do_xobject(&mut self, name: &[u8], page: &PageInfo) -> Result<()> {
1044 let xobj = self.resolve_xobject(name, page)?;
1045 let xobj = match xobj {
1046 Some(x) => x,
1047 None => return Ok(()),
1048 };
1049
1050 match xobj {
1051 XObjectData::Image { dict, data } => {
1052 let _ = self.render_image(&dict, &data);
1053 }
1054 XObjectData::Form { dict, data } => {
1055 if self.xobject_depth > 10 {
1056 return Ok(());
1057 }
1058 self.xobject_depth += 1;
1059 let _ = self.render_form_xobject(&dict, &data, page);
1060 self.xobject_depth -= 1;
1061 }
1062 }
1063
1064 Ok(())
1065 }
1066
1067 fn resolve_xobject(&mut self, name: &[u8], page: &PageInfo) -> Result<Option<XObjectData>> {
1068 let resources_obj = match &page.resources_ref {
1069 Some(obj) => self.resolve_object(obj)?,
1070 None => return Ok(None),
1071 };
1072
1073 let resources_dict = match &resources_obj {
1074 PdfObject::Dict(d) => d.clone(),
1075 _ => return Ok(None),
1076 };
1077
1078 let xobject_dict_obj = match resources_dict.get(b"XObject") {
1079 Some(PdfObject::Dict(d)) => PdfObject::Dict(d.clone()),
1080 Some(PdfObject::Reference(r)) => {
1081 let r = r.clone();
1082 self.doc.resolve(&r)?
1083 }
1084 _ => return Ok(None),
1085 };
1086
1087 let xobject_dict = match &xobject_dict_obj {
1088 PdfObject::Dict(d) => d,
1089 _ => return Ok(None),
1090 };
1091
1092 let xobj_ref = match xobject_dict.get(name) {
1093 Some(PdfObject::Reference(r)) => r.clone(),
1094 _ => return Ok(None),
1095 };
1096
1097 let xobj = self.doc.resolve(&xobj_ref)?;
1098
1099 match xobj {
1100 PdfObject::Stream { dict, data } => {
1101 let subtype = dict.get_name(b"Subtype").unwrap_or(b"");
1102 match subtype {
1103 b"Image" => {
1104 let filter = dict.get(b"Filter").and_then(|o| o.as_name());
1105 let image_data = if filter == Some(b"DCTDecode") {
1106 data.clone()
1107 } else {
1108 match self.doc.decode_stream(&dict, &data) {
1109 Ok(d) => d,
1110 Err(_) => return Ok(None),
1111 }
1112 };
1113 Ok(Some(XObjectData::Image {
1114 dict,
1115 data: image_data,
1116 }))
1117 }
1118 b"Form" => match self.doc.decode_stream(&dict, &data) {
1119 Ok(decoded) => Ok(Some(XObjectData::Form {
1120 dict,
1121 data: decoded,
1122 })),
1123 Err(_) => Ok(None),
1124 },
1125 _ => Ok(None),
1126 }
1127 }
1128 _ => Ok(None),
1129 }
1130 }
1131
1132 fn render_image(&mut self, dict: &PdfDict, data: &[u8]) -> Result<()> {
1133 let decoded = image::decode_image(data, dict).map_err(RenderError::Core)?;
1134
1135 let rgba_data = image_to_rgba(&decoded);
1136 let w = decoded.width;
1137 let h = decoded.height;
1138
1139 let png_bytes = encode_rgba_to_png(&rgba_data, w, h);
1141 let b64 = base64_encode(&png_bytes);
1142
1143 let image_transform = Matrix {
1145 a: 1.0 / w as f64,
1146 b: 0.0,
1147 c: 0.0,
1148 d: -1.0 / h as f64,
1149 e: 0.0,
1150 f: 1.0,
1151 };
1152
1153 let full_transform = image_transform
1154 .concat(&self.state.ctm)
1155 .concat(&self.page_transform);
1156
1157 let opacity = if self.state.fill_alpha < 1.0 {
1158 format!(" opacity=\"{}\"", fmt_f(self.state.fill_alpha))
1159 } else {
1160 String::new()
1161 };
1162 let clip = self.clip_attr();
1163 let bm = self.blend_mode_attr();
1164
1165 self.elements.push(format!(
1166 "<image width=\"{}\" height=\"{}\" href=\"data:image/png;base64,{}\" transform=\"matrix({},{},{},{},{},{})\" preserveAspectRatio=\"none\"{}{}{}/>",
1167 w, h, b64,
1168 fmt_f(full_transform.a), fmt_f(full_transform.b),
1169 fmt_f(full_transform.c), fmt_f(full_transform.d),
1170 fmt_f(full_transform.e), fmt_f(full_transform.f),
1171 opacity, clip, bm,
1172 ));
1173
1174 Ok(())
1175 }
1176
1177 fn render_form_xobject(
1178 &mut self,
1179 dict: &PdfDict,
1180 data: &[u8],
1181 page: &PageInfo,
1182 ) -> Result<()> {
1183 self.state_stack.push(self.state.clone());
1184 self.clip_id_stack.push(self.active_clip_id.clone());
1185
1186 if let Some(matrix_arr) = dict.get_array(b"Matrix") {
1188 if matrix_arr.len() >= 6 {
1189 let m = Matrix {
1190 a: matrix_arr[0].as_f64().unwrap_or(1.0),
1191 b: matrix_arr[1].as_f64().unwrap_or(0.0),
1192 c: matrix_arr[2].as_f64().unwrap_or(0.0),
1193 d: matrix_arr[3].as_f64().unwrap_or(1.0),
1194 e: matrix_arr[4].as_f64().unwrap_or(0.0),
1195 f: matrix_arr[5].as_f64().unwrap_or(0.0),
1196 };
1197 self.state.ctm = m.concat(&self.state.ctm);
1198 }
1199 }
1200
1201 let ops = parse_content_stream(data).map_err(RenderError::Core)?;
1202 let _ = self.execute_ops(&ops, page);
1203
1204 if let Some(s) = self.state_stack.pop() {
1205 self.state = s;
1206 }
1207 if let Some(clip_id) = self.clip_id_stack.pop() {
1208 self.active_clip_id = clip_id;
1209 }
1210
1211 Ok(())
1212 }
1213
1214 fn apply_extgstate(&mut self, name: &[u8], page: &PageInfo) -> Result<()> {
1219 let resources_obj = match &page.resources_ref {
1220 Some(obj) => self.resolve_object(obj)?,
1221 None => return Ok(()),
1222 };
1223
1224 let resources_dict = match &resources_obj {
1225 PdfObject::Dict(d) => d.clone(),
1226 _ => return Ok(()),
1227 };
1228
1229 let extgstate_dict_obj = match resources_dict.get(b"ExtGState") {
1230 Some(PdfObject::Dict(d)) => PdfObject::Dict(d.clone()),
1231 Some(PdfObject::Reference(r)) => {
1232 let r = r.clone();
1233 self.doc.resolve(&r)?
1234 }
1235 _ => return Ok(()),
1236 };
1237
1238 let extgstate_dict = match &extgstate_dict_obj {
1239 PdfObject::Dict(d) => d,
1240 _ => return Ok(()),
1241 };
1242
1243 let gs_obj = match extgstate_dict.get(name) {
1244 Some(PdfObject::Reference(r)) => {
1245 let r = r.clone();
1246 self.doc.resolve(&r)?
1247 }
1248 Some(other) => other.clone(),
1249 None => return Ok(()),
1250 };
1251
1252 if let PdfObject::Dict(gs_dict) = &gs_obj {
1253 if let Some(lw) = gs_dict.get(b"LW").and_then(|o| o.as_f64()) {
1254 self.state.line_width = lw;
1255 }
1256 if let Some(lc) = gs_dict.get(b"LC").and_then(|o| o.as_i64()) {
1257 self.state.line_cap = match lc {
1258 1 => LineCap::Round,
1259 2 => LineCap::Square,
1260 _ => LineCap::Butt,
1261 };
1262 }
1263 if let Some(lj) = gs_dict.get(b"LJ").and_then(|o| o.as_i64()) {
1264 self.state.line_join = match lj {
1265 1 => LineJoin::Round,
1266 2 => LineJoin::Bevel,
1267 _ => LineJoin::Miter,
1268 };
1269 }
1270 if let Some(ml) = gs_dict.get(b"ML").and_then(|o| o.as_f64()) {
1271 self.state.miter_limit = ml;
1272 }
1273 if let Some(a) = gs_dict.get(b"ca").and_then(|o| o.as_f64()) {
1274 self.state.fill_alpha = a;
1275 }
1276 if let Some(a) = gs_dict.get(b"CA").and_then(|o| o.as_f64()) {
1277 self.state.stroke_alpha = a;
1278 }
1279 if let Some(bm_name) = gs_dict.get(b"BM").and_then(|o| o.as_name()) {
1280 self.state.blend_mode = PdfBlendMode::from_name(bm_name);
1281 }
1282 }
1283
1284 Ok(())
1285 }
1286}
1287
1288enum XObjectData {
1293 Image { dict: PdfDict, data: Vec<u8> },
1294 Form { dict: PdfDict, data: Vec<u8> },
1295}
1296
1297fn f(operands: &[Operand], idx: usize) -> f64 {
1299 operands.get(idx).and_then(|o| o.as_f64()).unwrap_or(0.0)
1300}
1301
1302fn cs_from_name(name: &[u8]) -> ColorSpace {
1304 match name {
1305 b"DeviceRGB" => ColorSpace::DeviceRGB,
1306 b"DeviceCMYK" => ColorSpace::DeviceCMYK,
1307 b"DeviceGray" => ColorSpace::DeviceGray,
1308 _ => ColorSpace::DeviceGray,
1309 }
1310}
1311
1312fn fmt_f(v: f64) -> String {
1314 if (v - v.round()).abs() < 1e-6 {
1315 format!("{}", v.round() as i64)
1316 } else {
1317 format!("{:.4}", v)
1318 .trim_end_matches('0')
1319 .trim_end_matches('.')
1320 .to_string()
1321 }
1322}
1323
1324fn xml_escape(s: &str) -> String {
1326 s.replace('&', "&")
1327 .replace('<', "<")
1328 .replace('>', ">")
1329 .replace('"', """)
1330 .replace('\'', "'")
1331}
1332
1333fn extract_font_family(info: &FontInfo) -> String {
1335 let base = String::from_utf8_lossy(&info.base_font).to_string();
1336 if base.is_empty() {
1337 return "sans-serif".to_string();
1338 }
1339 let name = if base.len() > 7 && base.as_bytes()[6] == b'+' {
1341 &base[7..]
1342 } else {
1343 &base
1344 };
1345 name.replace(',', " ").replace('-', " ")
1347}
1348
1349fn image_to_rgba(img: &image::DecodedImage) -> Vec<u8> {
1351 let pixel_count = (img.width * img.height) as usize;
1352 let mut rgba = vec![255u8; pixel_count * 4];
1353
1354 match img.components {
1355 1 => {
1356 for i in 0..pixel_count.min(img.data.len()) {
1358 let g = img.data[i];
1359 rgba[i * 4] = g;
1360 rgba[i * 4 + 1] = g;
1361 rgba[i * 4 + 2] = g;
1362 }
1363 }
1364 3 => {
1365 for i in 0..pixel_count.min(img.data.len() / 3) {
1367 rgba[i * 4] = img.data[i * 3];
1368 rgba[i * 4 + 1] = img.data[i * 3 + 1];
1369 rgba[i * 4 + 2] = img.data[i * 3 + 2];
1370 }
1371 }
1372 4 => {
1373 for i in 0..pixel_count.min(img.data.len() / 4) {
1375 let c = img.data[i * 4] as f64 / 255.0;
1376 let m = img.data[i * 4 + 1] as f64 / 255.0;
1377 let y = img.data[i * 4 + 2] as f64 / 255.0;
1378 let k = img.data[i * 4 + 3] as f64 / 255.0;
1379 rgba[i * 4] = ((1.0 - c) * (1.0 - k) * 255.0) as u8;
1380 rgba[i * 4 + 1] = ((1.0 - m) * (1.0 - k) * 255.0) as u8;
1381 rgba[i * 4 + 2] = ((1.0 - y) * (1.0 - k) * 255.0) as u8;
1382 }
1383 }
1384 _ => {
1385 for i in 0..pixel_count {
1387 rgba[i * 4] = 0;
1388 rgba[i * 4 + 1] = 0;
1389 rgba[i * 4 + 2] = 0;
1390 }
1391 }
1392 }
1393
1394 rgba
1395}
1396
1397fn encode_rgba_to_png(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
1399 let mut buf = std::io::Cursor::new(Vec::new());
1401 let encoder = ::image::codecs::png::PngEncoder::new(&mut buf);
1402 let _ = ::image::ImageEncoder::write_image(
1403 encoder,
1404 rgba,
1405 width,
1406 height,
1407 ::image::ColorType::Rgba8.into(),
1408 );
1409 buf.into_inner()
1410}
1411
1412fn base64_encode(data: &[u8]) -> String {
1414 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1415 let mut result = String::with_capacity((data.len() + 2) / 3 * 4);
1416
1417 for chunk in data.chunks(3) {
1418 let b0 = chunk[0] as u32;
1419 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
1420 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
1421
1422 let n = (b0 << 16) | (b1 << 8) | b2;
1423
1424 result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
1425 result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
1426
1427 if chunk.len() > 1 {
1428 result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
1429 } else {
1430 result.push('=');
1431 }
1432
1433 if chunk.len() > 2 {
1434 result.push(CHARS[(n & 0x3F) as usize] as char);
1435 } else {
1436 result.push('=');
1437 }
1438 }
1439
1440 result
1441}