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