1use std::collections::HashMap;
31use std::io::Cursor;
32use std::path::Path;
33use std::sync::OnceLock;
34
35use image::{DynamicImage, RgbaImage};
36
37pub use image::ImageFormat;
39use tiny_skia::{
40 FillRule, Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform as SkTransform,
41};
42
43use crate::error::{OfdError, Result};
44use crate::model::graphics::{
45 CompositeObject, CtCgTransform, CtColor, CtVectorG, ImageObject, PageBlock, PathObject,
46 TextObject, parse_deltas,
47};
48use crate::model::resource::{CtColorSpace, CtDrawParam};
49use crate::types::{StBox, StLoc, StRefId, parent_dir, resolve_path};
50use crate::{LoadedDocument, OfdReader, PageObject, PageRef};
51
52use std::io::{Read, Seek};
53
54const MAX_DIMENSION: u32 = 20_000;
56
57#[derive(Debug, Clone)]
59pub struct RenderOptions {
60 pub dpi: f64,
62 pub background: Option<[u8; 4]>,
64}
65
66impl Default for RenderOptions {
67 fn default() -> Self {
68 RenderOptions {
69 dpi: 150.0,
70 background: Some([255, 255, 255, 255]),
71 }
72 }
73}
74
75impl RenderOptions {
76 pub fn with_dpi(dpi: f64) -> Self {
78 RenderOptions {
79 dpi,
80 ..Default::default()
81 }
82 }
83
84 pub fn background(mut self, background: Option<[u8; 4]>) -> Self {
86 self.background = background;
87 self
88 }
89}
90
91const MM_PER_INCH: f64 = 25.4;
93
94struct FontRes {
96 data: Option<Vec<u8>>,
99 index: u32,
101}
102
103static SYSTEM_FONTS: OnceLock<fontdb::Database> = OnceLock::new();
105
106fn system_fonts() -> &'static fontdb::Database {
108 SYSTEM_FONTS.get_or_init(|| {
109 let mut db = fontdb::Database::new();
110 db.load_system_fonts();
111 db
112 })
113}
114
115const CJK_FALLBACK_FAMILIES: &[&str] = &[
122 "Noto Sans CJK SC",
123 "Source Han Sans SC",
124 "Noto Sans SC",
125 "Microsoft YaHei",
126 "微软雅黑",
127 "SimSun",
128 "宋体",
129 "WenQuanYi Micro Hei",
130 "WenQuanYi Zen Hei",
131 "Noto Serif CJK SC",
132];
133
134fn is_cjk(ch: char) -> bool {
136 matches!(ch as u32,
137 0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x3000..=0x303F | 0xFF00..=0xFFEF | 0x2E80..=0x2EFF )
143}
144
145fn lookup_system_font(
152 name: &str,
153 family: &str,
154 bold: bool,
155 italic: bool,
156) -> Option<(Vec<u8>, u32)> {
157 let db = system_fonts();
158 let style = if italic {
159 fontdb::Style::Italic
160 } else {
161 fontdb::Style::Normal
162 };
163 let weight = if bold {
164 fontdb::Weight::BOLD
165 } else {
166 fontdb::Weight::NORMAL
167 };
168
169 let query_family = |fam: &str| -> Option<(Vec<u8>, u32)> {
170 let fam = fam.trim();
171 if fam.is_empty() {
172 return None;
173 }
174 let q = fontdb::Query {
175 families: &[fontdb::Family::Name(fam)],
176 weight,
177 style,
178 stretch: fontdb::Stretch::Normal,
179 };
180 db.query(&q)
181 .and_then(|id| db.with_face_data(id, |data, index| (data.to_vec(), index)))
182 };
183
184 for cand in [name, family] {
186 if let Some(data) = query_family(cand) {
187 return Some(data);
188 }
189 }
190
191 if name.chars().chain(family.chars()).any(is_cjk) {
194 for fam in CJK_FALLBACK_FAMILIES {
195 if let Some(data) = query_family(fam) {
196 return Some(data);
197 }
198 }
199 }
200
201 let q = fontdb::Query {
203 families: &[fontdb::Family::SansSerif],
204 weight,
205 style,
206 stretch: fontdb::Stretch::Normal,
207 };
208 db.query(&q)
209 .and_then(|id| db.with_face_data(id, |data, index| (data.to_vec(), index)))
210}
211
212#[derive(Default)]
214struct DocResources {
215 color_spaces: HashMap<u64, CtColorSpace>,
216 draw_params: HashMap<u64, CtDrawParam>,
217 fonts: HashMap<u64, FontRes>,
218 media: HashMap<u64, Vec<u8>>,
219 vector_gs: HashMap<u64, CtVectorG>,
220 default_cs: Option<u64>,
221}
222
223#[derive(Debug, Clone, Copy)]
230struct Mat {
231 a: f64,
232 b: f64,
233 c: f64,
234 d: f64,
235 e: f64,
236 f: f64,
237}
238
239impl Mat {
240 fn identity() -> Self {
242 Mat {
243 a: 1.0,
244 b: 0.0,
245 c: 0.0,
246 d: 1.0,
247 e: 0.0,
248 f: 0.0,
249 }
250 }
251
252 fn translate(x: f64, y: f64) -> Self {
254 Mat {
255 a: 1.0,
256 b: 0.0,
257 c: 0.0,
258 d: 1.0,
259 e: x,
260 f: y,
261 }
262 }
263
264 fn scale(sx: f64, sy: f64) -> Self {
266 Mat {
267 a: sx,
268 b: 0.0,
269 c: 0.0,
270 d: sy,
271 e: 0.0,
272 f: 0.0,
273 }
274 }
275
276 fn rotate(theta: f64) -> Self {
279 let (s, c) = theta.sin_cos();
280 Mat {
281 a: c,
282 b: s,
283 c: -s,
284 d: c,
285 e: 0.0,
286 f: 0.0,
287 }
288 }
289
290 fn from_array(v: &[f64]) -> Option<Self> {
292 if v.len() == 6 {
293 Some(Mat {
294 a: v[0],
295 b: v[1],
296 c: v[2],
297 d: v[3],
298 e: v[4],
299 f: v[5],
300 })
301 } else {
302 None
303 }
304 }
305
306 fn mul(self, rhs: Mat) -> Mat {
308 Mat {
309 a: self.a * rhs.a + self.c * rhs.b,
310 b: self.b * rhs.a + self.d * rhs.b,
311 c: self.a * rhs.c + self.c * rhs.d,
312 d: self.b * rhs.c + self.d * rhs.d,
313 e: self.a * rhs.e + self.c * rhs.f + self.e,
314 f: self.b * rhs.e + self.d * rhs.f + self.f,
315 }
316 }
317
318 fn to_skia(self) -> SkTransform {
320 SkTransform::from_row(
321 self.a as f32,
322 self.b as f32,
323 self.c as f32,
324 self.d as f32,
325 self.e as f32,
326 self.f as f32,
327 )
328 }
329}
330
331type Rgba = [u8; 4];
333
334impl<R: Read + Seek> OfdReader<R> {
335 pub fn render_page(
339 &mut self,
340 doc: &LoadedDocument,
341 page_index: usize,
342 options: &RenderOptions,
343 ) -> Result<Pixmap> {
344 let pages = doc.pages();
345 let page_ref = pages
346 .get(page_index)
347 .ok_or_else(|| OfdError::Render(format!("page index {page_index} out of range")))?
348 .clone();
349
350 let page = self.load_page(doc, &page_ref)?;
352 let mut resources = self.build_resources(doc)?;
353 let page_res: Vec<StLoc> = page.page_res.clone();
354 for loc in &page_res {
355 self.load_res_into(&doc.base, loc, &mut resources);
356 }
357
358 let mut bg_templates: Vec<PageObject> = Vec::new();
360 let mut fg_templates: Vec<PageObject> = Vec::new();
361 for tref in &page.templates {
362 let Some(tpl) = doc
363 .template_pages()
364 .iter()
365 .find(|t| t.id == tref.template_id.as_id())
366 .cloned()
367 else {
368 continue;
369 };
370 let zorder = tref
371 .z_order
372 .clone()
373 .or_else(|| tpl.z_order.clone())
374 .unwrap_or_else(|| "Background".to_string());
375 let Ok(tpl_page) = self.load_template(doc, &tpl) else {
377 continue;
378 };
379 if zorder.eq_ignore_ascii_case("Foreground") {
380 fg_templates.push(tpl_page);
381 } else {
382 bg_templates.push(tpl_page);
383 }
384 }
385
386 let area = page
388 .area
389 .as_ref()
390 .or(doc.document.common_data.page_area.as_ref())
391 .map(|a| a.physical_box)
392 .unwrap_or(StBox::A4_MM);
393
394 let scale = options.dpi / MM_PER_INCH;
395 let width_px = ((area.width * scale).round() as i64).max(1);
396 let height_px = ((area.height * scale).round() as i64).max(1);
397 if width_px > MAX_DIMENSION as i64 || height_px > MAX_DIMENSION as i64 {
398 return Err(OfdError::Render(format!(
399 "rendered size {width_px}x{height_px} exceeds limit {MAX_DIMENSION}"
400 )));
401 }
402
403 let mut pixmap = Pixmap::new(width_px as u32, height_px as u32)
404 .ok_or_else(|| OfdError::Render("failed to allocate pixmap".to_string()))?;
405 if let Some([r, g, b, a]) = options.background {
406 pixmap.fill(tiny_skia::Color::from_rgba8(r, g, b, a));
407 }
408
409 let page_to_device =
411 Mat::translate(-area.x * scale, -area.y * scale).mul(Mat::scale(scale, scale));
412
413 for tpl in &bg_templates {
415 render_page_object(&mut pixmap, &resources, page_to_device, tpl);
416 }
417 render_page_object(&mut pixmap, &resources, page_to_device, &page);
418 for tpl in &fg_templates {
419 render_page_object(&mut pixmap, &resources, page_to_device, tpl);
420 }
421
422 self.render_annotations(doc, &page_ref, &resources, page_to_device, &mut pixmap);
424
425 Ok(pixmap)
426 }
427
428 fn render_annotations(
432 &mut self,
433 doc: &LoadedDocument,
434 page_ref: &PageRef,
435 res: &DocResources,
436 page_to_device: Mat,
437 pixmap: &mut Pixmap,
438 ) {
439 let Some(loc) = doc.document.annotations.clone() else {
440 return;
441 };
442 let path = resolve_path(&doc.base, &loc);
443 let Ok(annotations) = self.package.parse::<crate::Annotations>(&path) else {
444 return;
445 };
446 let ann_dir = parent_dir(&path).to_string();
448 let page_id = page_ref.id.value();
449
450 for pg in &annotations.pages {
451 if pg.page_id.value() != page_id {
452 continue;
453 }
454 let file_path = resolve_path(&ann_dir, &pg.file_loc);
455 let Ok(page_annot) = self.package.parse::<crate::PageAnnot>(&file_path) else {
456 continue;
457 };
458 for annot in &page_annot.annots {
459 if annot.visible == Some(false) {
460 continue;
461 }
462 let Some(app) = &annot.appearance else {
463 continue;
464 };
465 let app_to_device =
467 page_to_device.mul(Mat::translate(app.boundary.x, app.boundary.y));
468 for obj in &app.objects {
469 render_block(pixmap, res, app_to_device, obj, None);
470 }
471 }
472 }
473 }
474
475 pub fn render_page_to_image(
477 &mut self,
478 doc: &LoadedDocument,
479 page_index: usize,
480 options: &RenderOptions,
481 ) -> Result<RgbaImage> {
482 let pixmap = self.render_page(doc, page_index, options)?;
483 Ok(pixmap_to_image(&pixmap))
484 }
485
486 pub fn render_page_to_bytes(
488 &mut self,
489 doc: &LoadedDocument,
490 page_index: usize,
491 options: &RenderOptions,
492 format: ImageFormat,
493 ) -> Result<Vec<u8>> {
494 let img = self.render_page_to_image(doc, page_index, options)?;
495 encode_image(img, format)
496 }
497
498 pub fn render_page_to_file<P: AsRef<Path>>(
502 &mut self,
503 doc: &LoadedDocument,
504 page_index: usize,
505 options: &RenderOptions,
506 path: P,
507 ) -> Result<()> {
508 let path = path.as_ref();
509 let format = format_from_path(path).ok_or_else(|| {
510 OfdError::Render(format!(
511 "cannot infer image format from path: {}",
512 path.display()
513 ))
514 })?;
515 let bytes = self.render_page_to_bytes(doc, page_index, options, format)?;
516 std::fs::write(path, bytes)?;
517 Ok(())
518 }
519
520 fn build_resources(&mut self, doc: &LoadedDocument) -> Result<DocResources> {
522 let mut res = DocResources {
523 default_cs: doc.document.common_data.default_cs.map(|id| id.value()),
524 ..Default::default()
525 };
526 let locs: Vec<StLoc> = doc
527 .public_res()
528 .iter()
529 .chain(doc.document_res().iter())
530 .cloned()
531 .collect();
532 for loc in &locs {
533 self.load_res_into(&doc.base, loc, &mut res);
534 }
535 Ok(res)
536 }
537
538 fn load_res_into(&mut self, base_dir: &str, loc: &StLoc, res: &mut DocResources) {
542 let res_path = resolve_path(base_dir, loc);
543 let Ok(parsed) = self.package.parse::<crate::Res>(&res_path) else {
544 return;
545 };
546 let res_dir = parent_dir(&res_path).to_string();
548 let data_base = resolve_path(&res_dir, &parsed.base_loc);
549
550 for cs in parsed.color_spaces() {
551 res.color_spaces.insert(cs.id.value(), cs.clone());
552 }
553 for dp in parsed.draw_params() {
554 res.draw_params.insert(dp.id.value(), dp.clone());
555 }
556 for font in parsed.fonts() {
557 let embedded = font
558 .font_file
559 .as_ref()
560 .map(|ff| resolve_path(&data_base, ff))
561 .and_then(|p| self.package.read(&p).ok());
562 let res_font = match embedded {
563 Some(data) => FontRes {
564 data: Some(data),
565 index: 0,
566 },
567 None => {
569 let (data, index) = lookup_system_font(
570 &font.font_name,
571 font.family_name.as_deref().unwrap_or(""),
572 font.bold.unwrap_or(false),
573 font.italic.unwrap_or(false),
574 )
575 .map(|(d, i)| (Some(d), i))
576 .unwrap_or((None, 0));
577 FontRes { data, index }
578 }
579 };
580 res.fonts.insert(font.id.value(), res_font);
581 }
582 for mm in parsed.multi_medias() {
583 let p = resolve_path(&data_base, &mm.media_file);
584 if let Ok(bytes) = self.package.read(&p) {
585 res.media.insert(mm.id.value(), bytes);
586 }
587 }
588 for vg in parsed.composite_graphic_units() {
589 res.vector_gs.insert(vg.id.value(), vg.clone());
590 }
591 }
592}
593
594fn render_page_object(
599 pixmap: &mut Pixmap,
600 res: &DocResources,
601 page_to_device: Mat,
602 page: &PageObject,
603) {
604 let Some(content) = &page.content else {
605 return;
606 };
607 let mut layers: Vec<_> = content.layers.iter().collect();
608 layers.sort_by_key(|l| match l.layer_type.as_deref() {
609 Some("Background") => 0,
610 Some("Foreground") => 2,
611 _ => 1, });
613 for layer in layers {
614 let layer_dp = resolve_draw_param(res, layer.draw_param);
616 for obj in &layer.objects {
617 render_block(pixmap, res, page_to_device, obj, layer_dp.as_ref());
618 }
619 }
620}
621
622fn render_block(
627 pixmap: &mut Pixmap,
628 res: &DocResources,
629 page_to_device: Mat,
630 block: &PageBlock,
631 inherited: Option<&CtDrawParam>,
632) {
633 match block {
634 PageBlock::Path(p) => render_path(pixmap, res, page_to_device, p, inherited),
635 PageBlock::Text(t) => render_text(pixmap, res, page_to_device, t, inherited),
636 PageBlock::Image(i) => render_image(pixmap, res, page_to_device, i),
637 PageBlock::Block(g) => {
638 for obj in &g.objects {
639 render_block(pixmap, res, page_to_device, obj, inherited);
640 }
641 }
642 PageBlock::Composite(c) => render_composite(pixmap, res, page_to_device, c, 0, inherited),
643 }
644}
645
646const MAX_COMPOSITE_DEPTH: u32 = 16;
648
649fn render_composite(
655 pixmap: &mut Pixmap,
656 res: &DocResources,
657 page_to_device: Mat,
658 obj: &CompositeObject,
659 depth: u32,
660 inherited: Option<&CtDrawParam>,
661) {
662 if depth >= MAX_COMPOSITE_DEPTH {
663 return;
664 }
665 let Some(vg) = res.vector_gs.get(&obj.resource_id.value()) else {
666 return;
667 };
668 let Some(content) = &vg.content else {
669 return;
670 };
671 let inner_to_device = object_to_device(
673 page_to_device,
674 &obj.boundary,
675 obj.ctm.as_ref().map(|a| a.as_slice()),
676 );
677 for block in &content.objects {
678 render_block_at_depth(pixmap, res, inner_to_device, block, depth + 1, inherited);
679 }
680}
681
682fn render_block_at_depth(
684 pixmap: &mut Pixmap,
685 res: &DocResources,
686 page_to_device: Mat,
687 block: &PageBlock,
688 depth: u32,
689 inherited: Option<&CtDrawParam>,
690) {
691 match block {
692 PageBlock::Composite(c) => {
693 render_composite(pixmap, res, page_to_device, c, depth, inherited)
694 }
695 PageBlock::Block(g) => {
696 for obj in &g.objects {
697 render_block_at_depth(pixmap, res, page_to_device, obj, depth, inherited);
698 }
699 }
700 _ => render_block(pixmap, res, page_to_device, block, inherited),
701 }
702}
703
704fn object_to_device(page_to_device: Mat, boundary: &StBox, ctm: Option<&[f64]>) -> Mat {
706 let m = ctm.and_then(Mat::from_array).unwrap_or_else(Mat::identity);
707 page_to_device
708 .mul(Mat::translate(boundary.x, boundary.y))
709 .mul(m)
710}
711
712fn resolve_draw_param(res: &DocResources, id: Option<StRefId>) -> Option<CtDrawParam> {
718 let mut cur = res.draw_params.get(&id?.value())?.clone();
719 let mut visited = vec![cur.id.value()];
720 while let Some(rid) = cur.relative.map(|r| r.value()) {
721 if visited.contains(&rid) {
722 break;
723 }
724 let Some(parent) = res.draw_params.get(&rid) else {
725 break;
726 };
727 visited.push(rid);
728 fill_missing_draw_param(&mut cur, parent);
729 cur.relative = parent.relative;
731 }
732 Some(cur)
733}
734
735fn fill_missing_draw_param(dst: &mut CtDrawParam, src: &CtDrawParam) {
738 dst.line_width = dst.line_width.or(src.line_width);
739 dst.join = dst.join.take().or_else(|| src.join.clone());
740 dst.cap = dst.cap.take().or_else(|| src.cap.clone());
741 dst.miter_limit = dst.miter_limit.or(src.miter_limit);
742 dst.dash_offset = dst.dash_offset.or(src.dash_offset);
743 dst.dash_pattern = dst.dash_pattern.take().or_else(|| src.dash_pattern.clone());
744 dst.fill_color = dst.fill_color.take().or_else(|| src.fill_color.clone());
745 dst.stroke_color = dst.stroke_color.take().or_else(|| src.stroke_color.clone());
746}
747
748fn effective_draw_param(
754 res: &DocResources,
755 id: Option<StRefId>,
756 inherited: Option<&CtDrawParam>,
757) -> Option<CtDrawParam> {
758 match (resolve_draw_param(res, id), inherited) {
759 (Some(mut own), Some(parent)) => {
760 fill_missing_draw_param(&mut own, parent);
761 Some(own)
762 }
763 (Some(own), None) => Some(own),
764 (None, Some(parent)) => Some(parent.clone()),
765 (None, None) => None,
766 }
767}
768
769fn render_path(
771 pixmap: &mut Pixmap,
772 res: &DocResources,
773 page_to_device: Mat,
774 obj: &PathObject,
775 inherited: Option<&CtDrawParam>,
776) {
777 let Some(data) = &obj.abbreviated_data else {
778 return;
779 };
780 let Some(path) = build_path(data) else {
781 return;
782 };
783 let transform = object_to_device(
784 page_to_device,
785 &obj.boundary,
786 obj.ctm.as_ref().map(|a| a.as_slice()),
787 )
788 .to_skia();
789 let dp = effective_draw_param(res, obj.draw_param, inherited);
790 let dp = dp.as_ref();
791
792 let fill = obj.fill.unwrap_or(false);
793 let stroke = obj.stroke.unwrap_or(true);
794 let obj_alpha = obj.alpha;
795
796 if fill {
797 let color = obj
798 .fill_color
799 .as_ref()
800 .or_else(|| dp.and_then(|d| d.fill_color.as_ref()))
801 .map(|c| resolve_color(res, c, obj_alpha))
802 .unwrap_or([0, 0, 0, alpha_or_opaque(obj_alpha)]);
803 let mut paint = Paint::default();
804 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
805 paint.anti_alias = true;
806 let rule = match obj.rule.as_deref() {
807 Some("Even-Odd") | Some("EvenOdd") => FillRule::EvenOdd,
808 _ => FillRule::Winding,
809 };
810 pixmap.fill_path(&path, &paint, rule, transform, None);
811 }
812
813 if stroke {
814 let color = obj
815 .stroke_color
816 .as_ref()
817 .or_else(|| dp.and_then(|d| d.stroke_color.as_ref()))
818 .map(|c| resolve_color(res, c, obj_alpha))
819 .unwrap_or([0, 0, 0, alpha_or_opaque(obj_alpha)]);
820 let width = obj
821 .line_width
822 .or_else(|| dp.and_then(|d| d.line_width))
823 .unwrap_or(0.353);
824 let mut paint = Paint::default();
825 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
826 paint.anti_alias = true;
827 let cap = obj
829 .cap
830 .as_deref()
831 .or_else(|| dp.and_then(|d| d.cap.as_deref()));
832 let join = obj
833 .join
834 .as_deref()
835 .or_else(|| dp.and_then(|d| d.join.as_deref()));
836 let miter = obj
837 .miter_limit
838 .or_else(|| dp.and_then(|d| d.miter_limit))
839 .unwrap_or(4.234);
840 let dash = obj
842 .dash_pattern
843 .as_ref()
844 .or_else(|| dp.and_then(|d| d.dash_pattern.as_ref()))
845 .map(|p| p.as_slice().iter().map(|&v| v as f32).collect::<Vec<_>>())
846 .filter(|p| p.len() >= 2 && p.iter().any(|&v| v > 0.0))
847 .and_then(|p| {
848 let off = obj
849 .dash_offset
850 .or_else(|| dp.and_then(|d| d.dash_offset))
851 .unwrap_or(0.0) as f32;
852 tiny_skia::StrokeDash::new(p, off)
853 });
854 let stroke_style = Stroke {
855 width: width.max(f64::MIN_POSITIVE) as f32,
856 line_cap: match cap {
857 Some("Round") => tiny_skia::LineCap::Round,
858 Some("Square") => tiny_skia::LineCap::Square,
859 _ => tiny_skia::LineCap::Butt,
860 },
861 line_join: match join {
862 Some("Round") => tiny_skia::LineJoin::Round,
863 Some("Bevel") => tiny_skia::LineJoin::Bevel,
864 _ => tiny_skia::LineJoin::Miter,
865 },
866 miter_limit: miter as f32,
867 dash,
868 };
869 pixmap.stroke_path(&path, &paint, &stroke_style, transform, None);
870 }
871}
872
873fn build_path(data: &str) -> Option<tiny_skia::Path> {
878 let normalized = data.replace(',', " ");
879 let mut tokens = normalized.split_whitespace().peekable();
880 let mut pb = PathBuilder::new();
881 let mut started = false;
882 let mut cur = (0.0_f64, 0.0_f64);
884 let mut start = (0.0_f64, 0.0_f64);
885
886 fn take(
888 tokens: &mut std::iter::Peekable<std::str::SplitWhitespace>,
889 n: usize,
890 ) -> Option<Vec<f64>> {
891 let mut v = Vec::with_capacity(n);
892 for _ in 0..n {
893 let t = tokens.next()?;
894 v.push(t.parse::<f64>().ok()?);
895 }
896 Some(v)
897 }
898
899 while let Some(tok) = tokens.next() {
900 match tok {
901 "S" | "M" => {
902 if let Some(v) = take(&mut tokens, 2) {
903 pb.move_to(v[0] as f32, v[1] as f32);
904 cur = (v[0], v[1]);
905 start = cur;
906 started = true;
907 }
908 }
909 "L" => {
910 if started && let Some(v) = take(&mut tokens, 2) {
911 pb.line_to(v[0] as f32, v[1] as f32);
912 cur = (v[0], v[1]);
913 }
914 }
915 "Q" => {
916 if started && let Some(v) = take(&mut tokens, 4) {
917 pb.quad_to(v[0] as f32, v[1] as f32, v[2] as f32, v[3] as f32);
918 cur = (v[2], v[3]);
919 }
920 }
921 "B" => {
922 if started && let Some(v) = take(&mut tokens, 6) {
923 pb.cubic_to(
924 v[0] as f32,
925 v[1] as f32,
926 v[2] as f32,
927 v[3] as f32,
928 v[4] as f32,
929 v[5] as f32,
930 );
931 cur = (v[4], v[5]);
932 }
933 }
934 "A" => {
935 if started && let Some(v) = take(&mut tokens, 7) {
937 let end = (v[5], v[6]);
938 append_arc(
939 &mut pb,
940 cur,
941 v[0],
942 v[1],
943 v[2],
944 v[3] != 0.0,
945 v[4] != 0.0,
946 end,
947 );
948 cur = end;
949 }
950 }
951 "C" => {
952 if started {
953 pb.close();
954 cur = start;
955 }
956 }
957 _ => {}
958 }
959 }
960
961 pb.finish()
962}
963
964#[allow(clippy::too_many_arguments)]
972fn append_arc(
973 pb: &mut PathBuilder,
974 start: (f64, f64),
975 rx: f64,
976 ry: f64,
977 angle_deg: f64,
978 large_arc: bool,
979 sweep: bool,
980 end: (f64, f64),
981) {
982 let (x1, y1) = start;
983 let (x2, y2) = end;
984
985 let mut rx = rx.abs();
987 let mut ry = ry.abs();
988 if rx == 0.0 || ry == 0.0 || (x1 == x2 && y1 == y2) {
989 pb.line_to(x2 as f32, y2 as f32);
990 return;
991 }
992
993 let phi = (angle_deg % 360.0).to_radians();
995 let (sin_p, cos_p) = phi.sin_cos();
996
997 let dx = (x1 - x2) / 2.0;
999 let dy = (y1 - y2) / 2.0;
1000 let x1p = cos_p * dx + sin_p * dy;
1001 let y1p = -sin_p * dx + cos_p * dy;
1002
1003 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
1005 if lambda > 1.0 {
1006 let s = lambda.sqrt();
1007 rx *= s;
1008 ry *= s;
1009 }
1010
1011 let num = (rx * rx) * (ry * ry) - (rx * rx) * (y1p * y1p) - (ry * ry) * (x1p * x1p);
1013 let den = (rx * rx) * (y1p * y1p) + (ry * ry) * (x1p * x1p);
1014 let mut coef = if den > 0.0 {
1015 (num / den).max(0.0).sqrt()
1016 } else {
1017 0.0
1018 };
1019 if large_arc == sweep {
1020 coef = -coef;
1021 }
1022 let cxp = coef * (rx * y1p) / ry;
1023 let cyp = -coef * (ry * x1p) / rx;
1024
1025 let cx = cos_p * cxp - sin_p * cyp + (x1 + x2) / 2.0;
1027 let cy = sin_p * cxp + cos_p * cyp + (y1 + y2) / 2.0;
1028
1029 let ux = (x1p - cxp) / rx;
1031 let uy = (y1p - cyp) / ry;
1032 let vx = (-x1p - cxp) / rx;
1033 let vy = (-y1p - cyp) / ry;
1034 let angle = |ux: f64, uy: f64, vx: f64, vy: f64| -> f64 {
1035 let dot = ux * vx + uy * vy;
1036 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
1037 let mut a = (dot / len).clamp(-1.0, 1.0).acos();
1038 if ux * vy - uy * vx < 0.0 {
1039 a = -a;
1040 }
1041 a
1042 };
1043 let theta1 = angle(1.0, 0.0, ux, uy);
1044 let mut dtheta = angle(ux, uy, vx, vy);
1045 if !sweep && dtheta > 0.0 {
1046 dtheta -= 2.0 * std::f64::consts::PI;
1047 } else if sweep && dtheta < 0.0 {
1048 dtheta += 2.0 * std::f64::consts::PI;
1049 }
1050
1051 let segments = (dtheta.abs() / (std::f64::consts::PI / 2.0))
1053 .ceil()
1054 .max(1.0) as usize;
1055 let delta = dtheta / segments as f64;
1056 let t = (4.0 / 3.0) * (delta / 4.0).tan();
1057 let mut th = theta1;
1058 let point = |th: f64| -> (f64, f64) {
1060 let (s, c) = th.sin_cos();
1061 let ex = rx * c;
1062 let ey = ry * s;
1063 (cx + cos_p * ex - sin_p * ey, cy + sin_p * ex + cos_p * ey)
1064 };
1065 let deriv = |th: f64| -> (f64, f64) {
1066 let (s, c) = th.sin_cos();
1067 let ex = -rx * s;
1068 let ey = ry * c;
1069 (cos_p * ex - sin_p * ey, sin_p * ex + cos_p * ey)
1070 };
1071 for _ in 0..segments {
1072 let th2 = th + delta;
1073 let (px1, py1) = point(th);
1074 let (px2, py2) = point(th2);
1075 let (d1x, d1y) = deriv(th);
1076 let (d2x, d2y) = deriv(th2);
1077 let c1 = (px1 + t * d1x, py1 + t * d1y);
1078 let c2 = (px2 - t * d2x, py2 - t * d2y);
1079 pb.cubic_to(
1080 c1.0 as f32,
1081 c1.1 as f32,
1082 c2.0 as f32,
1083 c2.1 as f32,
1084 px2 as f32,
1085 py2 as f32,
1086 );
1087 th = th2;
1088 }
1089}
1090
1091struct PlacedGlyph {
1093 gid: ttf_parser::GlyphId,
1094 origin: (f64, f64),
1096}
1097
1098fn render_text(
1104 pixmap: &mut Pixmap,
1105 res: &DocResources,
1106 page_to_device: Mat,
1107 obj: &TextObject,
1108 inherited: Option<&CtDrawParam>,
1109) {
1110 let Some(font) = res.fonts.get(&obj.font.value()) else {
1111 return;
1112 };
1113 let Some(data) = &font.data else {
1114 return;
1116 };
1117 let Ok(face) = ttf_parser::Face::parse(data, font.index) else {
1118 return;
1119 };
1120 let units_per_em = face.units_per_em() as f64;
1121 if units_per_em <= 0.0 {
1122 return;
1123 }
1124
1125 let dp = effective_draw_param(res, obj.draw_param, inherited);
1126 let dp = dp.as_ref();
1127 let fill = obj.fill.unwrap_or(true);
1129 let stroke = obj.stroke.unwrap_or(false);
1130 if !fill && !stroke {
1131 return;
1132 }
1133
1134 let h_scale = obj.h_scale.unwrap_or(1.0);
1135 let scale_x = obj.size / units_per_em * h_scale;
1136 let scale_y = obj.size / units_per_em;
1137 let char_dir = obj.char_direction.unwrap_or(0) as f64;
1139
1140 let transform = object_to_device(
1141 page_to_device,
1142 &obj.boundary,
1143 obj.ctm.as_ref().map(|a| a.as_slice()),
1144 )
1145 .to_skia();
1146
1147 let placed = place_glyphs(obj, |ch| face.glyph_index(ch));
1148
1149 let mut pb = PathBuilder::new();
1150 for g in &placed {
1151 let glyph_mat = glyph_to_object(g.origin.0, g.origin.1, scale_x, scale_y, char_dir);
1154 let mut outliner = Outliner {
1155 pb: &mut pb,
1156 m: glyph_mat,
1157 };
1158 face.outline_glyph(g.gid, &mut outliner);
1159 }
1160
1161 let Some(path) = pb.finish() else {
1162 return;
1163 };
1164
1165 if fill {
1166 let color = obj
1167 .fill_color
1168 .as_ref()
1169 .or_else(|| dp.and_then(|d| d.fill_color.as_ref()))
1170 .map(|c| resolve_color(res, c, obj.alpha))
1171 .unwrap_or([0, 0, 0, alpha_or_opaque(obj.alpha)]);
1172 let mut paint = Paint::default();
1173 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
1174 paint.anti_alias = true;
1175 pixmap.fill_path(&path, &paint, FillRule::Winding, transform, None);
1176 }
1177
1178 if stroke {
1179 let Some(color) = obj
1181 .stroke_color
1182 .as_ref()
1183 .or_else(|| dp.and_then(|d| d.stroke_color.as_ref()))
1184 .map(|c| resolve_color(res, c, obj.alpha))
1185 .filter(|c| c[3] > 0)
1186 else {
1187 return;
1188 };
1189 let mut paint = Paint::default();
1190 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
1191 paint.anti_alias = true;
1192 let width = dp.and_then(|d| d.line_width).unwrap_or(0.353);
1194 let stroke_style = Stroke {
1195 width: width.max(f64::MIN_POSITIVE) as f32,
1196 ..Default::default()
1197 };
1198 pixmap.stroke_path(&path, &paint, &stroke_style, transform, None);
1199 }
1200}
1201
1202fn text_code_points(obj: &TextObject) -> Vec<(f64, f64)> {
1207 let mut points: Vec<(f64, f64)> = Vec::new();
1208 let mut inherited_x = 0.0_f64;
1209 let mut inherited_y = 0.0_f64;
1210 for tc in &obj.text_codes {
1211 let Some(text) = &tc.text else { continue };
1212 let start_x = tc.x.unwrap_or(inherited_x);
1213 let start_y = tc.y.unwrap_or(inherited_y);
1214 inherited_x = start_x;
1215 inherited_y = start_y;
1216 let dx = tc.delta_x.as_deref().map(parse_deltas).unwrap_or_default();
1217 let dy = tc.delta_y.as_deref().map(parse_deltas).unwrap_or_default();
1218 let (mut cx, mut cy) = (start_x, start_y);
1219 for (i, _) in text.chars().enumerate() {
1220 if i > 0 {
1221 cx += dx
1222 .get(i - 1)
1223 .copied()
1224 .unwrap_or_else(|| dx.last().copied().unwrap_or(0.0));
1225 cy += dy
1226 .get(i - 1)
1227 .copied()
1228 .unwrap_or_else(|| dy.last().copied().unwrap_or(0.0));
1229 }
1230 points.push((cx, cy));
1231 }
1232 }
1233 points
1234}
1235
1236fn place_glyphs(
1242 obj: &TextObject,
1243 cmap: impl Fn(char) -> Option<ttf_parser::GlyphId>,
1244) -> Vec<PlacedGlyph> {
1245 let points = text_code_points(obj);
1246 let chars: Vec<char> = obj
1247 .text_codes
1248 .iter()
1249 .filter_map(|tc| tc.text.as_deref())
1250 .flat_map(|t| t.chars())
1251 .collect();
1252
1253 let transforms: HashMap<usize, &CtCgTransform> = obj
1255 .cg_transforms
1256 .iter()
1257 .filter(|t| t.code_position >= 0)
1258 .map(|t| (t.code_position as usize, t))
1259 .collect();
1260
1261 let mut out = Vec::with_capacity(chars.len());
1262 let mut i = 0;
1263 while i < chars.len() {
1264 if let Some(t) = transforms.get(&i) {
1265 let code_count = t.code_count.unwrap_or(1).max(1) as usize;
1267 if let Some(glyphs) = &t.glyphs {
1268 for (j, &g) in glyphs.as_slice().iter().enumerate() {
1269 let pi = i + j.min(code_count.saturating_sub(1));
1272 if let Some(&origin) = points.get(pi) {
1273 out.push(PlacedGlyph {
1274 gid: ttf_parser::GlyphId(g as u16),
1275 origin,
1276 });
1277 }
1278 }
1279 }
1280 i += code_count;
1281 } else {
1282 if let Some(gid) = cmap(chars[i])
1284 && let Some(&origin) = points.get(i)
1285 {
1286 out.push(PlacedGlyph { gid, origin });
1287 }
1288 i += 1;
1289 }
1290 }
1291 out
1292}
1293
1294fn glyph_to_object(cx: f64, cy: f64, scale_x: f64, scale_y: f64, char_dir_deg: f64) -> Mat {
1300 Mat::translate(cx, cy)
1301 .mul(Mat::rotate(char_dir_deg.to_radians()))
1302 .mul(Mat::scale(scale_x, -scale_y))
1303}
1304
1305struct Outliner<'a> {
1307 pb: &'a mut PathBuilder,
1308 m: Mat,
1309}
1310
1311impl Outliner<'_> {
1312 fn map(&self, x: f32, y: f32) -> (f32, f32) {
1314 let (xf, yf) = (x as f64, y as f64);
1315 (
1316 (self.m.a * xf + self.m.c * yf + self.m.e) as f32,
1317 (self.m.b * xf + self.m.d * yf + self.m.f) as f32,
1318 )
1319 }
1320}
1321
1322impl ttf_parser::OutlineBuilder for Outliner<'_> {
1323 fn move_to(&mut self, x: f32, y: f32) {
1324 let (px, py) = self.map(x, y);
1325 self.pb.move_to(px, py);
1326 }
1327 fn line_to(&mut self, x: f32, y: f32) {
1328 let (px, py) = self.map(x, y);
1329 self.pb.line_to(px, py);
1330 }
1331 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1332 let (cx1, cy1) = self.map(x1, y1);
1333 let (px, py) = self.map(x, y);
1334 self.pb.quad_to(cx1, cy1, px, py);
1335 }
1336 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1337 let (cx1, cy1) = self.map(x1, y1);
1338 let (cx2, cy2) = self.map(x2, y2);
1339 let (px, py) = self.map(x, y);
1340 self.pb.cubic_to(cx1, cy1, cx2, cy2, px, py);
1341 }
1342 fn close(&mut self) {
1343 self.pb.close();
1344 }
1345}
1346
1347fn render_image(pixmap: &mut Pixmap, res: &DocResources, page_to_device: Mat, obj: &ImageObject) {
1349 let Some(bytes) = res.media.get(&obj.resource_id.value()) else {
1350 return;
1351 };
1352 let Ok(decoded) = image::load_from_memory(bytes) else {
1353 return;
1354 };
1355 let rgba = decoded.to_rgba8();
1356 let (iw, ih) = (rgba.width(), rgba.height());
1357 if iw == 0 || ih == 0 {
1358 return;
1359 }
1360 let Some(src) = rgba_to_pixmap(&rgba) else {
1361 return;
1362 };
1363
1364 let unit_obj = obj
1367 .ctm
1368 .as_ref()
1369 .and_then(|a| Mat::from_array(a.as_slice()))
1370 .unwrap_or_else(|| Mat::scale(obj.boundary.width, obj.boundary.height));
1371 let unit_to_device = page_to_device
1372 .mul(Mat::translate(obj.boundary.x, obj.boundary.y))
1373 .mul(unit_obj);
1374 let pixel_to_device = unit_to_device.mul(Mat::scale(1.0 / iw as f64, 1.0 / ih as f64));
1376
1377 let opacity = obj.alpha.map(|a| a as f32 / 255.0).unwrap_or(1.0);
1378 let paint = PixmapPaint {
1379 opacity,
1380 ..Default::default()
1381 };
1382 pixmap.draw_pixmap(0, 0, src.as_ref(), &paint, pixel_to_device.to_skia(), None);
1383}
1384
1385fn rgba_to_pixmap(img: &RgbaImage) -> Option<Pixmap> {
1387 let mut pm = Pixmap::new(img.width(), img.height())?;
1388 let dst = pm.data_mut();
1389 for (i, px) in img.pixels().enumerate() {
1390 let [r, g, b, a] = px.0;
1391 let af = a as u16;
1392 dst[i * 4] = (r as u16 * af / 255) as u8;
1393 dst[i * 4 + 1] = (g as u16 * af / 255) as u8;
1394 dst[i * 4 + 2] = (b as u16 * af / 255) as u8;
1395 dst[i * 4 + 3] = a;
1396 }
1397 Some(pm)
1398}
1399
1400fn pixmap_to_image(pixmap: &Pixmap) -> RgbaImage {
1402 let (w, h) = (pixmap.width(), pixmap.height());
1403 let mut out = RgbaImage::new(w, h);
1404 for (i, px) in pixmap.pixels().iter().enumerate() {
1405 let c = px.demultiply();
1406 let x = (i as u32) % w;
1407 let y = (i as u32) / w;
1408 out.put_pixel(x, y, image::Rgba([c.red(), c.green(), c.blue(), c.alpha()]));
1409 }
1410 out
1411}
1412
1413fn encode_image(img: RgbaImage, format: ImageFormat) -> Result<Vec<u8>> {
1415 let mut buf = Cursor::new(Vec::new());
1416 match format {
1417 ImageFormat::Jpeg => {
1418 DynamicImage::ImageRgba8(img)
1419 .to_rgb8()
1420 .write_to(&mut buf, format)?;
1421 }
1422 _ => {
1423 img.write_to(&mut buf, format)?;
1424 }
1425 }
1426 Ok(buf.into_inner())
1427}
1428
1429fn format_from_path(path: &Path) -> Option<ImageFormat> {
1431 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
1432 image_format_from_ext(&ext)
1433}
1434
1435pub fn image_format_from_ext(ext: &str) -> Option<ImageFormat> {
1437 match ext.to_ascii_lowercase().as_str() {
1438 "png" => Some(ImageFormat::Png),
1439 "jpg" | "jpeg" => Some(ImageFormat::Jpeg),
1440 "bmp" => Some(ImageFormat::Bmp),
1441 "tif" | "tiff" => Some(ImageFormat::Tiff),
1442 "gif" => Some(ImageFormat::Gif),
1443 "webp" => Some(ImageFormat::WebP),
1444 _ => None,
1445 }
1446}
1447
1448fn alpha_or_opaque(alpha: Option<u8>) -> u8 {
1450 alpha.unwrap_or(255)
1451}
1452
1453fn resolve_color(res: &DocResources, color: &CtColor, obj_alpha: Option<u8>) -> Rgba {
1457 let values: Vec<f64> = color
1458 .value
1459 .as_ref()
1460 .map(|v| v.as_slice().to_vec())
1461 .unwrap_or_default();
1462
1463 let cs_id = color.color_space.map(|c| c.value()).or(res.default_cs);
1464 let cs = cs_id.and_then(|id| res.color_spaces.get(&id));
1465 let bits = cs.and_then(|c| c.bits_per_component).unwrap_or(8);
1466 let max = ((1u64 << bits.min(16)) - 1) as f64;
1467 let cs_type = cs.map(|c| c.cs_type.as_str()).unwrap_or("RGB");
1468
1469 let norm = |i: usize| -> f64 { values.get(i).copied().unwrap_or(0.0) / max };
1470
1471 let (r, g, b) = if values.is_empty() {
1472 (0.0, 0.0, 0.0)
1473 } else {
1474 match cs_type {
1475 "Gray" => {
1476 let v = norm(0);
1477 (v, v, v)
1478 }
1479 "CMYK" => {
1480 let (c, m, y, k) = (norm(0), norm(1), norm(2), norm(3));
1481 (
1482 (1.0 - c) * (1.0 - k),
1483 (1.0 - m) * (1.0 - k),
1484 (1.0 - y) * (1.0 - k),
1485 )
1486 }
1487 _ => (norm(0), norm(1), norm(2)),
1489 }
1490 };
1491
1492 let color_alpha = color.alpha.unwrap_or(255) as f64;
1493 let obj_a = obj_alpha.unwrap_or(255) as f64;
1494 let a = (color_alpha * obj_a / 255.0).round() as u8;
1495
1496 [
1497 (r.clamp(0.0, 1.0) * 255.0).round() as u8,
1498 (g.clamp(0.0, 1.0) * 255.0).round() as u8,
1499 (b.clamp(0.0, 1.0) * 255.0).round() as u8,
1500 a,
1501 ]
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506 use super::*;
1507 use crate::model::graphics::CtColor;
1508 use crate::types::StId;
1509
1510 fn dp(id: u64, relative: Option<u64>) -> CtDrawParam {
1511 CtDrawParam {
1512 id: StId(id),
1513 relative: relative.map(StId),
1514 ..Default::default()
1515 }
1516 }
1517
1518 fn color(component: f64) -> CtColor {
1519 CtColor {
1520 value: Some(StArray(vec![component])),
1521 ..Default::default()
1522 }
1523 }
1524
1525 fn resources_with(params: Vec<CtDrawParam>) -> DocResources {
1526 let mut res = DocResources::default();
1527 for p in params {
1528 res.draw_params.insert(p.id.value(), p);
1529 }
1530 res
1531 }
1532
1533 fn resolve_for(res: &DocResources, id: u64) -> CtDrawParam {
1535 resolve_draw_param(res, Some(StRefId(id))).expect("draw param exists")
1536 }
1537
1538 #[test]
1541 fn draw_param_inherits_missing_attrs_via_relative() {
1542 let mut parent = dp(1, None);
1544 parent.fill_color = Some(color(0.5));
1545 parent.line_width = Some(2.0);
1546 let mut child = dp(2, Some(1));
1547 child.stroke_color = Some(color(0.9));
1548
1549 let res = resources_with(vec![parent, child]);
1550 let flat = resolve_for(&res, 2);
1551 assert!(flat.fill_color.is_some());
1553 assert_eq!(flat.line_width, Some(2.0));
1554 assert!(flat.stroke_color.is_some());
1555 }
1556
1557 #[test]
1559 fn draw_param_inherits_across_multiple_levels() {
1560 let mut grandparent = dp(1, None);
1561 grandparent.fill_color = Some(color(0.5));
1562 let parent = dp(2, Some(1));
1563 let parent_id = parent.id.value();
1564 assert_eq!(parent_id, 2);
1565 let child = dp(3, Some(2));
1566
1567 let res = resources_with(vec![grandparent, parent, child]);
1568 let flat = resolve_for(&res, 3);
1569 assert!(flat.fill_color.is_some());
1570 }
1571
1572 #[test]
1574 fn draw_param_relative_cycle_terminates() {
1575 let res = resources_with(vec![dp(1, Some(2)), dp(2, Some(1))]);
1576 let flat = resolve_for(&res, 2);
1577 assert_eq!(flat.id.value(), 2);
1578 }
1579
1580 #[test]
1583 fn effective_draw_param_falls_back_to_layer() {
1584 let mut layer = dp(4, None);
1585 layer.fill_color = Some(color(0.6));
1586 let res = resources_with(vec![layer.clone()]);
1587
1588 let eff = effective_draw_param(&res, None, Some(&layer)).expect("inherited param");
1590 assert!(eff.fill_color.is_some());
1591 }
1592
1593 #[test]
1595 fn effective_draw_param_object_overrides_layer() {
1596 let mut layer = dp(4, None);
1597 layer.fill_color = Some(color(0.6));
1598 let mut own = dp(2, None);
1599 own.fill_color = Some(color(0.1));
1600 own.line_width = None; let mut layer_with_lw = layer.clone();
1602 layer_with_lw.line_width = Some(3.0);
1603
1604 let res = resources_with(vec![own.clone(), layer_with_lw.clone()]);
1605 let eff =
1606 effective_draw_param(&res, Some(StRefId(2)), Some(&layer_with_lw)).expect("param");
1607 assert_eq!(
1609 eff.fill_color
1610 .as_ref()
1611 .and_then(|c| c.value.as_ref())
1612 .map(|v| v.as_slice()[0]),
1613 Some(0.1)
1614 );
1615 assert_eq!(eff.line_width, Some(3.0));
1617 }
1618
1619 #[test]
1623 fn arc_bulges_like_a_semicircle() {
1624 let cw = build_path("S 0 0 A 5 5 0 1 1 10 0").expect("path");
1627 let b = cw.bounds();
1628 assert!(
1629 b.top() < -4.5,
1630 "clockwise semicircle should bulge up (-y), got {}",
1631 b.top()
1632 );
1633 assert!(b.bottom() < 0.5, "stays on one side of the chord");
1634 assert!(
1635 (b.left()).abs() < 0.5 && (b.right() - 10.0).abs() < 0.5,
1636 "span ≈ diameter"
1637 );
1638 assert!((b.height() - 5.0).abs() < 0.5, "bulge ≈ radius");
1639
1640 let ccw = build_path("S 0 0 A 5 5 0 1 0 10 0").expect("path");
1642 let b2 = ccw.bounds();
1643 assert!(
1644 b2.bottom() > 4.5,
1645 "counter-clockwise should bulge down (+y), got {}",
1646 b2.bottom()
1647 );
1648 }
1649
1650 #[test]
1652 fn arc_with_zero_radius_degenerates_to_line() {
1653 let path = build_path("S 0 0 A 0 0 0 0 0 10 0").expect("path");
1654 let b = path.bounds();
1655 assert!(b.height() < 0.001, "degenerate arc must be flat");
1656 }
1657
1658 fn glyph_pt(cx: f64, cy: f64, scale: f64, char_dir: f64, fx: f64, fy: f64) -> (f64, f64) {
1660 let m = glyph_to_object(cx, cy, scale, scale, char_dir);
1661 (m.a * fx + m.c * fy + m.e, m.b * fx + m.d * fy + m.f)
1662 }
1663
1664 #[test]
1667 fn char_direction_rotates_glyph_clockwise() {
1668 let approx = |a: f64, b: f64| (a - b).abs() < 1e-9;
1669 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
1672 assert!(approx(x, 0.0) && approx(y, -1.0), "0° up→up: {x},{y}");
1673 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 90.0, 0.0, 1.0);
1675 assert!(approx(x, 1.0) && approx(y, 0.0), "90° up→right: {x},{y}");
1676 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 180.0, 0.0, 1.0);
1678 assert!(approx(x, 0.0) && approx(y, 1.0), "180° up→down: {x},{y}");
1679 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 270.0, 0.0, 1.0);
1681 assert!(approx(x, -1.0) && approx(y, 0.0), "270° up→left: {x},{y}");
1682 }
1683
1684 #[test]
1686 fn glyph_origin_translates_to_pen_position() {
1687 let (x, y) = glyph_pt(12.0, 34.0, 2.0, 90.0, 0.0, 0.0);
1688 assert!((x - 12.0).abs() < 1e-9 && (y - 34.0).abs() < 1e-9);
1689 }
1690
1691 use crate::model::graphics::{CtCgTransform, TextCode, TextObject};
1692 use crate::types::StArray;
1693
1694 fn text_code(x: Option<f64>, y: Option<f64>, dx: &str, text: &str) -> TextCode {
1695 TextCode {
1696 x,
1697 y,
1698 delta_x: (!dx.is_empty()).then(|| dx.to_string()),
1699 delta_y: None,
1700 text: Some(text.to_string()),
1701 }
1702 }
1703
1704 #[test]
1706 fn text_points_advance_by_delta_x() {
1707 let obj = TextObject {
1708 text_codes: vec![text_code(Some(0.0), Some(25.0), "10 10", "ABC")],
1709 ..Default::default()
1710 };
1711 let pts = text_code_points(&obj);
1712 assert_eq!(pts, vec![(0.0, 25.0), (10.0, 25.0), (20.0, 25.0)]);
1713 }
1714
1715 #[test]
1717 fn text_points_inherit_xy_from_previous_text_code() {
1718 let obj = TextObject {
1719 text_codes: vec![
1720 text_code(Some(5.0), Some(7.0), "", "A"),
1721 text_code(None, None, "", "B"),
1723 ],
1724 ..Default::default()
1725 };
1726 let pts = text_code_points(&obj);
1727 assert_eq!(pts, vec![(5.0, 7.0), (5.0, 7.0)]);
1728 }
1729
1730 #[test]
1732 fn place_glyphs_uses_cmap_without_transform() {
1733 let obj = TextObject {
1734 text_codes: vec![text_code(Some(0.0), Some(0.0), "10", "AB")],
1735 ..Default::default()
1736 };
1737 let placed = place_glyphs(&obj, |ch| Some(ttf_parser::GlyphId(ch as u16)));
1739 assert_eq!(placed.len(), 2);
1740 assert_eq!(placed[0].gid.0, b'A' as u16);
1741 assert_eq!(placed[0].origin, (0.0, 0.0));
1742 assert_eq!(placed[1].gid.0, b'B' as u16);
1743 assert_eq!(placed[1].origin, (10.0, 0.0));
1744 }
1745
1746 #[test]
1749 fn place_glyphs_applies_ligature_transform() {
1750 let obj = TextObject {
1751 text_codes: vec![text_code(Some(0.0), Some(0.0), "10", "fi")],
1752 cg_transforms: vec![CtCgTransform {
1753 code_position: 0,
1754 code_count: Some(2),
1755 glyph_count: Some(1),
1756 glyphs: Some(StArray(vec![192])),
1757 }],
1758 ..Default::default()
1759 };
1760 let placed = place_glyphs(&obj, |_| {
1761 panic!("CMAP must not be used inside transform range")
1762 });
1763 assert_eq!(placed.len(), 1);
1764 assert_eq!(placed[0].gid.0, 192);
1765 assert_eq!(placed[0].origin, (0.0, 0.0));
1766 }
1767
1768 #[test]
1770 fn place_glyphs_mixes_transform_and_cmap() {
1771 let obj = TextObject {
1772 text_codes: vec![text_code(Some(0.0), Some(0.0), "10 10", "fix")],
1774 cg_transforms: vec![CtCgTransform {
1775 code_position: 0,
1776 code_count: Some(2),
1777 glyph_count: Some(1),
1778 glyphs: Some(StArray(vec![192])),
1779 }],
1780 ..Default::default()
1781 };
1782 let placed = place_glyphs(&obj, |ch| Some(ttf_parser::GlyphId(ch as u16)));
1783 assert_eq!(placed.len(), 2);
1784 assert_eq!(placed[0].gid.0, 192);
1785 assert_eq!(placed[0].origin, (0.0, 0.0));
1786 assert_eq!(placed[1].gid.0, b'x' as u16);
1788 assert_eq!(placed[1].origin, (20.0, 0.0));
1789 }
1790
1791 #[test]
1794 fn build_path_handles_all_commands() {
1795 let p = build_path("M 0,0 L 10 0 Q 10 5 5 10 B 4 4 2 2 0 10 A 3 3 0 0 1 0 0 C");
1797 assert!(p.is_some());
1798 }
1799
1800 #[test]
1801 fn build_path_edge_cases() {
1802 assert!(build_path("L 1 1").is_none());
1804 assert!(build_path("M 0 0 L 1").is_none());
1806 assert!(build_path("M 0 0 Z 9 L 5 5").is_some());
1808 assert!(build_path("").is_none());
1810 }
1811
1812 fn cs(id: u64, ty: &str, bits: Option<u32>) -> CtColorSpace {
1815 CtColorSpace {
1816 id: StId(id),
1817 cs_type: ty.to_string(),
1818 bits_per_component: bits,
1819 profile: None,
1820 }
1821 }
1822
1823 fn res_with_cs(spaces: Vec<CtColorSpace>, default_cs: Option<u64>) -> DocResources {
1824 let mut res = DocResources {
1825 default_cs,
1826 ..Default::default()
1827 };
1828 for c in spaces {
1829 res.color_spaces.insert(c.id.value(), c);
1830 }
1831 res
1832 }
1833
1834 fn colored(values: Vec<f64>, cs_id: Option<u64>, alpha: Option<u8>) -> CtColor {
1835 CtColor {
1836 value: Some(StArray(values)),
1837 color_space: cs_id.map(StRefId),
1838 alpha,
1839 ..Default::default()
1840 }
1841 }
1842
1843 #[test]
1844 fn resolve_color_rgb_default_when_no_space() {
1845 let res = DocResources::default();
1847 let c = resolve_color(&res, &colored(vec![255.0, 0.0, 0.0], None, None), None);
1848 assert_eq!(c, [255, 0, 0, 255]);
1849 }
1850
1851 #[test]
1852 fn resolve_color_gray_and_bits() {
1853 let res = res_with_cs(vec![cs(1, "Gray", Some(4))], None);
1855 let c = resolve_color(&res, &colored(vec![15.0], Some(1), None), None);
1856 assert_eq!(c, [255, 255, 255, 255]);
1857 }
1858
1859 #[test]
1860 fn resolve_color_cmyk() {
1861 let res = res_with_cs(vec![cs(2, "CMYK", None)], None);
1863 let c = resolve_color(
1864 &res,
1865 &colored(vec![0.0, 0.0, 0.0, 255.0], Some(2), None),
1866 None,
1867 );
1868 assert_eq!(&c[..3], &[0, 0, 0]);
1869 }
1870
1871 #[test]
1872 fn resolve_color_uses_default_cs_and_alpha_multiplies() {
1873 let res = res_with_cs(vec![cs(7, "Gray", None)], Some(7));
1875 let c = resolve_color(&res, &colored(vec![255.0], None, Some(255)), Some(128));
1877 assert_eq!(c, [255, 255, 255, 128]);
1878 }
1879
1880 #[test]
1881 fn resolve_color_empty_values_is_black() {
1882 let res = DocResources::default();
1883 let c = resolve_color(
1884 &res,
1885 &CtColor {
1886 value: None,
1887 ..Default::default()
1888 },
1889 None,
1890 );
1891 assert_eq!(c, [0, 0, 0, 255]);
1892 }
1893
1894 #[test]
1897 fn mat_from_array_and_ops() {
1898 assert!(Mat::from_array(&[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]).is_some());
1899 assert!(Mat::from_array(&[1.0, 2.0]).is_none());
1900 let m = Mat::translate(10.0, 20.0).mul(Mat::scale(2.0, 2.0));
1902 assert!((m.a - 2.0).abs() < 1e-9 && (m.e - 10.0).abs() < 1e-9);
1903 let sk = Mat::identity().to_skia();
1904 assert_eq!(sk.sx, 1.0);
1905 let r = Mat::rotate(std::f64::consts::FRAC_PI_2);
1907 assert!(r.a.abs() < 1e-9);
1908 }
1909
1910 #[test]
1913 fn is_cjk_and_alpha_helpers() {
1914 assert!(is_cjk('字'));
1915 assert!(is_cjk(',')); assert!(!is_cjk('A'));
1917 assert_eq!(alpha_or_opaque(None), 255);
1918 assert_eq!(alpha_or_opaque(Some(10)), 10);
1919 }
1920
1921 #[test]
1922 fn image_format_mapping() {
1923 for (ext, _) in [
1924 ("png", ()),
1925 ("JPG", ()),
1926 ("jpeg", ()),
1927 ("bmp", ()),
1928 ("tiff", ()),
1929 ("tif", ()),
1930 ("gif", ()),
1931 ("webp", ()),
1932 ] {
1933 assert!(image_format_from_ext(ext).is_some(), "{ext}");
1934 }
1935 assert!(image_format_from_ext("xyz").is_none());
1936 assert!(format_from_path(Path::new("a/b.PNG")).is_some());
1937 assert!(format_from_path(Path::new("noext")).is_none());
1938 }
1939
1940 #[test]
1941 fn lookup_system_font_finds_something() {
1942 let db = system_fonts();
1944 let fam = db
1945 .faces()
1946 .next()
1947 .and_then(|f| f.families.first().map(|(n, _)| n.clone()));
1948 if let Some(fam) = fam {
1949 assert!(lookup_system_font(&fam, "", false, false).is_some());
1950 }
1951 let _ = lookup_system_font("宋体", "宋体", true, false);
1954 let _ = lookup_system_font("ZZ-No-Such-Font", "", false, true);
1955 let _ = lookup_system_font("", "", false, false);
1956 }
1957
1958 #[test]
1959 fn pixmap_image_roundtrip_and_encode() {
1960 let mut img = RgbaImage::new(2, 2);
1961 img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
1962 img.put_pixel(1, 1, image::Rgba([0, 255, 0, 128]));
1963 let pm = rgba_to_pixmap(&img).unwrap();
1964 let back = pixmap_to_image(&pm);
1965 assert_eq!(back.get_pixel(0, 0).0, [255, 0, 0, 255]);
1966 assert!(
1968 !encode_image(img.clone(), ImageFormat::Png)
1969 .unwrap()
1970 .is_empty()
1971 );
1972 assert!(!encode_image(img, ImageFormat::Jpeg).unwrap().is_empty());
1973 }
1974
1975 #[test]
1976 fn render_options_builders() {
1977 let o = RenderOptions::with_dpi(300.0).background(Some([1, 2, 3, 4]));
1978 assert_eq!(o.dpi, 300.0);
1979 assert_eq!(o.background, Some([1, 2, 3, 4]));
1980 }
1981}