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,否则文档默认)。
387        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        // 页面坐标(毫米)→ 设备像素:缩放并将物理区域左上角平移到原点。
409        let page_to_device =
410            Mat::translate(-area.x * scale, -area.y * scale).mul(Mat::scale(scale, scale));
411
412        // 叠放次序:背景模板 → 页面内容 → 前景模板。
413        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        // 注释(如电子印章/签章)叠加在页面内容之上。
422        self.render_annotations(doc, &page_ref, &resources, page_to_device, &mut pixmap);
423
424        Ok(pixmap)
425    }
426
427    /// 渲染指定页的注释外观(叠加在页面内容之上)。
428    ///
429    /// 注释入口、页注释文件或其引用的资源缺失时静默跳过,不影响整页渲染。
430    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        // 页注释文件路径相对注释入口文件所在目录解析。
446        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                // 外观内图元坐标相对外观边界左上角。
465                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    /// 将指定页渲染为 [`image::RgbaImage`]。
475    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    /// 将指定页渲染并编码为给定格式的图片字节。
486    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    /// 将指定页渲染并写入文件,图片格式由路径扩展名推断。
498    ///
499    /// 支持的扩展名:`png`、`jpg`/`jpeg`、`bmp`、`tif`/`tiff`、`gif`、`webp`。
500    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    /// 收集文档级资源(公共资源 + 文档资源)。
520    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    /// 解析一个资源文件并把其中的颜色空间、绘制参数、字型、多媒体并入 `res`。
538    ///
539    /// 资源文件本身或其引用的数据文件缺失时静默跳过,以尽量完成渲染。
540    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        // 资源数据文件的基准目录:资源文件所在目录叠加其 BaseLoc。
546        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                // 未内嵌字型:回退到同名系统字体,避免文字整体缺失。
567                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
593/// 渲染一个页对象(普通页或模板页)的全部图层。
594///
595/// 图层按类型排序绘制:Background → Body → Foreground;同一图层内的对象按
596/// 文档顺序绘制。
597fn 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, // Body(默认)
611    });
612    for layer in layers {
613        // 图层级绘制参数:作为层内所有图元的默认绘制参数(见表 14、表 15)。
614        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
621/// 渲染单个页块对象(必要时递归进入分组)。
622///
623/// `inherited` 为来自图层(或外层分组)的默认绘制参数,供未自带绘制参数的
624/// 图元继承颜色、线宽等属性。
625fn 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
645/// 复合对象(矢量图像)的最大展开深度,防止矢量图相互引用导致无限递归。
646const MAX_COMPOSITE_DEPTH: u32 = 16;
647
648/// 绘制复合对象:展开其按 `ResourceID` 引用的矢量图像(`CT_VectorG`)内容。
649///
650/// 矢量图内部图元在其自身坐标系中描述,经复合对象的边界与变换矩阵 `CTM`
651/// 映射到页面,再按上层变换绘制到设备像素。矢量图内容本身可再嵌套复合对象,
652/// 以 `depth` 限制展开层数避免循环引用。
653fn 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    // 矢量图坐标系 → 页面:先经复合对象 CTM,再平移到其边界左上角。
671    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
681/// 与 [`render_block`] 相同,但携带复合对象展开深度,供矢量图内容递归绘制。
682fn 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
703/// 计算图元对象坐标系 → 设备像素的变换:`page_to_device ∘ translate(边界) ∘ CTM`。
704fn 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
711/// 解析图元引用的绘制参数,并沿 `@Relative` 继承链回退合并各属性。
712///
713/// 子绘制参数显式设置的属性优先,未设置的属性逐级回退到父绘制参数(见 8.2、
714/// 表 24)。返回展平后的拷贝,调用方可像访问单个绘制参数一样取用各字段。
715/// 通过记录已访问标识避免循环引用导致的无限递归。
716fn 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        // 沿链继续向上:父参数的 `Relative` 决定下一级。
729        cur.relative = parent.relative;
730    }
731    Some(cur)
732}
733
734/// 将 `dst` 中未显式设置(`None`)的属性回退填充为 `src` 的对应值。
735/// 用于 `@Relative` 继承与图层级绘制参数继承(子优先、父兜底)。
736fn 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
747/// 计算图元实际生效的绘制参数:先解析图元自身引用的绘制参数(含 `@Relative`
748/// 链),再以图层([`CtLayer`])继承的绘制参数 `inherited` 作为最低优先级兜底。
749///
750/// 优先级:图元内联属性 > 图元 `DrawParam` 链 > 图层 `DrawParam`(见 8.2、表 14)。
751/// 图元内联属性在各绘制函数中单独优先处理,故此处仅合并后两级。
752fn 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
768/// 绘制图形(路径)对象。
769fn 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        // 端点/连接/斜接样式:图元属性优先,缺省回退绘制参数,再回退规范默认(表 34)。
827        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        // 虚线样式:实线段/空白段长度交替;偏移为起始相位(均以毫米计)。
840        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
872/// 将紧缩路径数据 `AbbreviatedData` 解析为 tiny-skia 路径。
873///
874/// 操作符见规范表 36:`S`/`M` 起始/移动、`L` 线段、`Q` 二次贝塞尔、
875/// `B` 三次贝塞尔、`A` 椭圆弧(按 9.3.5、表 42 绘制)、`C` 自动闭合。
876fn 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    // 跟踪当前绘制点与子路径起点(弧线、闭合需要)。
882    let mut cur = (0.0_f64, 0.0_f64);
883    let mut start = (0.0_f64, 0.0_f64);
884
885    // 读取 n 个浮点数;不足则返回 None。
886    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                // 椭圆弧:rx ry angle large sweep x y(见表 36、表 42)。
935                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/// 将一段椭圆弧追加到路径,按规范 9.3.5、表 42 的端点参数化绘制。
964///
965/// 入参对应紧缩操作符 `A` 的操作数:`rx`/`ry` 为椭圆长短轴半径,
966/// `angle_deg` 为椭圆在当前坐标系下的旋转角(度,正值顺时针),
967/// `large_arc` 为是否取大于 180° 的弧,`sweep` 为是否顺时针由起点扫向终点。
968/// 采用 SVG 端点参数化算法换算出中心参数,再以不超过 90° 的三次贝塞尔逼近。
969// 入参与紧缩操作符 `A` 的操作数一一对应,保持平铺以贴合规范表述。
970#[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    // 退化处理(表 42 异常处理):半径含 0 或起讫点重合 → 直线段。
985    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    // 角度对 360 取模后转弧度;OFD 正角为顺时针,与 y 轴向下的屏幕系一致。
993    let phi = (angle_deg % 360.0).to_radians();
994    let (sin_p, cos_p) = phi.sin_cos();
995
996    // 步骤 1:把端点差转换到未旋转的椭圆坐标系。
997    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    // 步骤 2:必要时按比例放大半径,保证椭圆能容纳两端点。
1003    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    // 步骤 3:求旋转坐标系下的椭圆中心。
1011    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    // 步骤 4:换回原坐标系的中心。
1025    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    // 步骤 5:求起始角与扫掠角。
1029    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    // 步骤 6:按 ≤90° 的分段以三次贝塞尔逼近。
1051    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    // 椭圆参数点及其切向(含旋转)映射到原坐标系。
1058    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
1090/// 一个待绘制字形:字形索引与其在对象坐标系中的字形原点。
1091struct PlacedGlyph {
1092    gid: ttf_parser::GlyphId,
1093    /// 字形原点(对象坐标系,毫米)。
1094    origin: (f64, f64),
1095}
1096
1097/// 绘制文字对象:从字型取出字形轮廓,按 `Fill`/`Stroke` 填充或勾边。
1098///
1099/// 字形索引的取得遵循规范 11.4:默认按字型 CMAP 表由字符映射;若文字对象带有
1100/// `CGTransform`(字形变换),则在其覆盖的字符区间内改用显式给出的字形索引,
1101/// 从而支持一对一、多对一、一对多、多对多等映射关系。
1102fn 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        // 无内嵌字型文件,无法绘制轮廓。
1114        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    // 规范表 45:Fill 默认 true、Stroke 默认 false。两者皆否则无需绘制。
1127    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    // 字符方向:每个字形绕其原点顺时针旋转该角度(见 11.3、表 47)。
1137    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        // 字体单位(y 向上)→ 对象坐标(y 向下):缩放并翻转 y,
1151        // 叠加字符方向旋转,最后平移到字形原点。
1152        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        // 勾边色默认透明(表 45);透明时无可见笔迹,跳过以省去描边。
1179        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        // 文字对象自身无线宽属性,沿用绘制参数线宽,缺省回退规范默认(表 34)。
1192        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
1201/// 按规范表 46 求出文字对象内每个字符的绘制点(对象坐标系,毫米)。
1202///
1203/// 首字符取 `X`/`Y`,其后按 `DeltaX`/`DeltaY` 在对象 X、Y 轴上累加;`X`/`Y`
1204/// 缺省时沿用上一个 `TextCode` 的起绘坐标。返回与字符流一一对应的绘制点序列。
1205fn 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
1235/// 计算文字对象内全部字形的绘制点与字形索引(对象坐标系,毫米)。
1236///
1237/// 先按规范表 46 求出每个字符的绘制点,再按规范 11.4 应用 `CGTransform`:被
1238/// 字形变换覆盖的字符区间改用显式字形索引,其余字符经 `cmap` 由字符映射。
1239/// `cmap` 通常为字型 CMAP 查表,抽象为闭包以便脱离具体字型测试定位逻辑。
1240fn 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    // 以 CodePosition 为键索引字形变换,逐字符发射字形。
1253    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            // 字形变换覆盖区间 [i, i+code_count),整体对应 glyphs 列表。
1265            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                    // 字形较字符多时(一对多),多出的字形并置在区间末字符的
1269                    // 绘制点;字形较字符少时(多对一)则只用前若干个绘制点。
1270                    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            // 无字形变换:按 CMAP 由字符取字形索引(规范 11.4.2 一对一)。
1282            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
1293/// 计算单个字形的“字体单位 → 对象坐标系”变换矩阵。
1294///
1295/// 字体坐标 y 轴向上、对象坐标 y 轴向下,故先按字号缩放并翻转 y;
1296/// 再按字符方向 `char_dir_deg`(度,顺时针)绕原点旋转;最后平移到字形
1297/// 原点 `(cx, cy)`。`scale_x` 已含 `HScale` 横向缩放。
1298fn 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
1304/// 将字型轮廓(字体单位)按给定矩阵映射到对象坐标系路径并写入 `pb`。
1305struct Outliner<'a> {
1306    pb: &'a mut PathBuilder,
1307    m: Mat,
1308}
1309
1310impl Outliner<'_> {
1311    /// 将一个字体单位坐标点经矩阵映射到对象坐标系。
1312    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
1346/// 绘制图像对象。
1347fn 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    // 图像在自身坐标系中铺满单位正方形 [0,1]×[0,1]。
1364    // 有 CTM 时由 CTM 映射;无 CTM 时按边界尺寸缩放。
1365    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    // 源图像像素 → 设备像素。
1374    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
1384/// 将 `image` 的 RGBA 缓冲转换为 tiny-skia 像素图(预乘 alpha)。
1385fn 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
1399/// 将渲染结果像素图转换为 `image` 的 RGBA 图像(去预乘)。
1400fn 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
1412/// 编码图像为指定格式的字节。JPEG 不支持透明通道,编码前转为 RGB。
1413fn 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
1428/// 由文件路径扩展名推断图片格式。
1429fn 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
1434/// 由扩展名/格式名(不含点,大小写不敏感)映射到 [`ImageFormat`]。
1435pub 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
1447/// 透明度:给定值或默认不透明(255)。
1448fn alpha_or_opaque(alpha: Option<u8>) -> u8 {
1449    alpha.unwrap_or(255)
1450}
1451
1452/// 依颜色空间将 [`CtColor`] 解析为 RGBA。
1453///
1454/// `obj_alpha` 为图元对象级透明度,将与颜色自身透明度相乘。
1455fn 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            // RGB 及未知类型按 RGB 处理。
1487            _ => (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    /// 测试辅助:按标识解析展平后的绘制参数。
1533    fn resolve_for(res: &DocResources, id: u64) -> CtDrawParam {
1534        resolve_draw_param(res, Some(StRefId(id))).expect("draw param exists")
1535    }
1536
1537    /// 8.2 表 24:子绘制参数未显式设置的属性应沿 `@Relative` 链回退到父参数,
1538    /// 而子参数已设置的属性优先保留。
1539    #[test]
1540    fn draw_param_inherits_missing_attrs_via_relative() {
1541        // 父:定义填充色与线宽;子:仅覆盖勾边色,引用父。
1542        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        // 子未设置填充色/线宽:回退父值;勾边色保留子值。
1551        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    /// 多级链 3→2→1:属性应跨越中间节点回退到最远祖先。
1557    #[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    /// 循环引用(2→1→2)不应导致无限递归。
1572    #[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    /// 表 14:图元未引用绘制参数时,应继承图层(`inherited`)的填充色。
1580    /// 复现票样中“文字落在 DrawParam=4 的图层、自身不带颜色”导致渲染成黑色的缺陷。
1581    #[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        // 图元无自身绘制参数:应取图层填充色。
1588        let eff = effective_draw_param(&res, None, Some(&layer)).expect("inherited param");
1589        assert!(eff.fill_color.is_some());
1590    }
1591
1592    /// 图元自身绘制参数优先于图层绘制参数(子覆盖父)。
1593    #[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; // 线宽未设置:应回退图层。
1600        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        // 填充色取图元自身(0.1)而非图层(0.6)。
1607        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        // 线宽图元未设置:回退图层值。
1615        assert_eq!(eff.line_width, Some(3.0));
1616    }
1617
1618    /// 紧缩数据中的 `A` 应绘制真正的椭圆弧而非直线弦:半圆弧的包围盒
1619    /// 必须向弦的一侧鼓出约一个半径,且 `SweepDirection` 决定鼓出方向
1620    /// (验证规范 9.3.5、表 42 的弧线绘制)。
1621    #[test]
1622    fn arc_bulges_like_a_semicircle() {
1623        // 由 (0,0) 经半径 5 的半圆到 (10,0)。横向跨度≈直径 10、鼓出≈半径 5。
1624        // sweep=1 为顺时针:y 轴向下时 9→12→3 点方向,鼓向 -y(上方)。
1625        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        // sweep=0 为逆时针:应鼓向相反一侧(+y,下方)。
1640        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    /// 半径为 0 时按表 42 退化为直线段,不产生鼓出。
1650    #[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    /// 经字形变换后的对象坐标点(用于断言旋转方向)。
1658    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    /// `CharDirection` 应按规范表 47 顺时针旋转字形:字体坐标中“向上”的
1664    /// 点(+y)在 0/90/180/270 下分别落到对象坐标的 上/右/下/左。
1665    #[test]
1666    fn char_direction_rotates_glyph_clockwise() {
1667        let approx = |a: f64, b: f64| (a - b).abs() < 1e-9;
1668        // 字体单位 (0, 1):上方一个单位。缩放 1,原点 (0,0)。
1669        // 对象坐标 y 向下:CharDir=0 → 该点在上方 (0,-1)。
1670        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        // 顺时针 90° → 指向右方 (+1, 0)。
1673        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        // 180° → 指向下方 (0, +1)。
1676        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        // 270° → 指向左方 (-1, 0)。
1679        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    /// 字形原点平移正确:变换后的字体原点 (0,0) 落在 (cx, cy)。
1684    #[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    /// 表 46:首字符取 X/Y,其后沿对象 X 轴按 DeltaX 累加绘制点。
1704    #[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    /// 表 46:后续 TextCode 省略 X/Y 时沿用上一个 TextCode 的起绘坐标。
1715    #[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                // 省略 X/Y:应沿用上一段的起绘 (5,7) 而非回退到 0。
1721                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    /// 11.4 一对一:无字形变换时按 CMAP 逐字符取字形,定位于各自绘制点。
1730    #[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        // 桩 CMAP:字符的码位即字形索引。
1737        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    /// 11.4 多对一:两个字符经字形变换映射为单个连字字形,落在首字符绘制点;
1746    /// CMAP 不应被调用(验证显式字形索引优先于按字符查表)。
1747    #[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    /// 11.4:字形变换只覆盖部分字符,区间外的字符仍按 CMAP 处理。
1768    #[test]
1769    fn place_glyphs_mixes_transform_and_cmap() {
1770        let obj = TextObject {
1771            // 三个字符,绘制点 0/10/20;前两个被连字变换覆盖,第三个走 CMAP。
1772            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        // 第三个字符 'x' 落在其自身绘制点 (20,0)。
1786        assert_eq!(placed[1].gid.0, b'x' as u16);
1787        assert_eq!(placed[1].origin, (20.0, 0.0));
1788    }
1789}