Skip to main content

easypdf_core/
content.rs

1//! PDF 元素的内容模型类型——文本、表格、图片和形状。
2
3use crate::enums::{TextAlignment, VerticalAlignment};
4use crate::style::{PdfColor, PdfFont};
5
6// --- 文本 ---
7
8/// 带格式的定位文本块。
9#[derive(Debug, Clone)]
10pub struct PdfText {
11    /// 要渲染的文本字符串。
12    pub content: String,
13    /// 文本块内的水平对齐方式。
14    pub alignment: TextAlignment,
15    /// 此文本的字体规格。
16    pub font: PdfFont,
17    /// 文本颜色。
18    pub color: PdfColor,
19}
20
21impl PdfText {
22    /// 使用给定内容创建新的文本元素。
23    #[must_use]
24    pub fn new(content: impl Into<String>) -> Self {
25        Self {
26            content: content.into(),
27            alignment: TextAlignment::default(),
28            font: PdfFont::default(),
29            color: PdfColor::default(),
30        }
31    }
32
33    /// 设置此文本的字体。
34    #[must_use]
35    pub fn font(mut self, font: PdfFont) -> Self {
36        self.font = font;
37        self
38    }
39
40    /// 设置此文本的对齐方式。
41    #[must_use]
42    pub const fn alignment(mut self, alignment: TextAlignment) -> Self {
43        self.alignment = alignment;
44        self
45    }
46
47    /// 设置此文本的颜色。
48    #[must_use]
49    pub const fn color(mut self, color: PdfColor) -> Self {
50        self.color = color;
51        self
52    }
53}
54
55// --- 表格 ---
56
57/// 要在 PDF 中渲染的表格配置。
58#[derive(Debug, Clone)]
59pub struct PdfTable {
60    /// 表头。
61    pub headers: Vec<String>,
62    /// 行数据(每行为字符串值的向量)。
63    pub rows: Vec<Vec<String>>,
64    /// 列宽(PDF 点)。为空时列均匀分配。
65    pub column_widths: Vec<f64>,
66    /// 表格总宽度(PDF 点)。
67    pub width: f64,
68}
69
70impl PdfTable {
71    /// 使用给定表头创建新表格。
72    #[must_use]
73    pub fn new(headers: Vec<String>) -> Self {
74        Self {
75            headers,
76            rows: Vec::new(),
77            column_widths: Vec::new(),
78            width: 0.0,
79        }
80    }
81
82    /// 向表格添加一行数据。
83    #[must_use]
84    pub fn row(mut self, row: Vec<String>) -> Self {
85        self.rows.push(row);
86        self
87    }
88
89    /// 向表格添加多行数据。
90    #[must_use]
91    pub fn rows(mut self, rows: Vec<Vec<String>>) -> Self {
92        self.rows.extend(rows);
93        self
94    }
95
96    /// 设置表格宽度。
97    #[must_use]
98    pub const fn width(mut self, width: f64) -> Self {
99        self.width = width;
100        self
101    }
102}
103
104// --- 表格单元格 ---
105
106/// 表格中的单个单元格。
107#[derive(Debug, Clone, Default)]
108pub struct PdfTableCell {
109    /// 单元格文本内容。
110    pub content: String,
111    /// 单元格内的水平对齐方式。
112    pub h_alignment: TextAlignment,
113    /// 单元格内的垂直对齐方式。
114    pub v_alignment: VerticalAlignment,
115    /// 字体规格。
116    pub font: PdfFont,
117    /// 文本颜色。
118    pub color: PdfColor,
119}
120
121// --- 图片 ---
122
123/// 要嵌入 PDF 的图片。
124#[derive(Debug, Clone)]
125pub struct PdfImage {
126    /// 原始图片字节(PNG、JPEG 等——格式自动检测)。
127    pub data: Vec<u8>,
128    /// 期望宽度(PDF 点,0 = 按 72 DPI 使用原始尺寸)。
129    pub width: f64,
130    /// 期望高度(PDF 点,0 = 按 72 DPI 使用原始尺寸)。
131    pub height: f64,
132}
133
134impl PdfImage {
135    /// 从原始字节创建图片。
136    #[must_use]
137    pub fn from_bytes(data: Vec<u8>) -> Self {
138        Self {
139            data,
140            width: 0.0,
141            height: 0.0,
142        }
143    }
144
145    /// 从文件路径创建图片。
146    ///
147    /// # Errors
148    ///
149    /// 文件无法读取时返回 `PdfError::Io`。
150    pub fn from_path(path: impl AsRef<std::path::Path>) -> crate::error::Result<Self> {
151        let data = std::fs::read(path)?;
152        Ok(Self::from_bytes(data))
153    }
154}
155
156// --- 形状 ---
157
158/// 线段。
159#[derive(Debug, Clone, Copy)]
160pub struct PdfLine {
161    /// 起点 x 坐标。
162    pub x1: f64,
163    /// 起点 y 坐标。
164    pub y1: f64,
165    /// 终点 x 坐标。
166    pub x2: f64,
167    /// 终点 y 坐标。
168    pub y2: f64,
169    /// 线宽(PDF 点)。
170    pub width: f64,
171    /// 线条颜色。
172    pub color: PdfColor,
173}
174
175/// 矩形。
176#[derive(Debug, Clone, Copy)]
177pub struct PdfRect {
178    /// 左下角 x。
179    pub x: f64,
180    /// 左下角 y。
181    pub y: f64,
182    /// 宽度。
183    pub w: f64,
184    /// 高度。
185    pub h: f64,
186    /// 边框宽度(0 = 无边框)。
187    pub border_width: f64,
188    /// 边框颜色。
189    pub border_color: PdfColor,
190    /// 填充颜色(`None` 时透明)。
191    pub fill_color: Option<PdfColor>,
192}
193
194#[cfg(test)]
195#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn pdf_text_new() {
201        let t = PdfText::new("hello");
202        assert_eq!(t.content, "hello");
203    }
204
205    #[test]
206    fn pdf_text_font() {
207        let t = PdfText::new("x").font(PdfFont::default());
208        let _ = format!("{:?}", t.font);
209    }
210
211    #[test]
212    fn pdf_text_alignment() {
213        let t = PdfText::new("x").alignment(TextAlignment::Center);
214        assert_eq!(t.alignment, TextAlignment::Center);
215    }
216
217    #[test]
218    fn pdf_text_color() {
219        let t = PdfText::new("x").color(PdfColor::default());
220        assert_eq!(t.color, PdfColor::default());
221    }
222
223    #[test]
224    fn pdf_text_debug_clone() {
225        let t = PdfText::new("test");
226        let cloned = t.clone();
227        assert_eq!(t.content, cloned.content);
228        let _ = format!("{:?}", t);
229    }
230
231    #[test]
232    fn pdf_table_new() {
233        let t = PdfTable::new(vec!["A".into(), "B".into()]);
234        assert_eq!(t.headers.len(), 2);
235        assert!(t.rows.is_empty());
236    }
237
238    #[test]
239    fn pdf_table_row() {
240        let t = PdfTable::new(vec!["A".into()]).row(vec!["1".into()]);
241        assert_eq!(t.rows.len(), 1);
242    }
243
244    #[test]
245    fn pdf_table_rows() {
246        let t = PdfTable::new(vec!["A".into()]).rows(vec![vec!["1".into()], vec!["2".into()]]);
247        assert_eq!(t.rows.len(), 2);
248    }
249
250    #[test]
251    fn pdf_table_width() {
252        let t = PdfTable::new(vec!["A".into()]).width(200.0);
253        assert_eq!(t.width, 200.0);
254    }
255
256    #[test]
257    fn pdf_table_debug_clone() {
258        let t = PdfTable::new(vec!["A".into()]);
259        let cloned = t.clone();
260        assert_eq!(t.headers, cloned.headers);
261        let _ = format!("{:?}", t);
262    }
263
264    #[test]
265    fn pdf_image_from_bytes() {
266        let img = PdfImage::from_bytes(vec![1, 2, 3]);
267        assert_eq!(img.data, vec![1, 2, 3]);
268        assert_eq!(img.width, 0.0);
269        assert_eq!(img.height, 0.0);
270    }
271
272    #[test]
273    fn pdf_image_debug_clone() {
274        let img = PdfImage::from_bytes(vec![1]);
275        let cloned = img.clone();
276        assert_eq!(img.data, cloned.data);
277        let _ = format!("{:?}", img);
278    }
279
280    #[test]
281    fn pdf_table_cell_default() {
282        let cell = PdfTableCell::default();
283        assert!(cell.content.is_empty());
284    }
285
286    #[test]
287    fn pdf_table_cell_debug_clone() {
288        let cell = PdfTableCell {
289            content: "x".into(),
290            ..Default::default()
291        };
292        let cloned = cell.clone();
293        assert_eq!(cell.content, cloned.content);
294        let _ = format!("{:?}", cell);
295    }
296
297    #[test]
298    fn pdf_line_debug_copy() {
299        let line = PdfLine {
300            x1: 0.0,
301            y1: 0.0,
302            x2: 100.0,
303            y2: 100.0,
304            width: 1.0,
305            color: PdfColor::default(),
306        };
307        let copied = line;
308        assert_eq!(line.x2, copied.x2);
309        let _ = format!("{:?}", line);
310    }
311
312    #[test]
313    fn pdf_rect_debug_copy() {
314        let rect = PdfRect {
315            x: 0.0,
316            y: 0.0,
317            w: 100.0,
318            h: 50.0,
319            border_width: 1.0,
320            border_color: PdfColor::default(),
321            fill_color: None,
322        };
323        let copied = rect;
324        assert_eq!(rect.w, copied.w);
325        let _ = format!("{:?}", rect);
326    }
327}