Skip to main content

easypdf_core/layout/
flow_layout.rs

1//! 自动流式布局。
2
3use std::path::Path;
4
5use crate::{Orientation, PageSize, PdfFont, PdfText, Result};
6
7use crate::{Direction, LayoutSink};
8
9/// 在页面内自动定位内容的流式布局器。
10pub struct FlowLayout<S> {
11    direction: Direction,
12    margins: f64,
13    spacing: f64,
14    cursor: f64,
15    page_width: f64,
16    page_height: f64,
17    sink: S,
18}
19
20impl<S: LayoutSink> FlowLayout<S> {
21    /// 创建默认边距的纵向流式布局。
22    #[must_use]
23    pub fn vertical(sink: S, page_size: PageSize) -> Self {
24        let (width, height) = page_size.dimensions();
25        Self {
26            direction: Direction::Vertical,
27            margins: 72.0,
28            spacing: 12.0,
29            cursor: height - 72.0,
30            page_width: width,
31            page_height: height,
32            sink,
33        }
34    }
35
36    /// 设置页面边距。
37    #[must_use]
38    pub fn margins(mut self, margins: f64) -> Self {
39        self.margins = margins;
40        self.cursor = self.page_height - margins;
41        self
42    }
43
44    /// 设置元素间距。
45    #[must_use]
46    pub fn spacing(mut self, spacing: f64) -> Self {
47        self.spacing = spacing;
48        self
49    }
50
51    /// 返回布局方向。
52    #[must_use]
53    pub const fn direction(&self) -> Direction {
54        self.direction
55    }
56
57    /// 写入文本并自动推进游标。
58    ///
59    /// # Errors
60    ///
61    /// 写入后端失败时返回错误。
62    pub fn add_text(&mut self, content: &str, font: &PdfFont, estimated_height: f64) -> Result<()> {
63        let y = self.cursor - estimated_height;
64        self.sink
65            .write_text(&PdfText::new(content).font(font.clone()), self.margins, y)?;
66        self.cursor = y - self.spacing;
67        Ok(())
68    }
69
70    /// 返回当前页面剩余纵向空间。
71    #[must_use]
72    pub fn remaining_space(&self) -> f64 {
73        self.cursor - self.margins
74    }
75
76    /// 新增页面并重置游标。
77    ///
78    /// # Errors
79    ///
80    /// 写入后端无法创建页面时返回错误。
81    pub fn new_page(&mut self) -> Result<()> {
82        self.sink.add_page(
83            PageSize::Custom(self.page_width, self.page_height),
84            Orientation::Portrait,
85        )?;
86        self.cursor = self.page_height - self.margins;
87        Ok(())
88    }
89
90    /// 完成布局并保存文档。
91    ///
92    /// # Errors
93    ///
94    /// 写入后端无法完成文档时返回错误。
95    pub fn finish(self, path: impl AsRef<Path>) -> Result<()> {
96        self.sink.finish(path.as_ref())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use std::path::Path;
103
104    use crate::{Orientation, PageSize, PdfFont, PdfText, Result};
105
106    use super::FlowLayout;
107    use crate::LayoutSink;
108
109    #[derive(Default)]
110    struct RecordingSink {
111        pages: usize,
112        texts: Vec<String>,
113    }
114
115    impl LayoutSink for RecordingSink {
116        fn add_page(&mut self, _size: PageSize, _orientation: Orientation) -> Result<usize> {
117            self.pages += 1;
118            Ok(self.pages)
119        }
120
121        fn write_text(&mut self, text: &PdfText, _x: f64, _y: f64) -> Result<()> {
122            self.texts.push(text.content.clone());
123            Ok(())
124        }
125
126        fn finish(self, _path: &Path) -> Result<()> {
127            Ok(())
128        }
129    }
130
131    #[test]
132    fn lays_out_text_and_new_pages_without_writer_dependency() {
133        let mut layout = FlowLayout::vertical(RecordingSink::default(), PageSize::A4)
134            .margins(50.0)
135            .spacing(10.0);
136        layout
137            .add_text("Hello", &PdfFont::helvetica(12.0), 20.0)
138            .expect("add text");
139        assert!(layout.remaining_space() > 0.0);
140        layout.new_page().expect("new page");
141        layout.finish("unused.pdf").expect("finish");
142    }
143}