Skip to main content

ofd_core/
render.rs

1//! OFD → 图片渲染(光栅化)。
2//!
3//! 在 [`crate::OfdReader`] 之上提供将版式页面渲染为位图的能力:
4//!
5//! - 以毫米为单位的页面物理区域,按给定 **DPI** 换算为像素画布;
6//! - 依模板页(背景/前景)与图层类型(Background/Body/Foreground)的叠放
7//!   次序,逐个绘制文字、图形与图像对象;
8//! - 通过 [`image`] 编码为 PNG / JPEG / BMP / TIFF / GIF / WebP 等常见格式,
9//!   格式由参数指定。
10//!
11//! 坐标与变换遵循规范 8.5:页面坐标系原点位于物理区域左上角,y 轴向下;
12//! 每个图元对象的内部坐标先经其变换矩阵 `CTM`,再平移到边界 `Boundary`
13//! 左上角,得到页面坐标,最后按 `DPI` 缩放到设备像素。
14//!
15//! # 示例
16//!
17//! ```no_run
18//! use ofd_core::{OfdReader, render::RenderOptions};
19//!
20//! let mut reader = OfdReader::open("sample.ofd")?;
21//! let body = reader.ofd().doc_bodies[0].clone();
22//! let doc = reader.load_document(&body)?;
23//!
24//! // 将首页以 200 DPI 渲染并保存为 PNG(格式由扩展名推断)。
25//! let opts = RenderOptions::with_dpi(200.0);
26//! reader.render_page_to_file(&doc, 0, &opts, "page0.png")?;
27//! # Ok::<(), ofd_core::OfdError>(())
28//! ```
29
30use std::collections::HashMap;
31use std::io::Cursor;
32use std::path::Path;
33use std::sync::OnceLock;
34
35use image::{DynamicImage, RgbaImage};
36
37/// 重新导出 [`image::ImageFormat`],便于调用方指定输出格式而无需直接依赖 `image`。
38pub 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
54/// 设备像素的安全上限,超过则拒绝渲染以避免超大内存分配。
55const MAX_DIMENSION: u32 = 20_000;
56
57/// 渲染参数。
58#[derive(Debug, Clone)]
59pub struct RenderOptions {
60    /// 输出分辨率(每英寸点数)。毫米尺寸据此换算为像素。
61    pub dpi: f64,
62    /// 画布背景色 `[R, G, B, A]`。`None` 表示透明背景。
63    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    /// 以指定 DPI 构造渲染参数,其余取默认(白色不透明背景)。
77    pub fn with_dpi(dpi: f64) -> Self {
78        RenderOptions {
79            dpi,
80            ..Default::default()
81        }
82    }
83
84    /// 设置背景色(`None` 为透明)。
85    pub fn background(mut self, background: Option<[u8; 4]>) -> Self {
86        self.background = background;
87        self
88    }
89}
90
91/// OFD 长度单位(毫米)到英寸的换算系数。
92const MM_PER_INCH: f64 = 25.4;
93
94/// 解析出的字型资源。
95struct FontRes {
96    /// 字型文件字节:优先取包内内嵌字型,缺省时回退到同名系统字体;
97    /// 两者皆无时为 `None`,该字型无法绘制轮廓。
98    data: Option<Vec<u8>>,
99    /// 字型在字体文件中的字面索引(TTC 集合内的序号;单字体为 0)。
100    index: u32,
101}
102
103/// 进程级系统字体库,按需加载一次。
104static SYSTEM_FONTS: OnceLock<fontdb::Database> = OnceLock::new();
105
106/// 返回(首次调用时加载)系统字体库。
107fn 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
115/// 名称含 CJK 字符时优先回退的常见中文字型族(按可得性概率排序)。
116///
117/// OFD 常以“宋体”“楷体”等中文名引用字型却不内嵌,而这些名称在多数 Linux
118/// 系统并非已安装字型的族名(如方正楷体注册名为“方正楷体_GBK”)。按名称
119/// 匹配失败时若直接落到通用 sans-serif(往往是仅含拉丁字形的 Roboto),中文
120/// 会因无字形而整体丢失;故先尝试这一组确实含中文字形的字型族。
121const 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
134/// 判断字符是否落在常见 CJK 区段(用于决定是否需要中文字型兜底)。
135fn is_cjk(ch: char) -> bool {
136    matches!(ch as u32,
137        0x4E00..=0x9FFF   // CJK 统一表意文字
138        | 0x3400..=0x4DBF // 扩展 A
139        | 0x3000..=0x303F // CJK 标点
140        | 0xFF00..=0xFFEF // 全角字符
141        | 0x2E80..=0x2EFF // 部首补充
142    )
143}
144
145/// 按字型名/族名在系统字体中查找替代字体,返回(字体字节, 字面索引)。
146///
147/// OFD 常仅以名称引用系统字体(如“宋体”)而不内嵌字型文件;此时按名称匹配。
148/// 匹配失败时:若所引名称含中文字符,先回退到一组确实含中文字形的字型族
149/// (见 [`CJK_FALLBACK_FAMILIES`]),最后才回退到通用无衬线字体,避免中文
150/// 因落到仅含拉丁字形的字型而整体不可见。
151fn 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    // 1. 按字型名 → 族名精确匹配。
185    for cand in [name, family] {
186        if let Some(data) = query_family(cand) {
187            return Some(data);
188        }
189    }
190
191    // 2. 名称含中文却未命中已安装字型时,回退到任一可用的中文字型,
192    //    以免落到仅含拉丁字形的 sans-serif 导致中文整体缺失。
193    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    // 3. 通用无衬线兜底。
202    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/// 一篇文档渲染所需的资源集合(颜色空间、绘制参数、字型、多媒体)。
213#[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/// OFD 仿射变换矩阵,采用规范约定 `[a b c d e f]`:
224///
225/// ```text
226/// x' = a*x + c*y + e
227/// y' = b*x + d*y + f
228/// ```
229#[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    /// 单位矩阵。
241    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    /// 平移矩阵。
253    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    /// 缩放矩阵。
265    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    /// 旋转矩阵,`theta` 为弧度。在 y 轴向下的对象坐标系中表现为顺时针旋转,
277    /// 与规范表 47 “从 x 轴正向顺时针”的角度约定一致。
278    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    /// 由规范 `[a b c d e f]` 数组构造。
291    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    /// 矩阵复合:返回 `self ∘ rhs`,即先应用 `rhs` 再应用 `self`。
307    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    /// 转换为 tiny-skia 变换。
319    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
331/// 解析得到的 RGBA 颜色(分量 0~255)。
332type Rgba = [u8; 4];
333
334impl<R: Read + Seek> OfdReader<R> {
335    /// 将文档第 `page_index` 页渲染为像素图([`tiny_skia::Pixmap`])。
336    ///
337    /// `page_index` 为页树中的页序号(从 0 起)。
338    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        // 装载本页及其页级资源。
351        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        // 预装载模板页内容(背景/前景),避免后续与资源借用冲突。
359        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            // 模板内容文件缺失时跳过,不影响整页渲染。
376            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        // 页面物理区域(优先本页 Area,其次文档默认 PageArea,仍缺省用 A4)。
387        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        // 页面坐标(毫米)→ 设备像素:缩放并将物理区域左上角平移到原点。
410        let page_to_device =
411            Mat::translate(-area.x * scale, -area.y * scale).mul(Mat::scale(scale, scale));
412
413        // 叠放次序:背景模板 → 页面内容 → 前景模板。
414        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        // 注释(如电子印章/签章)叠加在页面内容之上。
423        self.render_annotations(doc, &page_ref, &resources, page_to_device, &mut pixmap);
424
425        Ok(pixmap)
426    }
427
428    /// 渲染指定页的注释外观(叠加在页面内容之上)。
429    ///
430    /// 注释入口、页注释文件或其引用的资源缺失时静默跳过,不影响整页渲染。
431    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        // 页注释文件路径相对注释入口文件所在目录解析。
447        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                // 外观内图元坐标相对外观边界左上角。
466                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    /// 将指定页渲染为 [`image::RgbaImage`]。
476    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    /// 将指定页渲染并编码为给定格式的图片字节。
487    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    /// 将指定页渲染并写入文件,图片格式由路径扩展名推断。
499    ///
500    /// 支持的扩展名:`png`、`jpg`/`jpeg`、`bmp`、`tif`/`tiff`、`gif`、`webp`。
501    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    /// 收集文档级资源(公共资源 + 文档资源)。
521    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    /// 解析一个资源文件并把其中的颜色空间、绘制参数、字型、多媒体并入 `res`。
539    ///
540    /// 资源文件本身或其引用的数据文件缺失时静默跳过,以尽量完成渲染。
541    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        // 资源数据文件的基准目录:资源文件所在目录叠加其 BaseLoc。
547        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                // 未内嵌字型:回退到同名系统字体,避免文字整体缺失。
568                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
594/// 渲染一个页对象(普通页或模板页)的全部图层。
595///
596/// 图层按类型排序绘制:Background → Body → Foreground;同一图层内的对象按
597/// 文档顺序绘制。
598fn 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, // Body(默认)
612    });
613    for layer in layers {
614        // 图层级绘制参数:作为层内所有图元的默认绘制参数(见表 14、表 15)。
615        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
622/// 渲染单个页块对象(必要时递归进入分组)。
623///
624/// `inherited` 为来自图层(或外层分组)的默认绘制参数,供未自带绘制参数的
625/// 图元继承颜色、线宽等属性。
626fn 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
646/// 复合对象(矢量图像)的最大展开深度,防止矢量图相互引用导致无限递归。
647const MAX_COMPOSITE_DEPTH: u32 = 16;
648
649/// 绘制复合对象:展开其按 `ResourceID` 引用的矢量图像(`CT_VectorG`)内容。
650///
651/// 矢量图内部图元在其自身坐标系中描述,经复合对象的边界与变换矩阵 `CTM`
652/// 映射到页面,再按上层变换绘制到设备像素。矢量图内容本身可再嵌套复合对象,
653/// 以 `depth` 限制展开层数避免循环引用。
654fn 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    // 矢量图坐标系 → 页面:先经复合对象 CTM,再平移到其边界左上角。
672    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
682/// 与 [`render_block`] 相同,但携带复合对象展开深度,供矢量图内容递归绘制。
683fn 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
704/// 计算图元对象坐标系 → 设备像素的变换:`page_to_device ∘ translate(边界) ∘ CTM`。
705fn 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
712/// 解析图元引用的绘制参数,并沿 `@Relative` 继承链回退合并各属性。
713///
714/// 子绘制参数显式设置的属性优先,未设置的属性逐级回退到父绘制参数(见 8.2、
715/// 表 24)。返回展平后的拷贝,调用方可像访问单个绘制参数一样取用各字段。
716/// 通过记录已访问标识避免循环引用导致的无限递归。
717fn 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        // 沿链继续向上:父参数的 `Relative` 决定下一级。
730        cur.relative = parent.relative;
731    }
732    Some(cur)
733}
734
735/// 将 `dst` 中未显式设置(`None`)的属性回退填充为 `src` 的对应值。
736/// 用于 `@Relative` 继承与图层级绘制参数继承(子优先、父兜底)。
737fn 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
748/// 计算图元实际生效的绘制参数:先解析图元自身引用的绘制参数(含 `@Relative`
749/// 链),再以图层([`CtLayer`])继承的绘制参数 `inherited` 作为最低优先级兜底。
750///
751/// 优先级:图元内联属性 > 图元 `DrawParam` 链 > 图层 `DrawParam`(见 8.2、表 14)。
752/// 图元内联属性在各绘制函数中单独优先处理,故此处仅合并后两级。
753fn 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
769/// 绘制图形(路径)对象。
770fn 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        // 端点/连接/斜接样式:图元属性优先,缺省回退绘制参数,再回退规范默认(表 34)。
828        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        // 虚线样式:实线段/空白段长度交替;偏移为起始相位(均以毫米计)。
841        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
873/// 将紧缩路径数据 `AbbreviatedData` 解析为 tiny-skia 路径。
874///
875/// 操作符见规范表 36:`S`/`M` 起始/移动、`L` 线段、`Q` 二次贝塞尔、
876/// `B` 三次贝塞尔、`A` 椭圆弧(按 9.3.5、表 42 绘制)、`C` 自动闭合。
877fn 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    // 跟踪当前绘制点与子路径起点(弧线、闭合需要)。
883    let mut cur = (0.0_f64, 0.0_f64);
884    let mut start = (0.0_f64, 0.0_f64);
885
886    // 读取 n 个浮点数;不足则返回 None。
887    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                // 椭圆弧:rx ry angle large sweep x y(见表 36、表 42)。
936                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/// 将一段椭圆弧追加到路径,按规范 9.3.5、表 42 的端点参数化绘制。
965///
966/// 入参对应紧缩操作符 `A` 的操作数:`rx`/`ry` 为椭圆长短轴半径,
967/// `angle_deg` 为椭圆在当前坐标系下的旋转角(度,正值顺时针),
968/// `large_arc` 为是否取大于 180° 的弧,`sweep` 为是否顺时针由起点扫向终点。
969/// 采用 SVG 端点参数化算法换算出中心参数,再以不超过 90° 的三次贝塞尔逼近。
970// 入参与紧缩操作符 `A` 的操作数一一对应,保持平铺以贴合规范表述。
971#[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    // 退化处理(表 42 异常处理):半径含 0 或起讫点重合 → 直线段。
986    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    // 角度对 360 取模后转弧度;OFD 正角为顺时针,与 y 轴向下的屏幕系一致。
994    let phi = (angle_deg % 360.0).to_radians();
995    let (sin_p, cos_p) = phi.sin_cos();
996
997    // 步骤 1:把端点差转换到未旋转的椭圆坐标系。
998    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    // 步骤 2:必要时按比例放大半径,保证椭圆能容纳两端点。
1004    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    // 步骤 3:求旋转坐标系下的椭圆中心。
1012    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    // 步骤 4:换回原坐标系的中心。
1026    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    // 步骤 5:求起始角与扫掠角。
1030    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    // 步骤 6:按 ≤90° 的分段以三次贝塞尔逼近。
1052    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    // 椭圆参数点及其切向(含旋转)映射到原坐标系。
1059    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
1091/// 一个待绘制字形:字形索引与其在对象坐标系中的字形原点。
1092struct PlacedGlyph {
1093    gid: ttf_parser::GlyphId,
1094    /// 字形原点(对象坐标系,毫米)。
1095    origin: (f64, f64),
1096}
1097
1098/// 绘制文字对象:从字型取出字形轮廓,按 `Fill`/`Stroke` 填充或勾边。
1099///
1100/// 字形索引的取得遵循规范 11.4:默认按字型 CMAP 表由字符映射;若文字对象带有
1101/// `CGTransform`(字形变换),则在其覆盖的字符区间内改用显式给出的字形索引,
1102/// 从而支持一对一、多对一、一对多、多对多等映射关系。
1103fn 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        // 无内嵌字型文件,无法绘制轮廓。
1115        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    // 规范表 45:Fill 默认 true、Stroke 默认 false。两者皆否则无需绘制。
1128    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    // 字符方向:每个字形绕其原点顺时针旋转该角度(见 11.3、表 47)。
1138    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        // 字体单位(y 向上)→ 对象坐标(y 向下):缩放并翻转 y,
1152        // 叠加字符方向旋转,最后平移到字形原点。
1153        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        // 勾边色默认透明(表 45);透明时无可见笔迹,跳过以省去描边。
1180        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        // 文字对象自身无线宽属性,沿用绘制参数线宽,缺省回退规范默认(表 34)。
1193        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
1202/// 按规范表 46 求出文字对象内每个字符的绘制点(对象坐标系,毫米)。
1203///
1204/// 首字符取 `X`/`Y`,其后按 `DeltaX`/`DeltaY` 在对象 X、Y 轴上累加;`X`/`Y`
1205/// 缺省时沿用上一个 `TextCode` 的起绘坐标。返回与字符流一一对应的绘制点序列。
1206fn 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
1236/// 计算文字对象内全部字形的绘制点与字形索引(对象坐标系,毫米)。
1237///
1238/// 先按规范表 46 求出每个字符的绘制点,再按规范 11.4 应用 `CGTransform`:被
1239/// 字形变换覆盖的字符区间改用显式字形索引,其余字符经 `cmap` 由字符映射。
1240/// `cmap` 通常为字型 CMAP 查表,抽象为闭包以便脱离具体字型测试定位逻辑。
1241fn 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    // 以 CodePosition 为键索引字形变换,逐字符发射字形。
1254    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            // 字形变换覆盖区间 [i, i+code_count),整体对应 glyphs 列表。
1266            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                    // 字形较字符多时(一对多),多出的字形并置在区间末字符的
1270                    // 绘制点;字形较字符少时(多对一)则只用前若干个绘制点。
1271                    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            // 无字形变换:按 CMAP 由字符取字形索引(规范 11.4.2 一对一)。
1283            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
1294/// 计算单个字形的“字体单位 → 对象坐标系”变换矩阵。
1295///
1296/// 字体坐标 y 轴向上、对象坐标 y 轴向下,故先按字号缩放并翻转 y;
1297/// 再按字符方向 `char_dir_deg`(度,顺时针)绕原点旋转;最后平移到字形
1298/// 原点 `(cx, cy)`。`scale_x` 已含 `HScale` 横向缩放。
1299fn 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
1305/// 将字型轮廓(字体单位)按给定矩阵映射到对象坐标系路径并写入 `pb`。
1306struct Outliner<'a> {
1307    pb: &'a mut PathBuilder,
1308    m: Mat,
1309}
1310
1311impl Outliner<'_> {
1312    /// 将一个字体单位坐标点经矩阵映射到对象坐标系。
1313    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
1347/// 绘制图像对象。
1348fn 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    // 图像在自身坐标系中铺满单位正方形 [0,1]×[0,1]。
1365    // 有 CTM 时由 CTM 映射;无 CTM 时按边界尺寸缩放。
1366    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    // 源图像像素 → 设备像素。
1375    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
1385/// 将 `image` 的 RGBA 缓冲转换为 tiny-skia 像素图(预乘 alpha)。
1386fn 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
1400/// 将渲染结果像素图转换为 `image` 的 RGBA 图像(去预乘)。
1401fn 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
1413/// 编码图像为指定格式的字节。JPEG 不支持透明通道,编码前转为 RGB。
1414fn 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
1429/// 由文件路径扩展名推断图片格式。
1430fn 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
1435/// 由扩展名/格式名(不含点,大小写不敏感)映射到 [`ImageFormat`]。
1436pub 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
1448/// 透明度:给定值或默认不透明(255)。
1449fn alpha_or_opaque(alpha: Option<u8>) -> u8 {
1450    alpha.unwrap_or(255)
1451}
1452
1453/// 依颜色空间将 [`CtColor`] 解析为 RGBA。
1454///
1455/// `obj_alpha` 为图元对象级透明度,将与颜色自身透明度相乘。
1456fn 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            // RGB 及未知类型按 RGB 处理。
1488            _ => (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    /// 测试辅助:按标识解析展平后的绘制参数。
1534    fn resolve_for(res: &DocResources, id: u64) -> CtDrawParam {
1535        resolve_draw_param(res, Some(StRefId(id))).expect("draw param exists")
1536    }
1537
1538    /// 8.2 表 24:子绘制参数未显式设置的属性应沿 `@Relative` 链回退到父参数,
1539    /// 而子参数已设置的属性优先保留。
1540    #[test]
1541    fn draw_param_inherits_missing_attrs_via_relative() {
1542        // 父:定义填充色与线宽;子:仅覆盖勾边色,引用父。
1543        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        // 子未设置填充色/线宽:回退父值;勾边色保留子值。
1552        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    /// 多级链 3→2→1:属性应跨越中间节点回退到最远祖先。
1558    #[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    /// 循环引用(2→1→2)不应导致无限递归。
1573    #[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    /// 表 14:图元未引用绘制参数时,应继承图层(`inherited`)的填充色。
1581    /// 复现票样中“文字落在 DrawParam=4 的图层、自身不带颜色”导致渲染成黑色的缺陷。
1582    #[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        // 图元无自身绘制参数:应取图层填充色。
1589        let eff = effective_draw_param(&res, None, Some(&layer)).expect("inherited param");
1590        assert!(eff.fill_color.is_some());
1591    }
1592
1593    /// 图元自身绘制参数优先于图层绘制参数(子覆盖父)。
1594    #[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; // 线宽未设置:应回退图层。
1601        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        // 填充色取图元自身(0.1)而非图层(0.6)。
1608        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        // 线宽图元未设置:回退图层值。
1616        assert_eq!(eff.line_width, Some(3.0));
1617    }
1618
1619    /// 紧缩数据中的 `A` 应绘制真正的椭圆弧而非直线弦:半圆弧的包围盒
1620    /// 必须向弦的一侧鼓出约一个半径,且 `SweepDirection` 决定鼓出方向
1621    /// (验证规范 9.3.5、表 42 的弧线绘制)。
1622    #[test]
1623    fn arc_bulges_like_a_semicircle() {
1624        // 由 (0,0) 经半径 5 的半圆到 (10,0)。横向跨度≈直径 10、鼓出≈半径 5。
1625        // sweep=1 为顺时针:y 轴向下时 9→12→3 点方向,鼓向 -y(上方)。
1626        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        // sweep=0 为逆时针:应鼓向相反一侧(+y,下方)。
1641        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    /// 半径为 0 时按表 42 退化为直线段,不产生鼓出。
1651    #[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    /// 经字形变换后的对象坐标点(用于断言旋转方向)。
1659    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    /// `CharDirection` 应按规范表 47 顺时针旋转字形:字体坐标中“向上”的
1665    /// 点(+y)在 0/90/180/270 下分别落到对象坐标的 上/右/下/左。
1666    #[test]
1667    fn char_direction_rotates_glyph_clockwise() {
1668        let approx = |a: f64, b: f64| (a - b).abs() < 1e-9;
1669        // 字体单位 (0, 1):上方一个单位。缩放 1,原点 (0,0)。
1670        // 对象坐标 y 向下:CharDir=0 → 该点在上方 (0,-1)。
1671        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        // 顺时针 90° → 指向右方 (+1, 0)。
1674        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        // 180° → 指向下方 (0, +1)。
1677        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        // 270° → 指向左方 (-1, 0)。
1680        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    /// 字形原点平移正确:变换后的字体原点 (0,0) 落在 (cx, cy)。
1685    #[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    /// 表 46:首字符取 X/Y,其后沿对象 X 轴按 DeltaX 累加绘制点。
1705    #[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    /// 表 46:后续 TextCode 省略 X/Y 时沿用上一个 TextCode 的起绘坐标。
1716    #[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                // 省略 X/Y:应沿用上一段的起绘 (5,7) 而非回退到 0。
1722                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    /// 11.4 一对一:无字形变换时按 CMAP 逐字符取字形,定位于各自绘制点。
1731    #[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        // 桩 CMAP:字符的码位即字形索引。
1738        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    /// 11.4 多对一:两个字符经字形变换映射为单个连字字形,落在首字符绘制点;
1747    /// CMAP 不应被调用(验证显式字形索引优先于按字符查表)。
1748    #[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    /// 11.4:字形变换只覆盖部分字符,区间外的字符仍按 CMAP 处理。
1769    #[test]
1770    fn place_glyphs_mixes_transform_and_cmap() {
1771        let obj = TextObject {
1772            // 三个字符,绘制点 0/10/20;前两个被连字变换覆盖,第三个走 CMAP。
1773            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        // 第三个字符 'x' 落在其自身绘制点 (20,0)。
1787        assert_eq!(placed[1].gid.0, b'x' as u16);
1788        assert_eq!(placed[1].origin, (20.0, 0.0));
1789    }
1790
1791    // —— 紧缩路径解析 build_path ——
1792
1793    #[test]
1794    fn build_path_handles_all_commands() {
1795        // S/M 起点、L 线段、Q 二次、B 三次、A 弧、C 闭合;逗号亦作分隔。
1796        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        // 无任何移动起点 → 空路径。
1803        assert!(build_path("L 1 1").is_none());
1804        // 操作数不足 → 该段被忽略,最终仍只有 move,finish 返回 None(无线段)。
1805        assert!(build_path("M 0 0 L 1").is_none());
1806        // 未知操作符被跳过。
1807        assert!(build_path("M 0 0 Z 9 L 5 5").is_some());
1808        // 空串。
1809        assert!(build_path("").is_none());
1810    }
1811
1812    // —— 颜色解析 resolve_color ——
1813
1814    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        // 无颜色空间 → 按 RGB、8 位处理(分量按 max=255 归一化)。
1846        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        // Gray,4 位 → max=15;值 15 → 白。
1854        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        // 纯黑 K=1 → 黑。
1862        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        // 颜色未指定空间 → 用文档默认颜色空间(Gray)。
1874        let res = res_with_cs(vec![cs(7, "Gray", None)], Some(7));
1875        // 对象透明度 128 与颜色透明度 255 相乘 → 128。
1876        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    // —— 矩阵 Mat ——
1895
1896    #[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        // 平移 ∘ 缩放:点 (1,1) → 缩放 2 → (2,2) → 平移 (10,20) → (12,22)。
1901        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        // 旋转 90°:a≈0。
1906        let r = Mat::rotate(std::f64::consts::FRAC_PI_2);
1907        assert!(r.a.abs() < 1e-9);
1908    }
1909
1910    // —— 杂项纯函数 ——
1911
1912    #[test]
1913    fn is_cjk_and_alpha_helpers() {
1914        assert!(is_cjk('字'));
1915        assert!(is_cjk(',')); // 全角标点
1916        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        // 取系统字体库中真实存在的某个族名,覆盖“按名称精确匹配成功”分支。
1943        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        // 含中文名走 CJK 兜底分支;空名/未知名走通用无衬线兜底分支(仅求执行到,
1952        // 是否命中取决于运行环境已装字型,故不强断言)。
1953        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        // PNG / JPEG 编码各走一条分支。
1967        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}