1pub mod gradient;
6pub mod outline;
7pub mod page;
8pub mod types;
9
10pub use types::{
11 LinkAnnotation, LinkDestination, PdfDocument, PdfExtGState, PdfGradient, PdfInfo, PdfObject,
12 PdfOutline, PdfOutlineItem, PdfPage, PdfValue,
13};
14
15use fop_types::{FopError, Gradient, Result};
16
17use crate::pdf::compliance::{generate_xmp_metadata, PdfCompliance, SRGB_ICC_PROFILE};
18use crate::pdf::image::ImageXObject;
19use crate::pdf::security::EncryptionDict;
20
21use gradient::write_gradient_objects;
22use outline::{count_outline_objects, write_outline_objects};
23
24impl PdfDocument {
25 pub fn new() -> Self {
27 Self {
28 version: "1.4".to_string(),
29 objects: Vec::new(),
30 pages: Vec::new(),
31 info: PdfInfo::default(),
32 image_xobjects: Vec::new(),
33 gradients: Vec::new(),
34 ext_g_states: Vec::new(),
35 outline: None,
36 font_manager: crate::pdf::font::FontManager::new(),
37 encryption: None,
38 file_id: None,
39 compliance: PdfCompliance::Standard,
40 }
41 }
42
43 pub fn set_compliance(&mut self, compliance: PdfCompliance) -> Result<()> {
49 if compliance.requires_pdfa() && self.encryption.is_some() {
50 return Err(FopError::Generic(
51 "PDF/A-1b compliance is incompatible with encryption (ISO 19005-1 §6.1.1)"
52 .to_string(),
53 ));
54 }
55 if compliance.requires_pdfa() {
57 self.version = "1.4".to_string();
58 }
59 self.compliance = compliance;
60 Ok(())
61 }
62
63 pub fn add_page(&mut self, page: PdfPage) {
65 self.pages.push(page);
66 }
67
68 pub fn add_image_xobject(&mut self, xobject: ImageXObject) -> usize {
70 self.image_xobjects.push(xobject);
71 self.image_xobjects.len() - 1
72 }
73
74 pub fn add_gradient(&mut self, gradient: Gradient) -> usize {
79 self.gradients.push(PdfGradient {
80 gradient,
81 object_id: 0, });
83 self.gradients.len() - 1
84 }
85
86 pub fn add_ext_g_state(&mut self, fill_opacity: f64, stroke_opacity: f64) -> usize {
95 for (idx, gs) in self.ext_g_states.iter().enumerate() {
97 if (gs.fill_opacity - fill_opacity).abs() < f64::EPSILON
98 && (gs.stroke_opacity - stroke_opacity).abs() < f64::EPSILON
99 {
100 return idx;
101 }
102 }
103
104 self.ext_g_states.push(PdfExtGState {
106 fill_opacity,
107 stroke_opacity,
108 object_id: 0, });
110 self.ext_g_states.len() - 1
111 }
112
113 pub fn set_outline(&mut self, outline: PdfOutline) {
115 self.outline = Some(outline);
116 }
117
118 pub fn set_encryption(&mut self, encryption: EncryptionDict, file_id: Vec<u8>) -> Result<()> {
127 if self.compliance.requires_pdfa() {
128 return Err(FopError::Generic(
129 "PDF/A-1b compliance is incompatible with encryption (ISO 19005-1 §6.1.1)"
130 .to_string(),
131 ));
132 }
133 self.encryption = Some(encryption);
134 self.file_id = Some(file_id);
135 Ok(())
136 }
137
138 fn encrypt_stream(&self, data: &[u8], obj_num: u32) -> Vec<u8> {
140 if let Some(ref enc) = self.encryption {
141 enc.encrypt_data(data, obj_num, 0)
142 } else {
143 data.to_vec()
144 }
145 }
146
147 pub fn embed_font(&mut self, font_data: Vec<u8>) -> Result<usize> {
155 self.font_manager.embed_font(font_data)
156 }
157
158 pub fn to_bytes(&self) -> Result<Vec<u8>> {
160 let mut bytes = Vec::new();
161 let mut xref_offsets = Vec::new();
162
163 bytes.extend_from_slice(format!("%PDF-{}\n", self.version).as_bytes());
165 bytes.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n"); xref_offsets.push(0);
169
170 let outline_obj_count = if let Some(ref outline) = self.outline {
172 count_outline_objects(outline)
173 } else {
174 0
175 };
176
177 let encrypt_obj_count = if self.encryption.is_some() { 1 } else { 0 };
179
180 let font_obj_id = 3;
187 let first_outline_obj_id = 4;
188 let num_embedded_fonts = self.font_manager.font_count();
189 let encrypt_obj_id = first_outline_obj_id + outline_obj_count;
191
192 let compliance_base_id = encrypt_obj_id + encrypt_obj_count;
194 let needs_compliance = self.compliance != PdfCompliance::Standard;
195 let xmp_obj_count = if needs_compliance { 1 } else { 0 };
196 let xmp_obj_id = compliance_base_id; let oi_obj_count = if self.compliance.requires_pdfa() {
198 2
199 } else {
200 0
201 };
202 let output_intent_obj_id = compliance_base_id + xmp_obj_count; let icc_profile_obj_id = output_intent_obj_id + 1; let struct_tree_obj_count = if self.compliance.requires_pdfua() {
205 1
206 } else {
207 0
208 };
209 let struct_tree_obj_id = compliance_base_id + xmp_obj_count + oi_obj_count; let total_compliance_obj_count = xmp_obj_count + oi_obj_count + struct_tree_obj_count;
211
212 let first_embedded_font_obj_id = compliance_base_id + total_compliance_obj_count;
213
214 xref_offsets.push(bytes.len());
216 bytes.extend_from_slice(b"1 0 obj\n");
217 bytes.extend_from_slice(b"<<\n");
218 bytes.extend_from_slice(b"/Type /Catalog\n");
219 bytes.extend_from_slice(b"/Pages 2 0 R\n");
220
221 if self.outline.is_some() {
223 bytes.extend_from_slice(b"/Outlines 4 0 R\n");
224 }
225
226 if needs_compliance {
228 bytes.extend_from_slice(format!("/Metadata {} 0 R\n", xmp_obj_id).as_bytes());
229 }
230
231 if self.compliance.requires_pdfa() {
232 bytes.extend_from_slice(
233 format!("/OutputIntents [{} 0 R]\n", output_intent_obj_id).as_bytes(),
234 );
235 }
236 if self.compliance.requires_pdfua() {
237 bytes.extend_from_slice(b"/MarkInfo <<\n/Marked true\n>>\n");
238 let lang = self.info.lang.as_deref().unwrap_or("en-US");
239 bytes.extend_from_slice(format!("/Lang ({})\n", lang).as_bytes());
240 bytes.extend_from_slice(
241 format!("/StructTreeRoot {} 0 R\n", struct_tree_obj_id).as_bytes(),
242 );
243 bytes.extend_from_slice(b"/ViewerPreferences <<\n/DisplayDocTitle true\n>>\n");
244 } else if let Some(ref lang) = self.info.lang {
245 bytes.extend_from_slice(format!("/Lang ({})\n", lang).as_bytes());
247 }
248
249 bytes.extend_from_slice(b">>\n");
250 bytes.extend_from_slice(b"endobj\n");
251 let first_image_obj_id = first_embedded_font_obj_id + num_embedded_fonts * 6; let num_images = self.image_xobjects.len();
253 let first_gradient_obj_id = first_image_obj_id + num_images;
254 let num_gradients = self.gradients.len();
255 let first_ext_g_state_obj_id = first_gradient_obj_id + num_gradients * 2; let num_ext_g_states = self.ext_g_states.len();
257 let first_page_obj_id = first_ext_g_state_obj_id + num_ext_g_states;
258
259 #[allow(unused_variables)]
261 let total_annotations: usize = self.pages.iter().map(|p| p.link_annotations.len()).sum();
262 let first_annotation_obj_id = first_page_obj_id + self.pages.len() * 2;
263
264 xref_offsets.push(bytes.len());
266 bytes.extend_from_slice(b"2 0 obj\n");
267 bytes.extend_from_slice(b"<<\n");
268 bytes.extend_from_slice(b"/Type /Pages\n");
269
270 let page_obj_ids: Vec<usize> = (0..self.pages.len())
272 .map(|i| first_page_obj_id + i * 2)
273 .collect();
274
275 bytes.extend_from_slice(b"/Kids [");
276 for page_id in &page_obj_ids {
277 bytes.extend_from_slice(format!("{} 0 R ", page_id).as_bytes());
278 }
279 bytes.extend_from_slice(b"]\n");
280 bytes.extend_from_slice(format!("/Count {}\n", self.pages.len()).as_bytes());
281 bytes.extend_from_slice(b">>\n");
282 bytes.extend_from_slice(b"endobj\n");
283
284 xref_offsets.push(bytes.len());
286 bytes.extend_from_slice(format!("{} 0 obj\n", font_obj_id).as_bytes());
287 bytes.extend_from_slice(b"<<\n");
288 bytes.extend_from_slice(b"/Type /Font\n");
289 bytes.extend_from_slice(b"/Subtype /Type1\n");
290 bytes.extend_from_slice(b"/BaseFont /Helvetica\n");
291 bytes.extend_from_slice(b">>\n");
292 bytes.extend_from_slice(b"endobj\n");
293
294 if let Some(ref outline) = self.outline {
296 write_outline_objects(
297 outline,
298 &mut bytes,
299 &mut xref_offsets,
300 first_outline_obj_id,
301 &page_obj_ids,
302 );
303 }
304
305 if let Some(ref enc) = self.encryption {
307 xref_offsets.push(bytes.len());
308 let enc_dict_str = enc.to_pdf_dict(encrypt_obj_id);
309 bytes.extend_from_slice(enc_dict_str.as_bytes());
310 }
311
312 if needs_compliance {
314 let title_ref = self.info.title.as_deref();
316 let creator_tool = format!("fop-rs {}", env!("CARGO_PKG_VERSION"));
317 let xmp_content = generate_xmp_metadata(title_ref, &creator_tool, self.compliance);
318 let xmp_bytes = xmp_content.as_bytes();
319 xref_offsets.push(bytes.len());
320 bytes.extend_from_slice(format!("{} 0 obj\n", xmp_obj_id).as_bytes());
321 bytes.extend_from_slice(b"<<\n");
322 bytes.extend_from_slice(b"/Type /Metadata\n");
323 bytes.extend_from_slice(b"/Subtype /XML\n");
324 bytes.extend_from_slice(format!("/Length {}\n", xmp_bytes.len()).as_bytes());
325 bytes.extend_from_slice(b">>\nstream\n");
326 bytes.extend_from_slice(xmp_bytes);
327 bytes.extend_from_slice(b"\nendstream\nendobj\n");
328 }
329
330 if self.compliance.requires_pdfa() {
331 xref_offsets.push(bytes.len());
333 bytes.extend_from_slice(format!("{} 0 obj\n", output_intent_obj_id).as_bytes());
334 bytes.extend_from_slice(b"<<\n");
335 bytes.extend_from_slice(b"/Type /OutputIntent\n");
336 bytes.extend_from_slice(b"/S /GTS_PDFA1\n");
337 bytes.extend_from_slice(b"/OutputConditionIdentifier (sRGB)\n");
338 bytes.extend_from_slice(b"/RegistryName (http://www.color.org)\n");
339 bytes.extend_from_slice(
340 format!("/DestOutputProfile {} 0 R\n", icc_profile_obj_id).as_bytes(),
341 );
342 bytes.extend_from_slice(b">>\nendobj\n");
343
344 let icc_data = SRGB_ICC_PROFILE;
346 xref_offsets.push(bytes.len());
347 bytes.extend_from_slice(format!("{} 0 obj\n", icc_profile_obj_id).as_bytes());
348 bytes.extend_from_slice(b"<<\n");
349 bytes.extend_from_slice(b"/N 3\n"); bytes.extend_from_slice(format!("/Length {}\n", icc_data.len()).as_bytes());
351 bytes.extend_from_slice(b">>\nstream\n");
352 bytes.extend_from_slice(icc_data);
353 bytes.extend_from_slice(b"\nendstream\nendobj\n");
354 }
355
356 if self.compliance.requires_pdfua() {
357 xref_offsets.push(bytes.len());
359 bytes.extend_from_slice(format!("{} 0 obj\n", struct_tree_obj_id).as_bytes());
360 bytes.extend_from_slice(b"<<\n");
361 bytes.extend_from_slice(b"/Type /StructTreeRoot\n");
362 bytes.extend_from_slice(b">>\nendobj\n");
363 }
364
365 if num_embedded_fonts > 0 {
367 use crate::pdf::font::{
368 generate_cidfont_dict, generate_font_descriptor, generate_font_dictionary,
369 generate_font_stream_header, generate_to_unicode_cmap,
370 };
371
372 let font_objects = self
373 .font_manager
374 .generate_font_objects(first_embedded_font_obj_id)?;
375
376 for (
377 font_idx,
378 (
379 descriptor_id,
380 stream_id,
381 cidfont_id,
382 type0_dict_id,
383 to_unicode_id,
384 cidtogidmap_id,
385 font,
386 ),
387 ) in font_objects.iter().enumerate()
388 {
389 xref_offsets.push(bytes.len());
391 bytes.extend_from_slice(format!("{} 0 obj\n", descriptor_id).as_bytes());
392 bytes.extend_from_slice(generate_font_descriptor(font, *stream_id).as_bytes());
393 bytes.extend_from_slice(b"\nendobj\n");
394
395 xref_offsets.push(bytes.len());
397 bytes.extend_from_slice(format!("{} 0 obj\n", stream_id).as_bytes());
398 bytes.extend_from_slice(generate_font_stream_header(font).as_bytes());
399 bytes.extend_from_slice(b"\nstream\n");
400 bytes.extend_from_slice(&font.font_data);
401 bytes.extend_from_slice(b"\nendstream\n");
402 bytes.extend_from_slice(b"endobj\n");
403
404 xref_offsets.push(bytes.len());
406 bytes.extend_from_slice(format!("{} 0 obj\n", cidfont_id).as_bytes());
407 bytes.extend_from_slice(
408 generate_cidfont_dict(font, *descriptor_id, *cidtogidmap_id).as_bytes(),
409 );
410 bytes.extend_from_slice(b"\nendobj\n");
411
412 xref_offsets.push(bytes.len());
414 bytes.extend_from_slice(format!("{} 0 obj\n", type0_dict_id).as_bytes());
415 bytes.extend_from_slice(
416 generate_font_dictionary(font, *cidfont_id, Some(*to_unicode_id)).as_bytes(),
417 );
418 bytes.extend_from_slice(b"\nendobj\n");
419
420 let cmap_content = generate_to_unicode_cmap(font);
422 xref_offsets.push(bytes.len());
423 bytes.extend_from_slice(format!("{} 0 obj\n", to_unicode_id).as_bytes());
424 bytes.extend_from_slice(b"<<\n/Length ");
425 bytes.extend_from_slice(cmap_content.len().to_string().as_bytes());
426 bytes.extend_from_slice(b"\n>>\nstream\n");
427 bytes.extend_from_slice(cmap_content.as_bytes());
428 bytes.extend_from_slice(b"\nendstream\nendobj\n");
429
430 let used_chars = if let Some(subsetter) = self.font_manager.get_subsetter(font_idx)
433 {
434 subsetter.used_chars()
435 } else {
436 &std::collections::BTreeSet::new()
437 };
438
439 let cidtogidmap_data = crate::pdf::cidfont::generate_cidtogidmap_stream(
440 &font.char_to_glyph,
441 used_chars,
442 );
443
444 xref_offsets.push(bytes.len());
445 bytes.extend_from_slice(format!("{} 0 obj\n", cidtogidmap_id).as_bytes());
446 bytes.extend_from_slice(b"<<\n/Length ");
447 bytes.extend_from_slice(cidtogidmap_data.len().to_string().as_bytes());
448 bytes.extend_from_slice(b"\n>>\nstream\n");
449 bytes.extend_from_slice(&cidtogidmap_data);
450 bytes.extend_from_slice(b"\nendstream\nendobj\n");
451 }
452 }
453
454 for (img_idx, xobject) in self.image_xobjects.iter().enumerate() {
456 let obj_id = first_image_obj_id + img_idx;
457 xref_offsets.push(bytes.len());
458
459 let stream_header = xobject.to_pdf_stream(obj_id as u32);
461 bytes.extend_from_slice(stream_header.as_bytes());
462
463 bytes.extend_from_slice(xobject.stream_data());
465
466 bytes.extend_from_slice(ImageXObject::stream_end().as_bytes());
468 }
469
470 for (grad_idx, pdf_gradient) in self.gradients.iter().enumerate() {
472 let function_obj_id = first_gradient_obj_id + grad_idx * 2;
473 let shading_obj_id = function_obj_id + 1;
474
475 write_gradient_objects(
477 &pdf_gradient.gradient,
478 function_obj_id,
479 shading_obj_id,
480 &mut bytes,
481 &mut xref_offsets,
482 );
483 }
484
485 for (gs_idx, ext_g_state) in self.ext_g_states.iter().enumerate() {
487 let obj_id = first_ext_g_state_obj_id + gs_idx;
488 xref_offsets.push(bytes.len());
489 bytes.extend_from_slice(format!("{} 0 obj\n", obj_id).as_bytes());
490 bytes.extend_from_slice(b"<<\n");
491 bytes.extend_from_slice(b"/Type /ExtGState\n");
492 bytes.extend_from_slice(format!("/ca {:.3}\n", ext_g_state.fill_opacity).as_bytes());
493 bytes.extend_from_slice(format!("/CA {:.3}\n", ext_g_state.stroke_opacity).as_bytes());
494 bytes.extend_from_slice(b">>\n");
495 bytes.extend_from_slice(b"endobj\n");
496 }
497
498 let mut current_annotation_obj_id = first_annotation_obj_id;
500 for (page_idx, page) in self.pages.iter().enumerate() {
501 let page_obj_id = first_page_obj_id + page_idx * 2;
502 let content_obj_id = page_obj_id + 1;
503
504 xref_offsets.push(bytes.len());
506 bytes.extend_from_slice(format!("{} 0 obj\n", page_obj_id).as_bytes());
507 bytes.extend_from_slice(b"<<\n");
508 bytes.extend_from_slice(b"/Type /Page\n");
509 bytes.extend_from_slice(b"/Parent 2 0 R\n");
510 bytes.extend_from_slice(
511 format!(
512 "/MediaBox [0 0 {} {}]\n",
513 page.width.to_pt(),
514 page.height.to_pt()
515 )
516 .as_bytes(),
517 );
518 bytes.extend_from_slice(b"/Resources <<\n");
519
520 bytes.extend_from_slice(b" /Font <<\n");
522 bytes.extend_from_slice(format!(" /F1 {} 0 R\n", font_obj_id).as_bytes());
523
524 if num_embedded_fonts > 0 {
526 for font_idx in 0..num_embedded_fonts {
527 let type0_dict_obj_id = first_embedded_font_obj_id + font_idx * 6 + 3; bytes.extend_from_slice(
529 format!(" /F{} {} 0 R\n", font_idx + 2, type0_dict_obj_id).as_bytes(),
530 );
531 }
532 }
533 bytes.extend_from_slice(b" >>\n");
534
535 if !self.image_xobjects.is_empty() {
537 bytes.extend_from_slice(b" /XObject <<\n");
538 for img_idx in 0..self.image_xobjects.len() {
539 let obj_id = first_image_obj_id + img_idx;
540 bytes.extend_from_slice(
541 format!(" /Im{} {} 0 R\n", img_idx, obj_id).as_bytes(),
542 );
543 }
544 bytes.extend_from_slice(b" >>\n");
545 }
546
547 if !self.gradients.is_empty() {
549 bytes.extend_from_slice(b" /Shading <<\n");
550 for grad_idx in 0..self.gradients.len() {
551 let shading_obj_id = first_gradient_obj_id + grad_idx * 2 + 1; bytes.extend_from_slice(
553 format!(" /Sh{} {} 0 R\n", grad_idx, shading_obj_id).as_bytes(),
554 );
555 }
556 bytes.extend_from_slice(b" >>\n");
557 }
558
559 if !self.ext_g_states.is_empty() {
561 bytes.extend_from_slice(b" /ExtGState <<\n");
562 for gs_idx in 0..self.ext_g_states.len() {
563 let gs_obj_id = first_ext_g_state_obj_id + gs_idx;
564 bytes.extend_from_slice(
565 format!(" /GS{} {} 0 R\n", gs_idx, gs_obj_id).as_bytes(),
566 );
567 }
568 bytes.extend_from_slice(b" >>\n");
569 }
570
571 bytes.extend_from_slice(b">>\n");
572 bytes.extend_from_slice(format!("/Contents {} 0 R\n", content_obj_id).as_bytes());
573
574 if !page.link_annotations.is_empty() {
576 bytes.extend_from_slice(b"/Annots [");
577 for annot_idx in 0..page.link_annotations.len() {
578 bytes.extend_from_slice(
579 format!("{} 0 R ", current_annotation_obj_id + annot_idx).as_bytes(),
580 );
581 }
582 bytes.extend_from_slice(b"]\n");
583 current_annotation_obj_id += page.link_annotations.len();
584 }
585
586 bytes.extend_from_slice(b">>\n");
587 bytes.extend_from_slice(b"endobj\n");
588
589 let stream_data = self.encrypt_stream(&page.content, content_obj_id as u32);
591 xref_offsets.push(bytes.len());
592 bytes.extend_from_slice(format!("{} 0 obj\n", content_obj_id).as_bytes());
593 bytes.extend_from_slice(b"<<\n");
594 bytes.extend_from_slice(format!("/Length {}\n", stream_data.len()).as_bytes());
595 bytes.extend_from_slice(b">>\n");
596 bytes.extend_from_slice(b"stream\n");
597 bytes.extend_from_slice(&stream_data);
598 bytes.extend_from_slice(b"\nendstream\n");
599 bytes.extend_from_slice(b"endobj\n");
600 }
601
602 if total_annotations > 0 {
604 let mut annot_obj_id = first_annotation_obj_id;
605 for (page_idx, page) in self.pages.iter().enumerate() {
606 let page_obj_id = first_page_obj_id + page_idx * 2;
607
608 for annot in &page.link_annotations {
609 xref_offsets.push(bytes.len());
610 bytes.extend_from_slice(format!("{} 0 obj\n", annot_obj_id).as_bytes());
611 bytes.extend_from_slice(b"<<\n");
612 bytes.extend_from_slice(b"/Type /Annot\n");
613 bytes.extend_from_slice(b"/Subtype /Link\n");
614 bytes.extend_from_slice(
615 format!(
616 "/Rect [{:.2} {:.2} {:.2} {:.2}]\n",
617 annot.rect[0], annot.rect[1], annot.rect[2], annot.rect[3]
618 )
619 .as_bytes(),
620 );
621 bytes.extend_from_slice(format!("/P {} 0 R\n", page_obj_id).as_bytes());
622 bytes.extend_from_slice(b"/Border [0 0 0]\n"); match &annot.destination {
626 LinkDestination::External(url) => {
627 bytes.extend_from_slice(b"/A <<\n");
628 bytes.extend_from_slice(b" /S /URI\n");
629 bytes.extend_from_slice(
630 format!(" /URI ({})\n", outline::escape_pdf_string(url))
631 .as_bytes(),
632 );
633 bytes.extend_from_slice(b">>\n");
634 }
635 LinkDestination::Internal(dest_id) => {
636 bytes.extend_from_slice(
639 format!("/Dest ({})\n", outline::escape_pdf_string(dest_id))
640 .as_bytes(),
641 );
642 }
643 }
644
645 bytes.extend_from_slice(b">>\n");
646 bytes.extend_from_slice(b"endobj\n");
647
648 annot_obj_id += 1;
649 }
650 }
651 }
652
653 let xref_offset = bytes.len();
655 bytes.extend_from_slice(b"xref\n");
656 bytes.extend_from_slice(format!("0 {}\n", xref_offsets.len()).as_bytes());
657 bytes.extend_from_slice(b"0000000000 65535 f \n"); for offset in xref_offsets.iter().skip(1) {
659 bytes.extend_from_slice(format!("{:010} 00000 n \n", offset).as_bytes());
660 }
661
662 bytes.extend_from_slice(b"trailer\n");
664 bytes.extend_from_slice(b"<<\n");
665 bytes.extend_from_slice(format!("/Size {}\n", xref_offsets.len()).as_bytes());
666 bytes.extend_from_slice(b"/Root 1 0 R\n");
667
668 if self.encryption.is_some() {
670 bytes.extend_from_slice(format!("/Encrypt {} 0 R\n", encrypt_obj_id).as_bytes());
671 }
672
673 if let Some(ref file_id) = self.file_id {
675 let hex = file_id
676 .iter()
677 .map(|b| format!("{:02X}", b))
678 .collect::<String>();
679 bytes.extend_from_slice(format!("/ID [<{}> <{}>]\n", hex, hex).as_bytes());
680 }
681
682 if self.info.title.is_some()
684 || self.info.author.is_some()
685 || self.info.subject.is_some()
686 || self.info.creation_date.is_some()
687 {
688 bytes.extend_from_slice(b"/Info <<\n");
689
690 if let Some(ref title) = self.info.title {
691 bytes.extend_from_slice(format!(" /Title ({})\n", title).as_bytes());
692 }
693
694 if let Some(ref author) = self.info.author {
695 bytes.extend_from_slice(format!(" /Author ({})\n", author).as_bytes());
696 }
697
698 if let Some(ref subject) = self.info.subject {
699 bytes.extend_from_slice(format!(" /Subject ({})\n", subject).as_bytes());
700 }
701
702 if let Some(ref creation_date) = self.info.creation_date {
703 bytes
704 .extend_from_slice(format!(" /CreationDate ({})\n", creation_date).as_bytes());
705 }
706
707 bytes.extend_from_slice(b">>\n");
708 }
709
710 bytes.extend_from_slice(b">>\n");
711 bytes.extend_from_slice(b"startxref\n");
712 bytes.extend_from_slice(format!("{}\n", xref_offset).as_bytes());
713 bytes.extend_from_slice(b"%%EOF\n");
714
715 Ok(bytes)
716 }
717}
718
719impl Default for PdfDocument {
720 fn default() -> Self {
721 Self::new()
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_pdf_document_creation() {
731 let doc = PdfDocument::new();
732 assert_eq!(doc.version, "1.4");
733 assert_eq!(doc.pages.len(), 0);
734 }
735
736 #[test]
737 fn test_pdf_page() {
738 let mut page = PdfPage::new(
739 fop_types::Length::from_mm(210.0),
740 fop_types::Length::from_mm(297.0),
741 );
742
743 page.add_text(
744 "Hello World",
745 fop_types::Length::from_pt(100.0),
746 fop_types::Length::from_pt(700.0),
747 fop_types::Length::from_pt(12.0),
748 );
749
750 assert!(!page.content.is_empty());
751 let content_str = String::from_utf8_lossy(&page.content);
752 assert!(content_str.contains("Hello World"));
753 assert!(content_str.contains("BT")); assert!(content_str.contains("ET")); }
756
757 #[test]
758 fn test_pdf_bytes() {
759 let doc = PdfDocument::new();
760 let bytes = doc.to_bytes().expect("test: should succeed");
761
762 let header = String::from_utf8_lossy(&bytes[..8]);
763 assert!(header.starts_with("%PDF-"));
764 }
765
766 #[test]
767 fn test_pdf_encrypted_bytes() {
768 use crate::pdf::security::{generate_file_id, PdfPermissions, PdfSecurity};
769
770 let mut doc = PdfDocument::new();
771
772 let mut page = PdfPage::new(
774 fop_types::Length::from_mm(210.0),
775 fop_types::Length::from_mm(297.0),
776 );
777 page.add_text(
778 "Secret Text",
779 fop_types::Length::from_pt(100.0),
780 fop_types::Length::from_pt(700.0),
781 fop_types::Length::from_pt(12.0),
782 );
783 doc.add_page(page);
784
785 let permissions = PdfPermissions {
787 allow_print: false,
788 allow_copy: false,
789 ..Default::default()
790 };
791 let security = PdfSecurity::new("owner123", "user456", permissions);
792 let file_id = generate_file_id("test-encrypted");
793 let encryption_dict = security.compute_encryption_dict(&file_id);
794 doc.set_encryption(encryption_dict, file_id)
795 .expect("test: should succeed");
796
797 let bytes = doc.to_bytes().expect("test: should succeed");
798 let content = String::from_utf8_lossy(&bytes);
799
800 assert!(content.contains("%PDF-"));
802 assert!(content.contains("/Filter /Standard"));
803 assert!(content.contains("/V 2")); assert!(content.contains("/R 3")); assert!(content.contains("/Length 128"));
806 assert!(content.contains("/Encrypt")); assert!(content.contains("/ID [<")); assert!(!content.contains("Secret Text"));
811 }
812
813 #[test]
814 fn test_pdf_without_encryption_has_plaintext() {
815 let mut doc = PdfDocument::new();
816 let mut page = PdfPage::new(
817 fop_types::Length::from_mm(210.0),
818 fop_types::Length::from_mm(297.0),
819 );
820 page.add_text(
821 "Visible Text",
822 fop_types::Length::from_pt(100.0),
823 fop_types::Length::from_pt(700.0),
824 fop_types::Length::from_pt(12.0),
825 );
826 doc.add_page(page);
827
828 let bytes = doc.to_bytes().expect("test: should succeed");
829 let content = String::from_utf8_lossy(&bytes);
830
831 assert!(content.contains("Visible Text"));
833 assert!(!content.contains("/Encrypt"));
835 assert!(!content.contains("/Filter /Standard"));
836 }
837}
838
839#[cfg(test)]
840mod tests_extended {
841 use super::*;
842 use crate::pdf::compliance::PdfCompliance;
843 use crate::pdf::security::{generate_file_id, PdfPermissions, PdfSecurity};
844 use fop_types::Length;
845
846 #[test]
847 fn test_pdf_document_default() {
848 let doc = PdfDocument::default();
849 assert_eq!(doc.version, "1.4");
850 assert!(doc.pages.is_empty());
851 }
852
853 #[test]
854 fn test_pdf_document_add_multiple_pages() {
855 let mut doc = PdfDocument::new();
856 for _ in 0..3 {
857 let page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
858 doc.add_page(page);
859 }
860 assert_eq!(doc.pages.len(), 3);
861 }
862
863 #[test]
864 fn test_pdf_page_new_has_correct_dimensions() {
865 let page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
866 assert_eq!(page.width, Length::from_mm(210.0));
867 assert_eq!(page.height, Length::from_mm(297.0));
868 assert!(page.content.is_empty());
869 }
870
871 #[test]
872 fn test_pdf_page_add_text_generates_bt_et() {
873 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
874 page.add_text(
875 "Test",
876 Length::from_pt(72.0),
877 Length::from_pt(700.0),
878 Length::from_pt(12.0),
879 );
880 let content = String::from_utf8_lossy(&page.content);
881 assert!(content.contains("BT"));
882 assert!(content.contains("ET"));
883 }
884
885 #[test]
886 fn test_pdf_page_add_background() {
887 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
888 page.add_background(
889 Length::ZERO,
890 Length::ZERO,
891 Length::from_pt(595.0),
892 Length::from_pt(842.0),
893 fop_types::Color::WHITE,
894 );
895 let content = String::from_utf8_lossy(&page.content);
896 assert!(content.contains("re f"));
898 }
899
900 #[test]
901 fn test_pdf_compliance_pdfa1b_adds_version_info() {
902 let mut doc = PdfDocument::new();
903 doc.set_compliance(PdfCompliance::PdfA1b)
904 .expect("test: should succeed");
905 let bytes = doc.to_bytes().expect("test: should succeed");
906 let content = String::from_utf8_lossy(&bytes);
907 assert!(content.contains("%PDF-1.4"));
909 }
910
911 #[test]
912 fn test_pdf_document_to_bytes_starts_with_header() {
913 let doc = PdfDocument::new();
914 let bytes = doc.to_bytes().expect("test: should succeed");
915 assert!(bytes.starts_with(b"%PDF-"));
916 }
917
918 #[test]
919 fn test_pdf_document_to_bytes_ends_with_eof() {
920 let doc = PdfDocument::new();
921 let bytes = doc.to_bytes().expect("test: should succeed");
922 let content = String::from_utf8_lossy(&bytes);
923 assert!(content.contains("%%EOF"));
924 }
925
926 #[test]
927 fn test_pdf_document_aes256_encryption() {
928 let mut doc = PdfDocument::new();
929 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
930 page.add_text(
931 "Private",
932 Length::from_pt(72.0),
933 Length::from_pt(700.0),
934 Length::from_pt(12.0),
935 );
936 doc.add_page(page);
937
938 let sec = PdfSecurity::new_aes256("owner", "user", PdfPermissions::default());
939 let file_id = generate_file_id("aes-doc");
940 let dict = sec.compute_encryption_dict(&file_id);
941 doc.set_encryption(dict, file_id)
942 .expect("test: should succeed");
943
944 let bytes = doc.to_bytes().expect("test: should succeed");
945 let content = String::from_utf8_lossy(&bytes);
946 assert!(content.contains("/V 5")); assert!(content.contains("/R 6")); assert!(content.contains("/OE <")); }
950
951 #[test]
952 fn test_pdf_outline_structure() {
953 let mut doc = PdfDocument::new();
954 let outline = PdfOutline {
955 items: vec![
956 PdfOutlineItem {
957 title: "Chapter 1".to_string(),
958 page_index: Some(0),
959 external_destination: None,
960 children: vec![],
961 },
962 PdfOutlineItem {
963 title: "Chapter 2".to_string(),
964 page_index: Some(1),
965 external_destination: None,
966 children: vec![],
967 },
968 ],
969 };
970 doc.set_outline(outline);
971
972 for _ in 0..2 {
974 let page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
975 doc.add_page(page);
976 }
977
978 let bytes = doc.to_bytes().expect("test: should succeed");
979 let content = String::from_utf8_lossy(&bytes);
980 assert!(content.contains("Chapter 1"));
981 assert!(content.contains("Chapter 2"));
982 assert!(content.contains("/Outlines"));
983 }
984
985 #[test]
986 fn test_pdf_page_add_rule() {
987 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
988 page.add_rule(
989 Length::from_pt(50.0),
990 Length::from_pt(400.0),
991 Length::from_pt(400.0),
992 Length::from_pt(2.0),
993 fop_types::Color::BLACK,
994 "solid",
995 );
996 let content = String::from_utf8_lossy(&page.content);
997 assert!(!content.is_empty());
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests_document_comprehensive {
1004 use super::*;
1005 use fop_types::Length;
1006
1007 #[test]
1010 fn test_new_produces_non_empty_output() {
1011 let doc = PdfDocument::new();
1012 let bytes = doc.to_bytes().expect("test: should succeed");
1013 assert!(!bytes.is_empty());
1014 }
1015
1016 #[test]
1017 fn test_new_version_is_1_4() {
1018 let doc = PdfDocument::new();
1019 assert_eq!(doc.version, "1.4");
1020 }
1021
1022 #[test]
1023 fn test_new_has_no_pages() {
1024 let doc = PdfDocument::new();
1025 assert_eq!(doc.pages.len(), 0);
1026 }
1027
1028 #[test]
1029 fn test_new_has_no_images() {
1030 let doc = PdfDocument::new();
1031 assert_eq!(doc.image_xobjects.len(), 0);
1032 }
1033
1034 #[test]
1035 fn test_new_has_no_outline() {
1036 let doc = PdfDocument::new();
1037 assert!(doc.outline.is_none());
1038 }
1039
1040 #[test]
1043 fn test_pdf_header_starts_with_pdf_1_4() {
1044 let doc = PdfDocument::new();
1045 let bytes = doc.to_bytes().expect("test: should succeed");
1046 assert!(bytes.starts_with(b"%PDF-1.4"));
1047 }
1048
1049 #[test]
1050 fn test_pdf_header_present_in_output() {
1051 let doc = PdfDocument::new();
1052 let bytes = doc.to_bytes().expect("test: should succeed");
1053 let s = String::from_utf8_lossy(&bytes);
1054 assert!(s.contains("%PDF-"));
1055 }
1056
1057 #[test]
1060 fn test_page_count_zero_pages_in_catalog() {
1061 let doc = PdfDocument::new();
1062 let bytes = doc.to_bytes().expect("test: should succeed");
1063 let s = String::from_utf8_lossy(&bytes);
1064 assert!(s.contains("/Count 0"));
1065 }
1066
1067 #[test]
1068 fn test_page_count_one_page_in_catalog() {
1069 let mut doc = PdfDocument::new();
1070 doc.add_page(PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0)));
1071 let bytes = doc.to_bytes().expect("test: should succeed");
1072 let s = String::from_utf8_lossy(&bytes);
1073 assert!(s.contains("/Count 1"));
1074 }
1075
1076 #[test]
1077 fn test_page_count_three_pages_in_catalog() {
1078 let mut doc = PdfDocument::new();
1079 for _ in 0..3 {
1080 doc.add_page(PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0)));
1081 }
1082 let bytes = doc.to_bytes().expect("test: should succeed");
1083 let s = String::from_utf8_lossy(&bytes);
1084 assert!(s.contains("/Count 3"));
1085 assert_eq!(doc.pages.len(), 3);
1086 }
1087
1088 #[test]
1091 fn test_info_title_appears_in_output() {
1092 let mut doc = PdfDocument::new();
1093 doc.info.title = Some("My Test Document".to_string());
1094 let bytes = doc.to_bytes().expect("test: should succeed");
1095 let s = String::from_utf8_lossy(&bytes);
1096 assert!(s.contains("/Title (My Test Document)"));
1097 }
1098
1099 #[test]
1100 fn test_info_author_appears_in_output() {
1101 let mut doc = PdfDocument::new();
1102 doc.info.author = Some("Jane Doe".to_string());
1103 let bytes = doc.to_bytes().expect("test: should succeed");
1104 let s = String::from_utf8_lossy(&bytes);
1105 assert!(s.contains("/Author (Jane Doe)"));
1106 }
1107
1108 #[test]
1109 fn test_info_subject_appears_in_output() {
1110 let mut doc = PdfDocument::new();
1111 doc.info.subject = Some("Unit Testing".to_string());
1112 let bytes = doc.to_bytes().expect("test: should succeed");
1113 let s = String::from_utf8_lossy(&bytes);
1114 assert!(s.contains("/Subject (Unit Testing)"));
1115 }
1116
1117 #[test]
1118 fn test_info_creation_date_appears_in_output() {
1119 let mut doc = PdfDocument::new();
1120 doc.info.creation_date = Some("D:20260220120000".to_string());
1121 let bytes = doc.to_bytes().expect("test: should succeed");
1122 let s = String::from_utf8_lossy(&bytes);
1123 assert!(s.contains("/CreationDate (D:20260220120000)"));
1124 }
1125
1126 #[test]
1127 fn test_info_lang_field_roundtrip() {
1128 let mut info = PdfInfo::default();
1129 assert!(info.lang.is_none());
1130 info.lang = Some("ja".to_string());
1131 assert_eq!(info.lang.as_deref(), Some("ja"));
1132 }
1133
1134 #[test]
1135 fn test_info_no_metadata_omits_info_dict() {
1136 let doc = PdfDocument::new();
1138 let bytes = doc.to_bytes().expect("test: should succeed");
1139 let s = String::from_utf8_lossy(&bytes);
1140 assert!(!s.contains("/Info <<"));
1141 }
1142
1143 #[test]
1144 fn test_info_all_fields_set() {
1145 let mut doc = PdfDocument::new();
1146 doc.info.title = Some("Full Meta".to_string());
1147 doc.info.author = Some("Author A".to_string());
1148 doc.info.subject = Some("Subject S".to_string());
1149 doc.info.creation_date = Some("D:20260101".to_string());
1150 let bytes = doc.to_bytes().expect("test: should succeed");
1151 let s = String::from_utf8_lossy(&bytes);
1152 assert!(s.contains("/Title (Full Meta)"));
1153 assert!(s.contains("/Author (Author A)"));
1154 assert!(s.contains("/Subject (Subject S)"));
1155 assert!(s.contains("/CreationDate (D:20260101)"));
1156 }
1157
1158 #[test]
1161 fn test_xref_table_present() {
1162 let doc = PdfDocument::new();
1163 let bytes = doc.to_bytes().expect("test: should succeed");
1164 let s = String::from_utf8_lossy(&bytes);
1165 assert!(s.contains("xref\n"));
1166 }
1167
1168 #[test]
1169 fn test_xref_free_object_zero() {
1170 let doc = PdfDocument::new();
1171 let bytes = doc.to_bytes().expect("test: should succeed");
1172 let s = String::from_utf8_lossy(&bytes);
1173 assert!(s.contains("0000000000 65535 f "));
1175 }
1176
1177 #[test]
1178 fn test_xref_entries_use_n_type() {
1179 let doc = PdfDocument::new();
1180 let bytes = doc.to_bytes().expect("test: should succeed");
1181 let s = String::from_utf8_lossy(&bytes);
1182 assert!(s.contains(" 00000 n "));
1184 }
1185
1186 #[test]
1189 fn test_trailer_has_root_reference() {
1190 let doc = PdfDocument::new();
1191 let bytes = doc.to_bytes().expect("test: should succeed");
1192 let s = String::from_utf8_lossy(&bytes);
1193 assert!(s.contains("/Root 1 0 R"));
1194 }
1195
1196 #[test]
1197 fn test_trailer_has_size_entry() {
1198 let doc = PdfDocument::new();
1199 let bytes = doc.to_bytes().expect("test: should succeed");
1200 let s = String::from_utf8_lossy(&bytes);
1201 assert!(s.contains("/Size "));
1203 }
1204
1205 #[test]
1208 fn test_startxref_keyword_present() {
1209 let doc = PdfDocument::new();
1210 let bytes = doc.to_bytes().expect("test: should succeed");
1211 let s = String::from_utf8_lossy(&bytes);
1212 assert!(s.contains("startxref\n"));
1213 }
1214
1215 #[test]
1216 fn test_startxref_offset_is_nonzero() {
1217 let doc = PdfDocument::new();
1218 let bytes = doc.to_bytes().expect("test: should succeed");
1219 let s = String::from_utf8_lossy(&bytes);
1220 let idx = s.find("startxref\n").expect("test: should succeed");
1222 let after = &s[idx + "startxref\n".len()..];
1223 let offset_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
1224 let offset: usize = offset_str.parse().expect("test: should succeed");
1225 assert!(offset > 0);
1226 }
1227
1228 #[test]
1229 fn test_eof_marker_present() {
1230 let doc = PdfDocument::new();
1231 let bytes = doc.to_bytes().expect("test: should succeed");
1232 let s = String::from_utf8_lossy(&bytes);
1233 assert!(s.contains("%%EOF"));
1234 }
1235
1236 #[test]
1239 fn test_pdfinfo_default_all_none() {
1240 let info = PdfInfo::default();
1241 assert!(info.title.is_none());
1242 assert!(info.author.is_none());
1243 assert!(info.subject.is_none());
1244 assert!(info.creation_date.is_none());
1245 assert!(info.lang.is_none());
1246 }
1247
1248 #[test]
1249 fn test_pdfinfo_clone() {
1250 let info = PdfInfo {
1251 title: Some("Clone Me".to_string()),
1252 ..Default::default()
1253 };
1254 let cloned = info.clone();
1255 assert_eq!(cloned.title.as_deref(), Some("Clone Me"));
1256 }
1257
1258 #[test]
1261 fn test_file_id_appears_in_trailer_as_id_array() {
1262 use crate::pdf::security::generate_file_id;
1263 let mut doc = PdfDocument::new();
1264 let fid = generate_file_id("id-test");
1265 doc.file_id = Some(fid);
1267 let bytes = doc.to_bytes().expect("test: should succeed");
1268 let s = String::from_utf8_lossy(&bytes);
1269 assert!(s.contains("/ID [<"));
1270 }
1271
1272 #[test]
1275 fn test_add_ext_g_state_deduplication() {
1276 let mut doc = PdfDocument::new();
1277 let idx1 = doc.add_ext_g_state(0.5, 0.5);
1278 let idx2 = doc.add_ext_g_state(0.5, 0.5);
1279 assert_eq!(idx1, idx2);
1280 assert_eq!(doc.ext_g_states.len(), 1);
1281 }
1282
1283 #[test]
1284 fn test_add_ext_g_state_different_values_creates_two() {
1285 let mut doc = PdfDocument::new();
1286 let idx1 = doc.add_ext_g_state(0.3, 0.3);
1287 let idx2 = doc.add_ext_g_state(0.7, 0.7);
1288 assert_ne!(idx1, idx2);
1289 assert_eq!(doc.ext_g_states.len(), 2);
1290 }
1291
1292 #[test]
1295 fn test_add_gradient_returns_index() {
1296 use fop_types::{Color, ColorStop, Gradient, Length, Point};
1297 let mut doc = PdfDocument::new();
1298 let gradient = Gradient::linear(
1299 Point::new(Length::from_pt(0.0), Length::from_pt(0.0)),
1300 Point::new(Length::from_pt(100.0), Length::from_pt(0.0)),
1301 vec![
1302 ColorStop::new(0.0, Color::BLACK),
1303 ColorStop::new(1.0, Color::WHITE),
1304 ],
1305 );
1306 let idx = doc.add_gradient(gradient);
1307 assert_eq!(idx, 0);
1308 assert_eq!(doc.gradients.len(), 1);
1309 }
1310
1311 #[test]
1314 fn test_catalog_type_present() {
1315 let doc = PdfDocument::new();
1316 let bytes = doc.to_bytes().expect("test: should succeed");
1317 let s = String::from_utf8_lossy(&bytes);
1318 assert!(s.contains("/Type /Catalog"));
1319 }
1320
1321 #[test]
1322 fn test_catalog_pages_reference_present() {
1323 let doc = PdfDocument::new();
1324 let bytes = doc.to_bytes().expect("test: should succeed");
1325 let s = String::from_utf8_lossy(&bytes);
1326 assert!(s.contains("/Pages 2 0 R"));
1327 }
1328
1329 #[test]
1332 fn test_pdfpage_new_empty_content() {
1333 let page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1334 assert!(page.content.is_empty());
1335 assert!(page.link_annotations.is_empty());
1336 }
1337
1338 #[test]
1339 fn test_pdfpage_add_text_with_spacing_produces_tc_tw() {
1340 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1341 page.add_text_with_spacing(
1342 "Hello",
1343 Length::from_pt(72.0),
1344 Length::from_pt(700.0),
1345 Length::from_pt(12.0),
1346 Some(Length::from_pt(1.0)),
1347 Some(Length::from_pt(2.0)),
1348 );
1349 let content = String::from_utf8_lossy(&page.content);
1350 assert!(content.contains("Tc"));
1351 assert!(content.contains("Tw"));
1352 }
1353
1354 #[test]
1355 fn test_pdfpage_add_background_generates_rg_and_re() {
1356 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1357 page.add_background(
1358 Length::from_pt(10.0),
1359 Length::from_pt(10.0),
1360 Length::from_pt(200.0),
1361 Length::from_pt(100.0),
1362 fop_types::Color::rgb(255, 0, 0),
1363 );
1364 let content = String::from_utf8_lossy(&page.content);
1365 assert!(content.contains("rg"));
1367 assert!(content.contains("re f"));
1368 }
1369
1370 #[test]
1371 fn test_pdfpage_add_link_annotation_stores_annotation() {
1372 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1373 page.add_link_annotation(
1374 Length::from_pt(50.0),
1375 Length::from_pt(700.0),
1376 Length::from_pt(100.0),
1377 Length::from_pt(12.0),
1378 LinkDestination::External("https://example.com".to_string()),
1379 );
1380 assert_eq!(page.link_annotations.len(), 1);
1381 }
1382
1383 #[test]
1384 fn test_pdfpage_link_annotation_rect_values() {
1385 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1386 page.add_link_annotation(
1387 Length::from_pt(10.0),
1388 Length::from_pt(20.0),
1389 Length::from_pt(80.0),
1390 Length::from_pt(14.0),
1391 LinkDestination::Internal("section-1".to_string()),
1392 );
1393 let ann = &page.link_annotations[0];
1394 assert!((ann.rect[0] - 10.0).abs() < 0.01);
1396 assert!((ann.rect[1] - 20.0).abs() < 0.01);
1397 assert!((ann.rect[2] - 90.0).abs() < 0.01);
1398 assert!((ann.rect[3] - 34.0).abs() < 0.01);
1399 }
1400
1401 #[test]
1402 fn test_pdfpage_multiple_texts_accumulate_in_content() {
1403 let mut page = PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
1404 page.add_text(
1405 "First",
1406 Length::from_pt(72.0),
1407 Length::from_pt(700.0),
1408 Length::from_pt(12.0),
1409 );
1410 page.add_text(
1411 "Second",
1412 Length::from_pt(72.0),
1413 Length::from_pt(680.0),
1414 Length::from_pt(12.0),
1415 );
1416 let content = String::from_utf8_lossy(&page.content);
1417 assert!(content.contains("First"));
1418 assert!(content.contains("Second"));
1419 }
1420
1421 #[test]
1424 fn test_pdf_value_boolean() {
1425 let v = PdfValue::Boolean(true);
1426 if let PdfValue::Boolean(b) = v {
1427 assert!(b);
1428 } else {
1429 panic!("Expected Boolean");
1430 }
1431 }
1432
1433 #[test]
1434 fn test_pdf_value_integer() {
1435 let v = PdfValue::Integer(42);
1436 if let PdfValue::Integer(n) = v {
1437 assert_eq!(n, 42);
1438 } else {
1439 panic!("Expected Integer");
1440 }
1441 }
1442
1443 #[test]
1444 #[allow(clippy::approx_constant)]
1445 fn test_pdf_value_real() {
1446 let v = PdfValue::Real(3.14);
1447 if let PdfValue::Real(f) = v {
1448 assert!((f - 3.14).abs() < f64::EPSILON);
1449 } else {
1450 panic!("Expected Real");
1451 }
1452 }
1453
1454 #[test]
1455 fn test_pdf_value_name() {
1456 let v = PdfValue::Name("Font".to_string());
1457 if let PdfValue::Name(s) = v {
1458 assert_eq!(s, "Font");
1459 } else {
1460 panic!("Expected Name");
1461 }
1462 }
1463
1464 #[test]
1465 fn test_pdf_value_null() {
1466 let v = PdfValue::Null;
1467 assert!(matches!(v, PdfValue::Null));
1468 }
1469
1470 #[test]
1473 fn test_set_compliance_pdfa_with_encryption_returns_error() {
1474 use crate::pdf::compliance::PdfCompliance;
1475 use crate::pdf::security::{generate_file_id, PdfPermissions, PdfSecurity};
1476 let mut doc = PdfDocument::new();
1477 let sec = PdfSecurity::new("owner", "user", PdfPermissions::default());
1478 let fid = generate_file_id("enc");
1479 let dict = sec.compute_encryption_dict(&fid);
1480 doc.set_encryption(dict, fid).expect("test: should succeed");
1481 let result = doc.set_compliance(PdfCompliance::PdfA1b);
1482 assert!(result.is_err());
1483 }
1484}