1#![forbid(unsafe_code)]
9#![deny(missing_docs)]
10
11#[macro_use]
12mod log;
13
14mod primitive;
15
16use crate::primitive::{WriteDirect, WriteIndirect};
17use flate2::Compression;
18use flate2::write::ZlibEncoder;
19use hayro_syntax::object::ObjRef;
20use hayro_syntax::object::Object;
21use hayro_syntax::object::dict::keys::{
22 COLORSPACE, EXT_G_STATE, FONT, GROUP, PATTERN, PROPERTIES, SHADING, XOBJECT,
23};
24use hayro_syntax::page::{Page, Resources, Rotation};
25use pdf_writer::{Chunk, Content, Filter, Finish, Name, Rect, Ref};
26use rustc_hash::FxHashMap;
27use std::collections::HashSet;
28use std::ops::Deref;
29use std::ops::DerefMut;
30
31pub use hayro_syntax;
32use hayro_syntax::Pdf;
33pub use pdf_writer;
34pub use pdf_writer::Settings as ChunkSettings;
35
36pub fn extract<'a>(
38 pdf: &Pdf,
39 new_ref: Box<dyn FnMut() -> Ref + 'a>,
40 chunk_settings: ChunkSettings,
41 queries: &[ExtractionQuery],
42) -> Result<ExtractionResult, ExtractionError> {
43 let pages = pdf.pages();
44 let mut ctx = ExtractionContext::new(new_ref, pdf, chunk_settings);
45
46 for query in queries {
47 let page = pages
48 .get(query.page_index)
49 .ok_or(ExtractionError::InvalidPageIndex(query.page_index))?;
50
51 let root_ref = ctx.new_ref();
52
53 let res = match query.query_type {
54 ExtractionQueryType::XObject => write_xobject(page, root_ref, &mut ctx),
55 ExtractionQueryType::Page => write_page(page, root_ref, query.page_index, &mut ctx),
56 };
57
58 ctx.root_refs.push(res.map(|_| root_ref));
59 }
60
61 write_dependencies(pdf, &mut ctx);
64
65 let mut global_chunk = Chunk::with_settings(chunk_settings);
66
67 for chunk in &ctx.chunks {
68 global_chunk.extend(chunk);
69 }
70
71 Ok(ExtractionResult {
72 chunk: global_chunk,
73 root_refs: ctx.root_refs,
74 page_tree_parent_ref: ctx.page_tree_parent_ref,
75 })
76}
77
78#[derive(Copy, Clone, Debug)]
81pub enum ExtractionQueryType {
82 XObject,
84 Page,
86}
87
88#[derive(Copy, Clone, Debug)]
90pub struct ExtractionQuery {
91 query_type: ExtractionQueryType,
92 page_index: usize,
93}
94
95impl ExtractionQuery {
96 pub fn new_page(page_index: usize) -> Self {
98 Self {
99 query_type: ExtractionQueryType::Page,
100 page_index,
101 }
102 }
103
104 pub fn new_xobject(page_index: usize) -> Self {
106 Self {
107 query_type: ExtractionQueryType::XObject,
108 page_index,
109 }
110 }
111}
112
113#[derive(Debug, Copy, Clone)]
115pub enum ExtractionError {
116 InvalidPageIndex(usize),
118}
119
120#[derive(Debug)]
122pub struct ExtractionResult {
123 pub chunk: Chunk,
125 pub root_refs: Vec<Result<Ref, ExtractionError>>,
127 pub page_tree_parent_ref: Ref,
129}
130
131struct ExtractionContext<'a> {
132 chunks: Vec<Chunk>,
133 visited_objects: HashSet<ObjRef>,
134 to_visit_refs: Vec<ObjRef>,
135 valid_ref_cache: FxHashMap<ObjRef, bool>,
136 root_refs: Vec<Result<Ref, ExtractionError>>,
137 pdf: &'a Pdf,
138 new_ref: Box<dyn FnMut() -> Ref + 'a>,
139 ref_map: FxHashMap<ObjRef, Ref>,
140 cached_content_streams: FxHashMap<usize, Ref>,
141 page_tree_parent_ref: Ref,
142 chunk_settings: ChunkSettings,
143}
144
145impl<'a> ExtractionContext<'a> {
146 fn new(
147 mut new_ref: Box<dyn FnMut() -> Ref + 'a>,
148 pdf: &'a Pdf,
149 chunk_settings: ChunkSettings,
150 ) -> Self {
151 let page_tree_parent_ref = new_ref();
152 Self {
153 chunks: vec![],
154 visited_objects: HashSet::new(),
155 to_visit_refs: Vec::new(),
156 valid_ref_cache: FxHashMap::default(),
157 pdf,
158 new_ref,
159 ref_map: FxHashMap::default(),
160 cached_content_streams: FxHashMap::default(),
161 root_refs: Vec::new(),
162 page_tree_parent_ref,
163 chunk_settings,
164 }
165 }
166
167 pub(crate) fn map_ref(&mut self, ref_: ObjRef) -> Ref {
168 if let Some(ref_) = self.ref_map.get(&ref_) {
169 *ref_
170 } else {
171 let new_ref = self.new_ref();
172 self.ref_map.insert(ref_, new_ref);
173
174 new_ref
175 }
176 }
177
178 pub(crate) fn new_ref(&mut self) -> Ref {
179 (self.new_ref)()
180 }
181}
182
183fn write_dependencies(pdf: &Pdf, ctx: &mut ExtractionContext<'_>) {
184 while let Some(ref_) = ctx.to_visit_refs.pop() {
185 if ctx.visited_objects.contains(&ref_) {
187 continue;
188 }
189
190 let mut chunk = Chunk::with_settings(ctx.chunk_settings);
191 if let Some(object) = pdf.xref().get::<Object<'_>>(ref_.into()) {
192 let new_ref = ctx.map_ref(ref_);
193 object.write_indirect(&mut chunk, new_ref, ctx);
194 ctx.chunks.push(chunk);
195
196 ctx.visited_objects.insert(ref_);
197 } else {
198 warn!("failed to extract object with ref: {ref_:?}");
199 }
200 }
201}
202
203#[doc(hidden)]
206pub fn extract_pages_to_pdf(hayro_pdf: &Pdf, page_indices: &[usize]) -> Vec<u8> {
207 let mut pdf = pdf_writer::Pdf::new();
208 let mut next_ref = Ref::new(1);
209 let requests = page_indices
210 .iter()
211 .map(|i| ExtractionQuery {
212 query_type: ExtractionQueryType::Page,
213 page_index: *i,
214 })
215 .collect::<Vec<_>>();
216
217 let catalog_id = next_ref.bump();
218
219 let extracted = extract(
220 hayro_pdf,
221 Box::new(|| next_ref.bump()),
222 ChunkSettings::default(),
223 &requests,
224 )
225 .unwrap();
226 pdf.catalog(catalog_id)
227 .pages(extracted.page_tree_parent_ref);
228 let count = extracted.root_refs.len();
229 pdf.pages(extracted.page_tree_parent_ref)
230 .kids(extracted.root_refs.iter().map(|r| r.unwrap()))
231 .count(count as i32);
232 pdf.extend(&extracted.chunk);
233
234 pdf.finish()
235}
236
237#[doc(hidden)]
240pub fn extract_pages_as_xobject_to_pdf(hayro_pdf: &Pdf, page_indices: &[usize]) -> Vec<u8> {
241 let hayro_pages = hayro_pdf.pages();
242 let page_list = hayro_pages.as_ref();
243
244 let mut pdf = pdf_writer::Pdf::new();
245 let mut next_ref = Ref::new(1);
246
247 let catalog_id = next_ref.bump();
248 let requests = page_indices
249 .iter()
250 .map(|i| ExtractionQuery {
251 query_type: ExtractionQueryType::XObject,
252 page_index: *i,
253 })
254 .collect::<Vec<_>>();
255
256 let extracted = extract(
257 hayro_pdf,
258 Box::new(|| next_ref.bump()),
259 ChunkSettings::default(),
260 &requests,
261 )
262 .unwrap();
263
264 pdf.catalog(catalog_id)
265 .pages(extracted.page_tree_parent_ref);
266 let mut page_refs = vec![];
267
268 for (x_object_ref, page_idx) in extracted.root_refs.iter().zip(page_indices) {
269 let page = &page_list[*page_idx];
270 let render_dimensions = page.render_dimensions();
271
272 let mut content = Content::new();
273 content.x_object(Name(b"O1"));
274
275 let finished = content.finish();
276
277 let page_id = next_ref.bump();
278 let stream_id = next_ref.bump();
279 page_refs.push(page_id);
280
281 let mut page = pdf.page(page_id);
282 page.resources()
283 .x_objects()
284 .pair(Name(b"O1"), x_object_ref.unwrap());
285 page.media_box(Rect::new(
286 0.0,
287 0.0,
288 render_dimensions.0,
289 render_dimensions.1,
290 ));
291 page.parent(extracted.page_tree_parent_ref);
292 page.contents(stream_id);
293 page.finish();
294
295 pdf.stream(stream_id, finished.as_slice());
296 }
297
298 let count = extracted.root_refs.len();
299 pdf.pages(extracted.page_tree_parent_ref)
300 .kids(page_refs)
301 .count(count as i32);
302 pdf.extend(&extracted.chunk);
303
304 pdf.finish()
305}
306
307fn write_page(
308 page: &Page<'_>,
309 page_ref: Ref,
310 page_idx: usize,
311 ctx: &mut ExtractionContext<'_>,
312) -> Result<(), ExtractionError> {
313 let mut chunk = Chunk::with_settings(ctx.chunk_settings);
314 let stream_ref = if let Some(cached) = ctx.cached_content_streams.get(&page_idx) {
317 *cached
318 } else {
319 let stream_ref = ctx.new_ref();
320
321 chunk
322 .stream(
323 stream_ref,
324 &deflate_encode(page.page_stream().unwrap_or(b"")),
325 )
326 .filter(Filter::FlateDecode);
327 ctx.cached_content_streams.insert(page_idx, stream_ref);
328
329 stream_ref
330 };
331
332 let mut pdf_page = chunk.page(page_ref);
333
334 pdf_page
335 .media_box(convert_rect(&page.media_box()))
336 .crop_box(convert_rect(&page.crop_box()))
337 .rotate(match page.rotation() {
338 Rotation::None => 0,
339 Rotation::Horizontal => 90,
340 Rotation::Flipped => 180,
341 Rotation::FlippedHorizontal => 270,
342 })
343 .parent(ctx.page_tree_parent_ref)
344 .contents(stream_ref);
345
346 let raw_dict = page.raw();
347
348 if let Some(group) = raw_dict.get_raw::<Object<'_>>(GROUP) {
349 group.write_direct(pdf_page.insert(Name(GROUP)), ctx);
350 }
351
352 serialize_resources(page.resources(), ctx, &mut pdf_page);
353
354 pdf_page.finish();
355
356 ctx.chunks.push(chunk);
357
358 Ok(())
359}
360
361fn write_xobject(
362 page: &Page<'_>,
363 xobj_ref: Ref,
364 ctx: &mut ExtractionContext<'_>,
365) -> Result<(), ExtractionError> {
366 let mut chunk = Chunk::with_settings(ctx.chunk_settings);
367 let encoded_stream = deflate_encode(page.page_stream().unwrap_or(b""));
368 let mut x_object = chunk.form_xobject(xobj_ref, &encoded_stream);
369 x_object.deref_mut().filter(Filter::FlateDecode);
370
371 let bbox = page.crop_box();
372 let initial_transform = page.initial_transform(false);
373
374 x_object.bbox(Rect::new(
375 bbox.x0 as f32,
376 bbox.y0 as f32,
377 bbox.x1 as f32,
378 bbox.y1 as f32,
379 ));
380
381 let i = initial_transform.as_coeffs();
382 x_object.matrix([
383 i[0] as f32,
384 i[1] as f32,
385 i[2] as f32,
386 i[3] as f32,
387 i[4] as f32,
388 i[5] as f32,
389 ]);
390
391 serialize_resources(page.resources(), ctx, &mut x_object);
392
393 let raw_dict = page.raw();
394
395 if let Some(group) = raw_dict.get_raw::<Object<'_>>(GROUP) {
396 group.write_direct(x_object.insert(Name(GROUP)), ctx);
397 }
398
399 x_object.finish();
400 ctx.chunks.push(chunk);
401
402 Ok(())
403}
404
405fn serialize_resources(
406 resources: &Resources<'_>,
407 ctx: &mut ExtractionContext<'_>,
408 writer: &mut impl ResourcesExt,
409) {
410 let mut output = writer.resources();
413
414 macro_rules! write {
415 ($name:ident, $key:expr) => {
416 if !resources.$name.is_empty() {
417 let mut dict = output.insert(Name($key)).dict();
418
419 for (name, obj) in resources.$name.entries() {
420 obj.write_direct(dict.insert(Name(name.deref())), ctx);
421 }
422 }
423 };
424 }
425
426 write!(ext_g_states, EXT_G_STATE);
427 write!(shadings, SHADING);
428 write!(patterns, PATTERN);
429 write!(x_objects, XOBJECT);
430 write!(color_spaces, COLORSPACE);
431 write!(fonts, FONT);
432 write!(properties, PROPERTIES);
433}
434
435pub(crate) fn deflate_encode(data: &[u8]) -> Vec<u8> {
436 use std::io::Write;
437
438 const COMPRESSION_LEVEL: u8 = 6;
439 let mut e = ZlibEncoder::new(Vec::new(), Compression::new(COMPRESSION_LEVEL as u32));
440 e.write_all(data).unwrap();
441 e.finish().unwrap()
442}
443
444fn convert_rect(hy_rect: &hayro_syntax::object::Rect) -> Rect {
445 Rect::new(
446 hy_rect.x0 as f32,
447 hy_rect.y0 as f32,
448 hy_rect.x1 as f32,
449 hy_rect.y1 as f32,
450 )
451}
452
453trait ResourcesExt {
454 fn resources(&mut self) -> pdf_writer::writers::Resources<'_>;
455}
456
457impl ResourcesExt for pdf_writer::writers::Page<'_> {
458 fn resources(&mut self) -> pdf_writer::writers::Resources<'_> {
459 Self::resources(self)
460 }
461}
462
463impl ResourcesExt for pdf_writer::writers::FormXObject<'_> {
464 fn resources(&mut self) -> pdf_writer::writers::Resources<'_> {
465 Self::resources(self)
466 }
467}