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 .map(|a| a.physical_box)
391 .unwrap_or(doc.document.common_data.page_area.physical_box);
392
393 let scale = options.dpi / MM_PER_INCH;
394 let width_px = ((area.width * scale).round() as i64).max(1);
395 let height_px = ((area.height * scale).round() as i64).max(1);
396 if width_px > MAX_DIMENSION as i64 || height_px > MAX_DIMENSION as i64 {
397 return Err(OfdError::Render(format!(
398 "rendered size {width_px}x{height_px} exceeds limit {MAX_DIMENSION}"
399 )));
400 }
401
402 let mut pixmap = Pixmap::new(width_px as u32, height_px as u32)
403 .ok_or_else(|| OfdError::Render("failed to allocate pixmap".to_string()))?;
404 if let Some([r, g, b, a]) = options.background {
405 pixmap.fill(tiny_skia::Color::from_rgba8(r, g, b, a));
406 }
407
408 let page_to_device =
410 Mat::translate(-area.x * scale, -area.y * scale).mul(Mat::scale(scale, scale));
411
412 for tpl in &bg_templates {
414 render_page_object(&mut pixmap, &resources, page_to_device, tpl);
415 }
416 render_page_object(&mut pixmap, &resources, page_to_device, &page);
417 for tpl in &fg_templates {
418 render_page_object(&mut pixmap, &resources, page_to_device, tpl);
419 }
420
421 self.render_annotations(doc, &page_ref, &resources, page_to_device, &mut pixmap);
423
424 Ok(pixmap)
425 }
426
427 fn render_annotations(
431 &mut self,
432 doc: &LoadedDocument,
433 page_ref: &PageRef,
434 res: &DocResources,
435 page_to_device: Mat,
436 pixmap: &mut Pixmap,
437 ) {
438 let Some(loc) = doc.document.annotations.clone() else {
439 return;
440 };
441 let path = resolve_path(&doc.base, &loc);
442 let Ok(annotations) = self.package.parse::<crate::Annotations>(&path) else {
443 return;
444 };
445 let ann_dir = parent_dir(&path).to_string();
447 let page_id = page_ref.id.value();
448
449 for pg in &annotations.pages {
450 if pg.page_id.value() != page_id {
451 continue;
452 }
453 let file_path = resolve_path(&ann_dir, &pg.file_loc);
454 let Ok(page_annot) = self.package.parse::<crate::PageAnnot>(&file_path) else {
455 continue;
456 };
457 for annot in &page_annot.annots {
458 if annot.visible == Some(false) {
459 continue;
460 }
461 let Some(app) = &annot.appearance else {
462 continue;
463 };
464 let app_to_device =
466 page_to_device.mul(Mat::translate(app.boundary.x, app.boundary.y));
467 for obj in &app.objects {
468 render_block(pixmap, res, app_to_device, obj, None);
469 }
470 }
471 }
472 }
473
474 pub fn render_page_to_image(
476 &mut self,
477 doc: &LoadedDocument,
478 page_index: usize,
479 options: &RenderOptions,
480 ) -> Result<RgbaImage> {
481 let pixmap = self.render_page(doc, page_index, options)?;
482 Ok(pixmap_to_image(&pixmap))
483 }
484
485 pub fn render_page_to_bytes(
487 &mut self,
488 doc: &LoadedDocument,
489 page_index: usize,
490 options: &RenderOptions,
491 format: ImageFormat,
492 ) -> Result<Vec<u8>> {
493 let img = self.render_page_to_image(doc, page_index, options)?;
494 encode_image(img, format)
495 }
496
497 pub fn render_page_to_file<P: AsRef<Path>>(
501 &mut self,
502 doc: &LoadedDocument,
503 page_index: usize,
504 options: &RenderOptions,
505 path: P,
506 ) -> Result<()> {
507 let path = path.as_ref();
508 let format = format_from_path(path).ok_or_else(|| {
509 OfdError::Render(format!(
510 "cannot infer image format from path: {}",
511 path.display()
512 ))
513 })?;
514 let bytes = self.render_page_to_bytes(doc, page_index, options, format)?;
515 std::fs::write(path, bytes)?;
516 Ok(())
517 }
518
519 fn build_resources(&mut self, doc: &LoadedDocument) -> Result<DocResources> {
521 let mut res = DocResources {
522 default_cs: doc.document.common_data.default_cs.map(|id| id.value()),
523 ..Default::default()
524 };
525 let locs: Vec<StLoc> = doc
526 .public_res()
527 .iter()
528 .chain(doc.document_res().iter())
529 .cloned()
530 .collect();
531 for loc in &locs {
532 self.load_res_into(&doc.base, loc, &mut res);
533 }
534 Ok(res)
535 }
536
537 fn load_res_into(&mut self, base_dir: &str, loc: &StLoc, res: &mut DocResources) {
541 let res_path = resolve_path(base_dir, loc);
542 let Ok(parsed) = self.package.parse::<crate::Res>(&res_path) else {
543 return;
544 };
545 let res_dir = parent_dir(&res_path).to_string();
547 let data_base = resolve_path(&res_dir, &parsed.base_loc);
548
549 for cs in parsed.color_spaces() {
550 res.color_spaces.insert(cs.id.value(), cs.clone());
551 }
552 for dp in parsed.draw_params() {
553 res.draw_params.insert(dp.id.value(), dp.clone());
554 }
555 for font in parsed.fonts() {
556 let embedded = font
557 .font_file
558 .as_ref()
559 .map(|ff| resolve_path(&data_base, ff))
560 .and_then(|p| self.package.read(&p).ok());
561 let res_font = match embedded {
562 Some(data) => FontRes {
563 data: Some(data),
564 index: 0,
565 },
566 None => {
568 let (data, index) = lookup_system_font(
569 &font.font_name,
570 font.family_name.as_deref().unwrap_or(""),
571 font.bold.unwrap_or(false),
572 font.italic.unwrap_or(false),
573 )
574 .map(|(d, i)| (Some(d), i))
575 .unwrap_or((None, 0));
576 FontRes { data, index }
577 }
578 };
579 res.fonts.insert(font.id.value(), res_font);
580 }
581 for mm in parsed.multi_medias() {
582 let p = resolve_path(&data_base, &mm.media_file);
583 if let Ok(bytes) = self.package.read(&p) {
584 res.media.insert(mm.id.value(), bytes);
585 }
586 }
587 for vg in parsed.composite_graphic_units() {
588 res.vector_gs.insert(vg.id.value(), vg.clone());
589 }
590 }
591}
592
593fn render_page_object(
598 pixmap: &mut Pixmap,
599 res: &DocResources,
600 page_to_device: Mat,
601 page: &PageObject,
602) {
603 let Some(content) = &page.content else {
604 return;
605 };
606 let mut layers: Vec<_> = content.layers.iter().collect();
607 layers.sort_by_key(|l| match l.layer_type.as_deref() {
608 Some("Background") => 0,
609 Some("Foreground") => 2,
610 _ => 1, });
612 for layer in layers {
613 let layer_dp = resolve_draw_param(res, layer.draw_param);
615 for obj in &layer.objects {
616 render_block(pixmap, res, page_to_device, obj, layer_dp.as_ref());
617 }
618 }
619}
620
621fn render_block(
626 pixmap: &mut Pixmap,
627 res: &DocResources,
628 page_to_device: Mat,
629 block: &PageBlock,
630 inherited: Option<&CtDrawParam>,
631) {
632 match block {
633 PageBlock::Path(p) => render_path(pixmap, res, page_to_device, p, inherited),
634 PageBlock::Text(t) => render_text(pixmap, res, page_to_device, t, inherited),
635 PageBlock::Image(i) => render_image(pixmap, res, page_to_device, i),
636 PageBlock::Block(g) => {
637 for obj in &g.objects {
638 render_block(pixmap, res, page_to_device, obj, inherited);
639 }
640 }
641 PageBlock::Composite(c) => render_composite(pixmap, res, page_to_device, c, 0, inherited),
642 }
643}
644
645const MAX_COMPOSITE_DEPTH: u32 = 16;
647
648fn render_composite(
654 pixmap: &mut Pixmap,
655 res: &DocResources,
656 page_to_device: Mat,
657 obj: &CompositeObject,
658 depth: u32,
659 inherited: Option<&CtDrawParam>,
660) {
661 if depth >= MAX_COMPOSITE_DEPTH {
662 return;
663 }
664 let Some(vg) = res.vector_gs.get(&obj.resource_id.value()) else {
665 return;
666 };
667 let Some(content) = &vg.content else {
668 return;
669 };
670 let inner_to_device = object_to_device(
672 page_to_device,
673 &obj.boundary,
674 obj.ctm.as_ref().map(|a| a.as_slice()),
675 );
676 for block in &content.objects {
677 render_block_at_depth(pixmap, res, inner_to_device, block, depth + 1, inherited);
678 }
679}
680
681fn render_block_at_depth(
683 pixmap: &mut Pixmap,
684 res: &DocResources,
685 page_to_device: Mat,
686 block: &PageBlock,
687 depth: u32,
688 inherited: Option<&CtDrawParam>,
689) {
690 match block {
691 PageBlock::Composite(c) => {
692 render_composite(pixmap, res, page_to_device, c, depth, inherited)
693 }
694 PageBlock::Block(g) => {
695 for obj in &g.objects {
696 render_block_at_depth(pixmap, res, page_to_device, obj, depth, inherited);
697 }
698 }
699 _ => render_block(pixmap, res, page_to_device, block, inherited),
700 }
701}
702
703fn object_to_device(page_to_device: Mat, boundary: &StBox, ctm: Option<&[f64]>) -> Mat {
705 let m = ctm.and_then(Mat::from_array).unwrap_or_else(Mat::identity);
706 page_to_device
707 .mul(Mat::translate(boundary.x, boundary.y))
708 .mul(m)
709}
710
711fn resolve_draw_param(res: &DocResources, id: Option<StRefId>) -> Option<CtDrawParam> {
717 let mut cur = res.draw_params.get(&id?.value())?.clone();
718 let mut visited = vec![cur.id.value()];
719 while let Some(rid) = cur.relative.map(|r| r.value()) {
720 if visited.contains(&rid) {
721 break;
722 }
723 let Some(parent) = res.draw_params.get(&rid) else {
724 break;
725 };
726 visited.push(rid);
727 fill_missing_draw_param(&mut cur, parent);
728 cur.relative = parent.relative;
730 }
731 Some(cur)
732}
733
734fn fill_missing_draw_param(dst: &mut CtDrawParam, src: &CtDrawParam) {
737 dst.line_width = dst.line_width.or(src.line_width);
738 dst.join = dst.join.take().or_else(|| src.join.clone());
739 dst.cap = dst.cap.take().or_else(|| src.cap.clone());
740 dst.miter_limit = dst.miter_limit.or(src.miter_limit);
741 dst.dash_offset = dst.dash_offset.or(src.dash_offset);
742 dst.dash_pattern = dst.dash_pattern.take().or_else(|| src.dash_pattern.clone());
743 dst.fill_color = dst.fill_color.take().or_else(|| src.fill_color.clone());
744 dst.stroke_color = dst.stroke_color.take().or_else(|| src.stroke_color.clone());
745}
746
747fn effective_draw_param(
753 res: &DocResources,
754 id: Option<StRefId>,
755 inherited: Option<&CtDrawParam>,
756) -> Option<CtDrawParam> {
757 match (resolve_draw_param(res, id), inherited) {
758 (Some(mut own), Some(parent)) => {
759 fill_missing_draw_param(&mut own, parent);
760 Some(own)
761 }
762 (Some(own), None) => Some(own),
763 (None, Some(parent)) => Some(parent.clone()),
764 (None, None) => None,
765 }
766}
767
768fn render_path(
770 pixmap: &mut Pixmap,
771 res: &DocResources,
772 page_to_device: Mat,
773 obj: &PathObject,
774 inherited: Option<&CtDrawParam>,
775) {
776 let Some(data) = &obj.abbreviated_data else {
777 return;
778 };
779 let Some(path) = build_path(data) else {
780 return;
781 };
782 let transform = object_to_device(
783 page_to_device,
784 &obj.boundary,
785 obj.ctm.as_ref().map(|a| a.as_slice()),
786 )
787 .to_skia();
788 let dp = effective_draw_param(res, obj.draw_param, inherited);
789 let dp = dp.as_ref();
790
791 let fill = obj.fill.unwrap_or(false);
792 let stroke = obj.stroke.unwrap_or(true);
793 let obj_alpha = obj.alpha;
794
795 if fill {
796 let color = obj
797 .fill_color
798 .as_ref()
799 .or_else(|| dp.and_then(|d| d.fill_color.as_ref()))
800 .map(|c| resolve_color(res, c, obj_alpha))
801 .unwrap_or([0, 0, 0, alpha_or_opaque(obj_alpha)]);
802 let mut paint = Paint::default();
803 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
804 paint.anti_alias = true;
805 let rule = match obj.rule.as_deref() {
806 Some("Even-Odd") | Some("EvenOdd") => FillRule::EvenOdd,
807 _ => FillRule::Winding,
808 };
809 pixmap.fill_path(&path, &paint, rule, transform, None);
810 }
811
812 if stroke {
813 let color = obj
814 .stroke_color
815 .as_ref()
816 .or_else(|| dp.and_then(|d| d.stroke_color.as_ref()))
817 .map(|c| resolve_color(res, c, obj_alpha))
818 .unwrap_or([0, 0, 0, alpha_or_opaque(obj_alpha)]);
819 let width = obj
820 .line_width
821 .or_else(|| dp.and_then(|d| d.line_width))
822 .unwrap_or(0.353);
823 let mut paint = Paint::default();
824 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
825 paint.anti_alias = true;
826 let cap = obj
828 .cap
829 .as_deref()
830 .or_else(|| dp.and_then(|d| d.cap.as_deref()));
831 let join = obj
832 .join
833 .as_deref()
834 .or_else(|| dp.and_then(|d| d.join.as_deref()));
835 let miter = obj
836 .miter_limit
837 .or_else(|| dp.and_then(|d| d.miter_limit))
838 .unwrap_or(4.234);
839 let dash = obj
841 .dash_pattern
842 .as_ref()
843 .or_else(|| dp.and_then(|d| d.dash_pattern.as_ref()))
844 .map(|p| p.as_slice().iter().map(|&v| v as f32).collect::<Vec<_>>())
845 .filter(|p| p.len() >= 2 && p.iter().any(|&v| v > 0.0))
846 .and_then(|p| {
847 let off = obj
848 .dash_offset
849 .or_else(|| dp.and_then(|d| d.dash_offset))
850 .unwrap_or(0.0) as f32;
851 tiny_skia::StrokeDash::new(p, off)
852 });
853 let stroke_style = Stroke {
854 width: width.max(f64::MIN_POSITIVE) as f32,
855 line_cap: match cap {
856 Some("Round") => tiny_skia::LineCap::Round,
857 Some("Square") => tiny_skia::LineCap::Square,
858 _ => tiny_skia::LineCap::Butt,
859 },
860 line_join: match join {
861 Some("Round") => tiny_skia::LineJoin::Round,
862 Some("Bevel") => tiny_skia::LineJoin::Bevel,
863 _ => tiny_skia::LineJoin::Miter,
864 },
865 miter_limit: miter as f32,
866 dash,
867 };
868 pixmap.stroke_path(&path, &paint, &stroke_style, transform, None);
869 }
870}
871
872fn build_path(data: &str) -> Option<tiny_skia::Path> {
877 let normalized = data.replace(',', " ");
878 let mut tokens = normalized.split_whitespace().peekable();
879 let mut pb = PathBuilder::new();
880 let mut started = false;
881 let mut cur = (0.0_f64, 0.0_f64);
883 let mut start = (0.0_f64, 0.0_f64);
884
885 fn take(
887 tokens: &mut std::iter::Peekable<std::str::SplitWhitespace>,
888 n: usize,
889 ) -> Option<Vec<f64>> {
890 let mut v = Vec::with_capacity(n);
891 for _ in 0..n {
892 let t = tokens.next()?;
893 v.push(t.parse::<f64>().ok()?);
894 }
895 Some(v)
896 }
897
898 while let Some(tok) = tokens.next() {
899 match tok {
900 "S" | "M" => {
901 if let Some(v) = take(&mut tokens, 2) {
902 pb.move_to(v[0] as f32, v[1] as f32);
903 cur = (v[0], v[1]);
904 start = cur;
905 started = true;
906 }
907 }
908 "L" => {
909 if started && let Some(v) = take(&mut tokens, 2) {
910 pb.line_to(v[0] as f32, v[1] as f32);
911 cur = (v[0], v[1]);
912 }
913 }
914 "Q" => {
915 if started && let Some(v) = take(&mut tokens, 4) {
916 pb.quad_to(v[0] as f32, v[1] as f32, v[2] as f32, v[3] as f32);
917 cur = (v[2], v[3]);
918 }
919 }
920 "B" => {
921 if started && let Some(v) = take(&mut tokens, 6) {
922 pb.cubic_to(
923 v[0] as f32,
924 v[1] as f32,
925 v[2] as f32,
926 v[3] as f32,
927 v[4] as f32,
928 v[5] as f32,
929 );
930 cur = (v[4], v[5]);
931 }
932 }
933 "A" => {
934 if started && let Some(v) = take(&mut tokens, 7) {
936 let end = (v[5], v[6]);
937 append_arc(
938 &mut pb,
939 cur,
940 v[0],
941 v[1],
942 v[2],
943 v[3] != 0.0,
944 v[4] != 0.0,
945 end,
946 );
947 cur = end;
948 }
949 }
950 "C" => {
951 if started {
952 pb.close();
953 cur = start;
954 }
955 }
956 _ => {}
957 }
958 }
959
960 pb.finish()
961}
962
963#[allow(clippy::too_many_arguments)]
971fn append_arc(
972 pb: &mut PathBuilder,
973 start: (f64, f64),
974 rx: f64,
975 ry: f64,
976 angle_deg: f64,
977 large_arc: bool,
978 sweep: bool,
979 end: (f64, f64),
980) {
981 let (x1, y1) = start;
982 let (x2, y2) = end;
983
984 let mut rx = rx.abs();
986 let mut ry = ry.abs();
987 if rx == 0.0 || ry == 0.0 || (x1 == x2 && y1 == y2) {
988 pb.line_to(x2 as f32, y2 as f32);
989 return;
990 }
991
992 let phi = (angle_deg % 360.0).to_radians();
994 let (sin_p, cos_p) = phi.sin_cos();
995
996 let dx = (x1 - x2) / 2.0;
998 let dy = (y1 - y2) / 2.0;
999 let x1p = cos_p * dx + sin_p * dy;
1000 let y1p = -sin_p * dx + cos_p * dy;
1001
1002 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
1004 if lambda > 1.0 {
1005 let s = lambda.sqrt();
1006 rx *= s;
1007 ry *= s;
1008 }
1009
1010 let num = (rx * rx) * (ry * ry) - (rx * rx) * (y1p * y1p) - (ry * ry) * (x1p * x1p);
1012 let den = (rx * rx) * (y1p * y1p) + (ry * ry) * (x1p * x1p);
1013 let mut coef = if den > 0.0 {
1014 (num / den).max(0.0).sqrt()
1015 } else {
1016 0.0
1017 };
1018 if large_arc == sweep {
1019 coef = -coef;
1020 }
1021 let cxp = coef * (rx * y1p) / ry;
1022 let cyp = -coef * (ry * x1p) / rx;
1023
1024 let cx = cos_p * cxp - sin_p * cyp + (x1 + x2) / 2.0;
1026 let cy = sin_p * cxp + cos_p * cyp + (y1 + y2) / 2.0;
1027
1028 let ux = (x1p - cxp) / rx;
1030 let uy = (y1p - cyp) / ry;
1031 let vx = (-x1p - cxp) / rx;
1032 let vy = (-y1p - cyp) / ry;
1033 let angle = |ux: f64, uy: f64, vx: f64, vy: f64| -> f64 {
1034 let dot = ux * vx + uy * vy;
1035 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
1036 let mut a = (dot / len).clamp(-1.0, 1.0).acos();
1037 if ux * vy - uy * vx < 0.0 {
1038 a = -a;
1039 }
1040 a
1041 };
1042 let theta1 = angle(1.0, 0.0, ux, uy);
1043 let mut dtheta = angle(ux, uy, vx, vy);
1044 if !sweep && dtheta > 0.0 {
1045 dtheta -= 2.0 * std::f64::consts::PI;
1046 } else if sweep && dtheta < 0.0 {
1047 dtheta += 2.0 * std::f64::consts::PI;
1048 }
1049
1050 let segments = (dtheta.abs() / (std::f64::consts::PI / 2.0))
1052 .ceil()
1053 .max(1.0) as usize;
1054 let delta = dtheta / segments as f64;
1055 let t = (4.0 / 3.0) * (delta / 4.0).tan();
1056 let mut th = theta1;
1057 let point = |th: f64| -> (f64, f64) {
1059 let (s, c) = th.sin_cos();
1060 let ex = rx * c;
1061 let ey = ry * s;
1062 (cx + cos_p * ex - sin_p * ey, cy + sin_p * ex + cos_p * ey)
1063 };
1064 let deriv = |th: f64| -> (f64, f64) {
1065 let (s, c) = th.sin_cos();
1066 let ex = -rx * s;
1067 let ey = ry * c;
1068 (cos_p * ex - sin_p * ey, sin_p * ex + cos_p * ey)
1069 };
1070 for _ in 0..segments {
1071 let th2 = th + delta;
1072 let (px1, py1) = point(th);
1073 let (px2, py2) = point(th2);
1074 let (d1x, d1y) = deriv(th);
1075 let (d2x, d2y) = deriv(th2);
1076 let c1 = (px1 + t * d1x, py1 + t * d1y);
1077 let c2 = (px2 - t * d2x, py2 - t * d2y);
1078 pb.cubic_to(
1079 c1.0 as f32,
1080 c1.1 as f32,
1081 c2.0 as f32,
1082 c2.1 as f32,
1083 px2 as f32,
1084 py2 as f32,
1085 );
1086 th = th2;
1087 }
1088}
1089
1090struct PlacedGlyph {
1092 gid: ttf_parser::GlyphId,
1093 origin: (f64, f64),
1095}
1096
1097fn render_text(
1103 pixmap: &mut Pixmap,
1104 res: &DocResources,
1105 page_to_device: Mat,
1106 obj: &TextObject,
1107 inherited: Option<&CtDrawParam>,
1108) {
1109 let Some(font) = res.fonts.get(&obj.font.value()) else {
1110 return;
1111 };
1112 let Some(data) = &font.data else {
1113 return;
1115 };
1116 let Ok(face) = ttf_parser::Face::parse(data, font.index) else {
1117 return;
1118 };
1119 let units_per_em = face.units_per_em() as f64;
1120 if units_per_em <= 0.0 {
1121 return;
1122 }
1123
1124 let dp = effective_draw_param(res, obj.draw_param, inherited);
1125 let dp = dp.as_ref();
1126 let fill = obj.fill.unwrap_or(true);
1128 let stroke = obj.stroke.unwrap_or(false);
1129 if !fill && !stroke {
1130 return;
1131 }
1132
1133 let h_scale = obj.h_scale.unwrap_or(1.0);
1134 let scale_x = obj.size / units_per_em * h_scale;
1135 let scale_y = obj.size / units_per_em;
1136 let char_dir = obj.char_direction.unwrap_or(0) as f64;
1138
1139 let transform = object_to_device(
1140 page_to_device,
1141 &obj.boundary,
1142 obj.ctm.as_ref().map(|a| a.as_slice()),
1143 )
1144 .to_skia();
1145
1146 let placed = place_glyphs(obj, |ch| face.glyph_index(ch));
1147
1148 let mut pb = PathBuilder::new();
1149 for g in &placed {
1150 let glyph_mat = glyph_to_object(g.origin.0, g.origin.1, scale_x, scale_y, char_dir);
1153 let mut outliner = Outliner {
1154 pb: &mut pb,
1155 m: glyph_mat,
1156 };
1157 face.outline_glyph(g.gid, &mut outliner);
1158 }
1159
1160 let Some(path) = pb.finish() else {
1161 return;
1162 };
1163
1164 if fill {
1165 let color = obj
1166 .fill_color
1167 .as_ref()
1168 .or_else(|| dp.and_then(|d| d.fill_color.as_ref()))
1169 .map(|c| resolve_color(res, c, obj.alpha))
1170 .unwrap_or([0, 0, 0, alpha_or_opaque(obj.alpha)]);
1171 let mut paint = Paint::default();
1172 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
1173 paint.anti_alias = true;
1174 pixmap.fill_path(&path, &paint, FillRule::Winding, transform, None);
1175 }
1176
1177 if stroke {
1178 let Some(color) = obj
1180 .stroke_color
1181 .as_ref()
1182 .or_else(|| dp.and_then(|d| d.stroke_color.as_ref()))
1183 .map(|c| resolve_color(res, c, obj.alpha))
1184 .filter(|c| c[3] > 0)
1185 else {
1186 return;
1187 };
1188 let mut paint = Paint::default();
1189 paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
1190 paint.anti_alias = true;
1191 let width = dp.and_then(|d| d.line_width).unwrap_or(0.353);
1193 let stroke_style = Stroke {
1194 width: width.max(f64::MIN_POSITIVE) as f32,
1195 ..Default::default()
1196 };
1197 pixmap.stroke_path(&path, &paint, &stroke_style, transform, None);
1198 }
1199}
1200
1201fn text_code_points(obj: &TextObject) -> Vec<(f64, f64)> {
1206 let mut points: Vec<(f64, f64)> = Vec::new();
1207 let mut inherited_x = 0.0_f64;
1208 let mut inherited_y = 0.0_f64;
1209 for tc in &obj.text_codes {
1210 let Some(text) = &tc.text else { continue };
1211 let start_x = tc.x.unwrap_or(inherited_x);
1212 let start_y = tc.y.unwrap_or(inherited_y);
1213 inherited_x = start_x;
1214 inherited_y = start_y;
1215 let dx = tc.delta_x.as_deref().map(parse_deltas).unwrap_or_default();
1216 let dy = tc.delta_y.as_deref().map(parse_deltas).unwrap_or_default();
1217 let (mut cx, mut cy) = (start_x, start_y);
1218 for (i, _) in text.chars().enumerate() {
1219 if i > 0 {
1220 cx += dx
1221 .get(i - 1)
1222 .copied()
1223 .unwrap_or_else(|| dx.last().copied().unwrap_or(0.0));
1224 cy += dy
1225 .get(i - 1)
1226 .copied()
1227 .unwrap_or_else(|| dy.last().copied().unwrap_or(0.0));
1228 }
1229 points.push((cx, cy));
1230 }
1231 }
1232 points
1233}
1234
1235fn place_glyphs(
1241 obj: &TextObject,
1242 cmap: impl Fn(char) -> Option<ttf_parser::GlyphId>,
1243) -> Vec<PlacedGlyph> {
1244 let points = text_code_points(obj);
1245 let chars: Vec<char> = obj
1246 .text_codes
1247 .iter()
1248 .filter_map(|tc| tc.text.as_deref())
1249 .flat_map(|t| t.chars())
1250 .collect();
1251
1252 let transforms: HashMap<usize, &CtCgTransform> = obj
1254 .cg_transforms
1255 .iter()
1256 .filter(|t| t.code_position >= 0)
1257 .map(|t| (t.code_position as usize, t))
1258 .collect();
1259
1260 let mut out = Vec::with_capacity(chars.len());
1261 let mut i = 0;
1262 while i < chars.len() {
1263 if let Some(t) = transforms.get(&i) {
1264 let code_count = t.code_count.unwrap_or(1).max(1) as usize;
1266 if let Some(glyphs) = &t.glyphs {
1267 for (j, &g) in glyphs.as_slice().iter().enumerate() {
1268 let pi = i + j.min(code_count.saturating_sub(1));
1271 if let Some(&origin) = points.get(pi) {
1272 out.push(PlacedGlyph {
1273 gid: ttf_parser::GlyphId(g as u16),
1274 origin,
1275 });
1276 }
1277 }
1278 }
1279 i += code_count;
1280 } else {
1281 if let Some(gid) = cmap(chars[i])
1283 && let Some(&origin) = points.get(i)
1284 {
1285 out.push(PlacedGlyph { gid, origin });
1286 }
1287 i += 1;
1288 }
1289 }
1290 out
1291}
1292
1293fn glyph_to_object(cx: f64, cy: f64, scale_x: f64, scale_y: f64, char_dir_deg: f64) -> Mat {
1299 Mat::translate(cx, cy)
1300 .mul(Mat::rotate(char_dir_deg.to_radians()))
1301 .mul(Mat::scale(scale_x, -scale_y))
1302}
1303
1304struct Outliner<'a> {
1306 pb: &'a mut PathBuilder,
1307 m: Mat,
1308}
1309
1310impl Outliner<'_> {
1311 fn map(&self, x: f32, y: f32) -> (f32, f32) {
1313 let (xf, yf) = (x as f64, y as f64);
1314 (
1315 (self.m.a * xf + self.m.c * yf + self.m.e) as f32,
1316 (self.m.b * xf + self.m.d * yf + self.m.f) as f32,
1317 )
1318 }
1319}
1320
1321impl ttf_parser::OutlineBuilder for Outliner<'_> {
1322 fn move_to(&mut self, x: f32, y: f32) {
1323 let (px, py) = self.map(x, y);
1324 self.pb.move_to(px, py);
1325 }
1326 fn line_to(&mut self, x: f32, y: f32) {
1327 let (px, py) = self.map(x, y);
1328 self.pb.line_to(px, py);
1329 }
1330 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1331 let (cx1, cy1) = self.map(x1, y1);
1332 let (px, py) = self.map(x, y);
1333 self.pb.quad_to(cx1, cy1, px, py);
1334 }
1335 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1336 let (cx1, cy1) = self.map(x1, y1);
1337 let (cx2, cy2) = self.map(x2, y2);
1338 let (px, py) = self.map(x, y);
1339 self.pb.cubic_to(cx1, cy1, cx2, cy2, px, py);
1340 }
1341 fn close(&mut self) {
1342 self.pb.close();
1343 }
1344}
1345
1346fn render_image(pixmap: &mut Pixmap, res: &DocResources, page_to_device: Mat, obj: &ImageObject) {
1348 let Some(bytes) = res.media.get(&obj.resource_id.value()) else {
1349 return;
1350 };
1351 let Ok(decoded) = image::load_from_memory(bytes) else {
1352 return;
1353 };
1354 let rgba = decoded.to_rgba8();
1355 let (iw, ih) = (rgba.width(), rgba.height());
1356 if iw == 0 || ih == 0 {
1357 return;
1358 }
1359 let Some(src) = rgba_to_pixmap(&rgba) else {
1360 return;
1361 };
1362
1363 let unit_obj = obj
1366 .ctm
1367 .as_ref()
1368 .and_then(|a| Mat::from_array(a.as_slice()))
1369 .unwrap_or_else(|| Mat::scale(obj.boundary.width, obj.boundary.height));
1370 let unit_to_device = page_to_device
1371 .mul(Mat::translate(obj.boundary.x, obj.boundary.y))
1372 .mul(unit_obj);
1373 let pixel_to_device = unit_to_device.mul(Mat::scale(1.0 / iw as f64, 1.0 / ih as f64));
1375
1376 let opacity = obj.alpha.map(|a| a as f32 / 255.0).unwrap_or(1.0);
1377 let paint = PixmapPaint {
1378 opacity,
1379 ..Default::default()
1380 };
1381 pixmap.draw_pixmap(0, 0, src.as_ref(), &paint, pixel_to_device.to_skia(), None);
1382}
1383
1384fn rgba_to_pixmap(img: &RgbaImage) -> Option<Pixmap> {
1386 let mut pm = Pixmap::new(img.width(), img.height())?;
1387 let dst = pm.data_mut();
1388 for (i, px) in img.pixels().enumerate() {
1389 let [r, g, b, a] = px.0;
1390 let af = a as u16;
1391 dst[i * 4] = (r as u16 * af / 255) as u8;
1392 dst[i * 4 + 1] = (g as u16 * af / 255) as u8;
1393 dst[i * 4 + 2] = (b as u16 * af / 255) as u8;
1394 dst[i * 4 + 3] = a;
1395 }
1396 Some(pm)
1397}
1398
1399fn pixmap_to_image(pixmap: &Pixmap) -> RgbaImage {
1401 let (w, h) = (pixmap.width(), pixmap.height());
1402 let mut out = RgbaImage::new(w, h);
1403 for (i, px) in pixmap.pixels().iter().enumerate() {
1404 let c = px.demultiply();
1405 let x = (i as u32) % w;
1406 let y = (i as u32) / w;
1407 out.put_pixel(x, y, image::Rgba([c.red(), c.green(), c.blue(), c.alpha()]));
1408 }
1409 out
1410}
1411
1412fn encode_image(img: RgbaImage, format: ImageFormat) -> Result<Vec<u8>> {
1414 let mut buf = Cursor::new(Vec::new());
1415 match format {
1416 ImageFormat::Jpeg => {
1417 DynamicImage::ImageRgba8(img)
1418 .to_rgb8()
1419 .write_to(&mut buf, format)?;
1420 }
1421 _ => {
1422 img.write_to(&mut buf, format)?;
1423 }
1424 }
1425 Ok(buf.into_inner())
1426}
1427
1428fn format_from_path(path: &Path) -> Option<ImageFormat> {
1430 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
1431 image_format_from_ext(&ext)
1432}
1433
1434pub fn image_format_from_ext(ext: &str) -> Option<ImageFormat> {
1436 match ext.to_ascii_lowercase().as_str() {
1437 "png" => Some(ImageFormat::Png),
1438 "jpg" | "jpeg" => Some(ImageFormat::Jpeg),
1439 "bmp" => Some(ImageFormat::Bmp),
1440 "tif" | "tiff" => Some(ImageFormat::Tiff),
1441 "gif" => Some(ImageFormat::Gif),
1442 "webp" => Some(ImageFormat::WebP),
1443 _ => None,
1444 }
1445}
1446
1447fn alpha_or_opaque(alpha: Option<u8>) -> u8 {
1449 alpha.unwrap_or(255)
1450}
1451
1452fn resolve_color(res: &DocResources, color: &CtColor, obj_alpha: Option<u8>) -> Rgba {
1456 let values: Vec<f64> = color
1457 .value
1458 .as_ref()
1459 .map(|v| v.as_slice().to_vec())
1460 .unwrap_or_default();
1461
1462 let cs_id = color.color_space.map(|c| c.value()).or(res.default_cs);
1463 let cs = cs_id.and_then(|id| res.color_spaces.get(&id));
1464 let bits = cs.and_then(|c| c.bits_per_component).unwrap_or(8);
1465 let max = ((1u64 << bits.min(16)) - 1) as f64;
1466 let cs_type = cs.map(|c| c.cs_type.as_str()).unwrap_or("RGB");
1467
1468 let norm = |i: usize| -> f64 { values.get(i).copied().unwrap_or(0.0) / max };
1469
1470 let (r, g, b) = if values.is_empty() {
1471 (0.0, 0.0, 0.0)
1472 } else {
1473 match cs_type {
1474 "Gray" => {
1475 let v = norm(0);
1476 (v, v, v)
1477 }
1478 "CMYK" => {
1479 let (c, m, y, k) = (norm(0), norm(1), norm(2), norm(3));
1480 (
1481 (1.0 - c) * (1.0 - k),
1482 (1.0 - m) * (1.0 - k),
1483 (1.0 - y) * (1.0 - k),
1484 )
1485 }
1486 _ => (norm(0), norm(1), norm(2)),
1488 }
1489 };
1490
1491 let color_alpha = color.alpha.unwrap_or(255) as f64;
1492 let obj_a = obj_alpha.unwrap_or(255) as f64;
1493 let a = (color_alpha * obj_a / 255.0).round() as u8;
1494
1495 [
1496 (r.clamp(0.0, 1.0) * 255.0).round() as u8,
1497 (g.clamp(0.0, 1.0) * 255.0).round() as u8,
1498 (b.clamp(0.0, 1.0) * 255.0).round() as u8,
1499 a,
1500 ]
1501}
1502
1503#[cfg(test)]
1504mod tests {
1505 use super::*;
1506 use crate::model::graphics::CtColor;
1507 use crate::types::StId;
1508
1509 fn dp(id: u64, relative: Option<u64>) -> CtDrawParam {
1510 CtDrawParam {
1511 id: StId(id),
1512 relative: relative.map(StId),
1513 ..Default::default()
1514 }
1515 }
1516
1517 fn color(component: f64) -> CtColor {
1518 CtColor {
1519 value: Some(StArray(vec![component])),
1520 ..Default::default()
1521 }
1522 }
1523
1524 fn resources_with(params: Vec<CtDrawParam>) -> DocResources {
1525 let mut res = DocResources::default();
1526 for p in params {
1527 res.draw_params.insert(p.id.value(), p);
1528 }
1529 res
1530 }
1531
1532 fn resolve_for(res: &DocResources, id: u64) -> CtDrawParam {
1534 resolve_draw_param(res, Some(StRefId(id))).expect("draw param exists")
1535 }
1536
1537 #[test]
1540 fn draw_param_inherits_missing_attrs_via_relative() {
1541 let mut parent = dp(1, None);
1543 parent.fill_color = Some(color(0.5));
1544 parent.line_width = Some(2.0);
1545 let mut child = dp(2, Some(1));
1546 child.stroke_color = Some(color(0.9));
1547
1548 let res = resources_with(vec![parent, child]);
1549 let flat = resolve_for(&res, 2);
1550 assert!(flat.fill_color.is_some());
1552 assert_eq!(flat.line_width, Some(2.0));
1553 assert!(flat.stroke_color.is_some());
1554 }
1555
1556 #[test]
1558 fn draw_param_inherits_across_multiple_levels() {
1559 let mut grandparent = dp(1, None);
1560 grandparent.fill_color = Some(color(0.5));
1561 let parent = dp(2, Some(1));
1562 let parent_id = parent.id.value();
1563 assert_eq!(parent_id, 2);
1564 let child = dp(3, Some(2));
1565
1566 let res = resources_with(vec![grandparent, parent, child]);
1567 let flat = resolve_for(&res, 3);
1568 assert!(flat.fill_color.is_some());
1569 }
1570
1571 #[test]
1573 fn draw_param_relative_cycle_terminates() {
1574 let res = resources_with(vec![dp(1, Some(2)), dp(2, Some(1))]);
1575 let flat = resolve_for(&res, 2);
1576 assert_eq!(flat.id.value(), 2);
1577 }
1578
1579 #[test]
1582 fn effective_draw_param_falls_back_to_layer() {
1583 let mut layer = dp(4, None);
1584 layer.fill_color = Some(color(0.6));
1585 let res = resources_with(vec![layer.clone()]);
1586
1587 let eff = effective_draw_param(&res, None, Some(&layer)).expect("inherited param");
1589 assert!(eff.fill_color.is_some());
1590 }
1591
1592 #[test]
1594 fn effective_draw_param_object_overrides_layer() {
1595 let mut layer = dp(4, None);
1596 layer.fill_color = Some(color(0.6));
1597 let mut own = dp(2, None);
1598 own.fill_color = Some(color(0.1));
1599 own.line_width = None; let mut layer_with_lw = layer.clone();
1601 layer_with_lw.line_width = Some(3.0);
1602
1603 let res = resources_with(vec![own.clone(), layer_with_lw.clone()]);
1604 let eff =
1605 effective_draw_param(&res, Some(StRefId(2)), Some(&layer_with_lw)).expect("param");
1606 assert_eq!(
1608 eff.fill_color
1609 .as_ref()
1610 .and_then(|c| c.value.as_ref())
1611 .map(|v| v.as_slice()[0]),
1612 Some(0.1)
1613 );
1614 assert_eq!(eff.line_width, Some(3.0));
1616 }
1617
1618 #[test]
1622 fn arc_bulges_like_a_semicircle() {
1623 let cw = build_path("S 0 0 A 5 5 0 1 1 10 0").expect("path");
1626 let b = cw.bounds();
1627 assert!(
1628 b.top() < -4.5,
1629 "clockwise semicircle should bulge up (-y), got {}",
1630 b.top()
1631 );
1632 assert!(b.bottom() < 0.5, "stays on one side of the chord");
1633 assert!(
1634 (b.left()).abs() < 0.5 && (b.right() - 10.0).abs() < 0.5,
1635 "span ≈ diameter"
1636 );
1637 assert!((b.height() - 5.0).abs() < 0.5, "bulge ≈ radius");
1638
1639 let ccw = build_path("S 0 0 A 5 5 0 1 0 10 0").expect("path");
1641 let b2 = ccw.bounds();
1642 assert!(
1643 b2.bottom() > 4.5,
1644 "counter-clockwise should bulge down (+y), got {}",
1645 b2.bottom()
1646 );
1647 }
1648
1649 #[test]
1651 fn arc_with_zero_radius_degenerates_to_line() {
1652 let path = build_path("S 0 0 A 0 0 0 0 0 10 0").expect("path");
1653 let b = path.bounds();
1654 assert!(b.height() < 0.001, "degenerate arc must be flat");
1655 }
1656
1657 fn glyph_pt(cx: f64, cy: f64, scale: f64, char_dir: f64, fx: f64, fy: f64) -> (f64, f64) {
1659 let m = glyph_to_object(cx, cy, scale, scale, char_dir);
1660 (m.a * fx + m.c * fy + m.e, m.b * fx + m.d * fy + m.f)
1661 }
1662
1663 #[test]
1666 fn char_direction_rotates_glyph_clockwise() {
1667 let approx = |a: f64, b: f64| (a - b).abs() < 1e-9;
1668 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
1671 assert!(approx(x, 0.0) && approx(y, -1.0), "0° up→up: {x},{y}");
1672 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 90.0, 0.0, 1.0);
1674 assert!(approx(x, 1.0) && approx(y, 0.0), "90° up→right: {x},{y}");
1675 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 180.0, 0.0, 1.0);
1677 assert!(approx(x, 0.0) && approx(y, 1.0), "180° up→down: {x},{y}");
1678 let (x, y) = glyph_pt(0.0, 0.0, 1.0, 270.0, 0.0, 1.0);
1680 assert!(approx(x, -1.0) && approx(y, 0.0), "270° up→left: {x},{y}");
1681 }
1682
1683 #[test]
1685 fn glyph_origin_translates_to_pen_position() {
1686 let (x, y) = glyph_pt(12.0, 34.0, 2.0, 90.0, 0.0, 0.0);
1687 assert!((x - 12.0).abs() < 1e-9 && (y - 34.0).abs() < 1e-9);
1688 }
1689
1690 use crate::model::graphics::{CtCgTransform, TextCode, TextObject};
1691 use crate::types::StArray;
1692
1693 fn text_code(x: Option<f64>, y: Option<f64>, dx: &str, text: &str) -> TextCode {
1694 TextCode {
1695 x,
1696 y,
1697 delta_x: (!dx.is_empty()).then(|| dx.to_string()),
1698 delta_y: None,
1699 text: Some(text.to_string()),
1700 }
1701 }
1702
1703 #[test]
1705 fn text_points_advance_by_delta_x() {
1706 let obj = TextObject {
1707 text_codes: vec![text_code(Some(0.0), Some(25.0), "10 10", "ABC")],
1708 ..Default::default()
1709 };
1710 let pts = text_code_points(&obj);
1711 assert_eq!(pts, vec![(0.0, 25.0), (10.0, 25.0), (20.0, 25.0)]);
1712 }
1713
1714 #[test]
1716 fn text_points_inherit_xy_from_previous_text_code() {
1717 let obj = TextObject {
1718 text_codes: vec![
1719 text_code(Some(5.0), Some(7.0), "", "A"),
1720 text_code(None, None, "", "B"),
1722 ],
1723 ..Default::default()
1724 };
1725 let pts = text_code_points(&obj);
1726 assert_eq!(pts, vec![(5.0, 7.0), (5.0, 7.0)]);
1727 }
1728
1729 #[test]
1731 fn place_glyphs_uses_cmap_without_transform() {
1732 let obj = TextObject {
1733 text_codes: vec![text_code(Some(0.0), Some(0.0), "10", "AB")],
1734 ..Default::default()
1735 };
1736 let placed = place_glyphs(&obj, |ch| Some(ttf_parser::GlyphId(ch as u16)));
1738 assert_eq!(placed.len(), 2);
1739 assert_eq!(placed[0].gid.0, b'A' as u16);
1740 assert_eq!(placed[0].origin, (0.0, 0.0));
1741 assert_eq!(placed[1].gid.0, b'B' as u16);
1742 assert_eq!(placed[1].origin, (10.0, 0.0));
1743 }
1744
1745 #[test]
1748 fn place_glyphs_applies_ligature_transform() {
1749 let obj = TextObject {
1750 text_codes: vec![text_code(Some(0.0), Some(0.0), "10", "fi")],
1751 cg_transforms: vec![CtCgTransform {
1752 code_position: 0,
1753 code_count: Some(2),
1754 glyph_count: Some(1),
1755 glyphs: Some(StArray(vec![192])),
1756 }],
1757 ..Default::default()
1758 };
1759 let placed = place_glyphs(&obj, |_| {
1760 panic!("CMAP must not be used inside transform range")
1761 });
1762 assert_eq!(placed.len(), 1);
1763 assert_eq!(placed[0].gid.0, 192);
1764 assert_eq!(placed[0].origin, (0.0, 0.0));
1765 }
1766
1767 #[test]
1769 fn place_glyphs_mixes_transform_and_cmap() {
1770 let obj = TextObject {
1771 text_codes: vec![text_code(Some(0.0), Some(0.0), "10 10", "fix")],
1773 cg_transforms: vec![CtCgTransform {
1774 code_position: 0,
1775 code_count: Some(2),
1776 glyph_count: Some(1),
1777 glyphs: Some(StArray(vec![192])),
1778 }],
1779 ..Default::default()
1780 };
1781 let placed = place_glyphs(&obj, |ch| Some(ttf_parser::GlyphId(ch as u16)));
1782 assert_eq!(placed.len(), 2);
1783 assert_eq!(placed[0].gid.0, 192);
1784 assert_eq!(placed[0].origin, (0.0, 0.0));
1785 assert_eq!(placed[1].gid.0, b'x' as u16);
1787 assert_eq!(placed[1].origin, (20.0, 0.0));
1788 }
1789}