Skip to main content

easypdf_reader/reader/
pdf_reader.rs

1//! [`PdfReader`] 的定义与核心打开/构造方法。
2
3use std::ops::Range;
4use std::path::Path;
5
6use easypdf_core::error::{PdfError, Result};
7use easypdf_core::io::guards::guard_element_explosion;
8use easypdf_core::io::repair::{RepairOptions, attempt_repair, is_likely_corrupt};
9use easypdf_core::{PageRange, PdfInput, ResourceLimits};
10
11use crate::strategy::ReadStrategy;
12
13/// 从 PDF 文档中提取内容的读取器。
14///
15/// 底层使用 `lopdf` crate 进行低层 PDF 解析。支持多种读取策略
16/// ([`ReadStrategy`]),可根据文档大小自动选择最优解析方式。
17///
18/// # Examples
19///
20/// ```no_run
21/// use easypdf_reader::PdfReader;
22///
23/// let text = PdfReader::open("document.pdf")?.extract_text()?;
24/// # Ok::<(), easypdf_core::PdfError>(())
25/// ```
26pub struct PdfReader {
27    /// 已解析的文档对象([`ReadStrategy::Streaming`] 模式下为 `None`)。
28    pub(super) document: Option<lopdf::Document>,
29    pub(super) pages: Option<PageRange>,
30    pub(super) limits: ResourceLimits,
31    pub(super) strategy: ReadStrategy,
32    /// 原始 PDF 字节 -- 供 Streaming 策略使用。
33    pub(super) raw_bytes: Vec<u8>,
34}
35
36impl PdfReader {
37    /// 打开 PDF 文件进行读取,自动选择解析策略。
38    ///
39    /// 根据文件大小自动选择 [`ReadStrategy`]:
40    /// 5 MB 以下使用 [`Full`](ReadStrategy::Full),
41    /// 5--100 MB 使用 [`Lazy`](ReadStrategy::Lazy),
42    /// 更大的文件使用 [`Streaming`](ReadStrategy::Streaming)。
43    ///
44    /// # Errors
45    ///
46    /// 当文件无法打开或不是有效的 PDF 时,返回 `PdfError::Parse`。
47    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
48        let path = path.as_ref();
49        let file_size = std::fs::metadata(path).map_or(0, |m| m.len());
50        let strategy = ReadStrategy::auto(file_size);
51        Self::open_with_strategy(path, strategy)
52    }
53
54    /// 从内存字节打开 PDF,自动选择解析策略。
55    ///
56    /// # Errors
57    ///
58    /// 当字节数据不是有效的 PDF 时,返回 [`PdfError::Parse`]。
59    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self> {
60        let bytes = bytes.into();
61        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
62        let strategy = ReadStrategy::auto(file_size);
63        let input = PdfInput::from_bytes(bytes);
64        Self::open_with_limits_and_strategy(&input, ResourceLimits::default(), strategy)
65    }
66
67    /// 使用指定的资源限制打开 PDF 输入。
68    ///
69    /// 文档仅解析一次并由读取器会话保留。
70    ///
71    /// # Errors
72    ///
73    /// 当输入超出限制或解析失败时返回错误。
74    pub fn open_with_limits(input: &PdfInput, limits: ResourceLimits) -> Result<Self> {
75        let bytes = input.read(limits)?;
76        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
77        let strategy = ReadStrategy::auto(file_size);
78        Self::load_from_bytes(bytes, limits, strategy)
79    }
80
81    /// 使用指定的 [`ReadStrategy`] 打开 PDF 文件。
82    ///
83    /// # Errors
84    ///
85    /// 当文件无法读取、解析或超出资源限制时返回错误。
86    pub fn open_with_strategy(path: impl AsRef<Path>, strategy: ReadStrategy) -> Result<Self> {
87        let input = PdfInput::from_path(path.as_ref());
88        Self::open_with_limits_and_strategy(&input, ResourceLimits::default(), strategy)
89    }
90
91    /// 使用指定的修复选项和读取策略打开 PDF 文件。
92    ///
93    /// 如果 [`is_likely_corrupt`] 检测到损坏,将使用提供的
94    /// [`RepairOptions`] 调用 [`attempt_repair`] 进行修复后再加载。
95    ///
96    /// # Errors
97    ///
98    /// 当文件无法读取、修复、解析或超出资源限制时返回错误。
99    pub fn open_with_repair(
100        path: impl AsRef<Path>,
101        repair: RepairOptions,
102        strategy: ReadStrategy,
103    ) -> Result<Self> {
104        let input = PdfInput::from_path(path.as_ref());
105
106        let bytes = if is_likely_corrupt(&input) {
107            attempt_repair(&input, &repair)?
108        } else {
109            input.read(ResourceLimits::default())?
110        };
111
112        Self::load_from_bytes(bytes, ResourceLimits::default(), strategy)
113    }
114
115    /// 使用指定的资源限制和读取策略打开 PDF 输入。
116    ///
117    /// # Errors
118    ///
119    /// 当输入超出限制或解析失败时返回错误。
120    pub fn open_with_limits_and_strategy(
121        input: &PdfInput,
122        limits: ResourceLimits,
123        strategy: ReadStrategy,
124    ) -> Result<Self> {
125        let bytes = input.read(limits)?;
126        Self::load_from_bytes(bytes, limits, strategy)
127    }
128
129    /// 内部方法:从原始字节加载 PDF,使用给定的限制和策略。
130    ///
131    /// 解析前应用安全防护(元素爆炸检测)。`Streaming` 策略完全跳过
132    /// `lopdf::Document` 的构建。
133    fn load_from_bytes(
134        bytes: Vec<u8>,
135        limits: ResourceLimits,
136        strategy: ReadStrategy,
137    ) -> Result<Self> {
138        if strategy == ReadStrategy::Streaming {
139            // Streaming 模式:不需要 lopdf::Document -- 直接扫描原始字节。
140            return Ok(Self {
141                document: None,
142                pages: None,
143                limits,
144                strategy,
145                raw_bytes: bytes,
146            });
147        }
148
149        let document = lopdf::Document::load_mem(&bytes)
150            .map_err(|error| PdfError::Parse(error.to_string()))?;
151
152        // 安全防护:元素爆炸 -- 检查对象总数。
153        let element_count = document.objects.len();
154        guard_element_explosion(element_count, &limits)?;
155
156        let page_count = document.get_pages().len();
157        if page_count > limits.max_pages() {
158            return Err(PdfError::ResourceLimitExceeded {
159                resource: "pages",
160                limit: usize_to_u64_saturating(limits.max_pages()),
161                actual: usize_to_u64_saturating(page_count),
162            });
163        }
164
165        Ok(Self {
166            document: Some(document),
167            pages: None,
168            limits,
169            strategy,
170            raw_bytes: bytes,
171        })
172    }
173
174    /// 返回此读取器打开时使用的读取策略。
175    #[must_use]
176    pub const fn strategy(&self) -> ReadStrategy {
177        self.strategy
178    }
179
180    /// 将提取范围限制为指定的页面范围(从 0 开始)。
181    #[must_use]
182    pub fn pages(mut self, range: Range<usize>) -> Self {
183        let start = range.start;
184        self.pages = Some(match PageRange::new(range) {
185            Ok(pages) => pages,
186            Err(_) => PageRange::empty_at(start),
187        });
188        self
189    }
190
191    /// 尝试将提取范围限制为经验证的从零开始的页面范围。
192    ///
193    /// # Errors
194    ///
195    /// 当范围反转(起始大于结束)时返回错误。
196    pub fn try_pages(mut self, range: Range<usize>) -> Result<Self> {
197        self.pages = Some(PageRange::new(range)?);
198        Ok(self)
199    }
200}
201
202/// 将 `usize` 转换为 `u64`,溢出时饱和到 `u64::MAX`。
203pub(crate) fn usize_to_u64_saturating(value: usize) -> u64 {
204    u64::try_from(value).unwrap_or(u64::MAX)
205}