//! Generate a sample PDF exercising every Phase 4 feature. //! //! Run with: //! //! ```text //! cargo run -p nigig-pdf-graphics --example generate_sample -- out.pdf //! ``` //! //! The output is meant to be opened in a real viewer. Phase 4's exit //! criterion is "generated PDFs open cleanly in external viewers", which no //! test in this repository can assert on its own. use std::collections::BTreeSet; use nigig_pdf_cos::encrypt_write::EncryptionSettings; use nigig_pdf_cos::writer::{ Attachment, DocumentMetadata, OutlineItem, PageLabelRange, PageLabelStyle, PageMode, ViewerPreferences, }; use nigig_pdf_graphics::content_writer::ContentWriter; use nigig_pdf_graphics::create::{DocumentCreator, NewField, NewFieldKind}; fn main() { let out = std::env::args() .nth(1) .unwrap_or_else(|| "nigig-phase4-sample.pdf".to_string()); let font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; let mut creator = DocumentCreator::new(); creator.add_standard_font("Helv", "Helvetica"); let heading = "Nigig PDF — Phase 4"; let body = "Generated by nigig-pdf with an embedded DejaVu Sans subset."; let embedded = std::fs::read(font_path).ok().and_then(|data| { let chars: BTreeSet = heading.chars().chain(body.chars()).collect(); creator.embed_truetype("F1", &data, &chars).ok() }); // Page 1: text, colour and vector graphics. let mut cw = ContentWriter::new(); cw.rgb_fill(0.10, 0.20, 0.45); cw.rectangle(0.0, 742.0, 595.0, 50.0); cw.fill(); if let Some(font) = &embedded { cw.begin_text(); cw.rgb_fill(1.0, 1.0, 1.0); cw.set_font("F1", 22.0); cw.set_text_matrix(1.0, 0.0, 0.0, 1.0, 40.0, 758.0); cw.show_glyph_hex(&font.encode(heading)); cw.end_text(); cw.begin_text(); cw.rgb_fill(0.0, 0.0, 0.0); cw.set_font("F1", 12.0); cw.set_text_matrix(1.0, 0.0, 0.0, 1.0, 40.0, 700.0); cw.show_glyph_hex(&font.encode(body)); cw.end_text(); } cw.begin_text(); cw.rgb_fill(0.0, 0.0, 0.0); cw.set_font("Helv", 11.0); cw.text_at( 40.0, 676.0, "This line uses base-14 Helvetica, not embedded.", ); cw.end_text(); cw.rgb_stroke(0.8, 0.2, 0.2); cw.set_stroke_width(2.0); cw.rounded_rect(40.0, 560.0, 200.0, 80.0, 8.0); cw.stroke(); cw.rgb_fill(0.2, 0.6, 0.3); cw.ellipse(400.0, 600.0, 60.0, 40.0); cw.fill(); creator.builder().add_page(595.0, 792.0, &cw.build()); // Page 3 content is built here so the CFF font is registered before // the pages that use it; page order is unaffected. let cff_path = concat!( env!("CARGO_MANIFEST_DIR"), "/../tests/corpus/fonts/cff_sample.otf" ); let cff_text = "Hello CFF 123"; let cff_font = std::fs::read(cff_path).ok().and_then(|data| { let chars: BTreeSet = cff_text.chars().collect(); creator.embed_opentype_whole("C1", &data, &chars).ok() }); // Page 2: the interactive form. let mut p2 = ContentWriter::new(); p2.begin_text(); p2.set_font("Helv", 16.0); p2.text_at(40.0, 720.0, "Interactive form"); p2.end_text(); for (y, label) in [ (652.0, "Full name:"), (600.0, "Notes:"), (505.0, "Agree:"), (465.0, "Colour:"), (417.0, "Country:"), (377.0, "Locked:"), ] { p2.begin_text(); p2.set_font("Helv", 10.0); p2.text_at(40.0, y, label); p2.end_text(); } // Labels beside the radio buttons, placed to the right of each. p2.begin_text(); p2.set_font("Helv", 10.0); p2.text_at(134.0, 464.0, "red"); p2.end_text(); p2.begin_text(); p2.set_font("Helv", 10.0); p2.text_at(224.0, 464.0, "blue"); p2.end_text(); creator.builder().add_page(595.0, 792.0, &p2.build()); creator.add_field(NewField { name: "fullname".to_string(), kind: NewFieldKind::Text { value: "Ada Lovelace".to_string(), multiline: false, max_len: Some(60), }, rect: [110.0, 645.0, 380.0, 668.0], page: 1, read_only: false, required: true, default_appearance: None, }); creator.add_field(NewField { name: "notes".to_string(), kind: NewFieldKind::Text { value: "A multi-line note.".to_string(), multiline: true, max_len: None, }, rect: [110.0, 540.0, 380.0, 615.0], page: 1, read_only: false, required: false, default_appearance: None, }); creator.add_field(NewField { name: "agree".to_string(), kind: NewFieldKind::Checkbox { checked: true }, rect: [110.0, 498.0, 128.0, 516.0], page: 1, read_only: false, required: false, default_appearance: None, }); creator.add_field(NewField { name: "colour".to_string(), kind: NewFieldKind::Radio { options: vec![ ("red".to_string(), [110.0, 458.0, 128.0, 476.0]), ("blue".to_string(), [200.0, 458.0, 218.0, 476.0]), ], selected: Some("blue".to_string()), }, rect: [0.0; 4], page: 1, read_only: false, required: false, default_appearance: None, }); creator.add_field(NewField { name: "country".to_string(), kind: NewFieldKind::Choice { options: vec![ "Kenya".to_string(), "Uganda".to_string(), "Tanzania".to_string(), ], selected: Some("Kenya".to_string()), combo: true, }, rect: [110.0, 410.0, 310.0, 433.0], page: 1, read_only: false, required: false, default_appearance: None, }); creator.add_field(NewField { name: "locked".to_string(), kind: NewFieldKind::Text { value: "read-only".to_string(), multiline: false, max_len: None, }, rect: [110.0, 370.0, 310.0, 393.0], page: 1, read_only: true, required: false, default_appearance: None, }); let builder = creator.builder(); builder.set_metadata(DocumentMetadata { title: Some("Nigig PDF Phase 4 sample".to_string()), author: Some("nigig".to_string()), subject: Some("Document creation".to_string()), keywords: Some("pdf generation phase4".to_string()), creator: Some("nigig-pdf".to_string()), producer: Some("nigig-pdf".to_string()), creation_date: Some("D:20260816120000Z".to_string()), mod_date: None, }); builder.set_outline(vec![ OutlineItem::new("Cover page", 0) .opened() .with_children(vec![OutlineItem::new("Heading", 0).with_top(770.0)]), OutlineItem::new("The form", 1).with_top(740.0), ]); builder.add_named_destination("form", 1, Some(740.0)); builder.add_named_destination("cover", 0, None); builder.add_attachment(Attachment { name: "readme.txt".to_string(), data: b"Generated by nigig-pdf, Phase 4.\n".to_vec(), description: Some("About this file".to_string()), mime_type: Some("text/plain".to_string()), }); builder.set_page_labels(vec![ PageLabelRange { start_page: 0, style: PageLabelStyle::LowerRoman, prefix: None, first: None, }, PageLabelRange { start_page: 1, style: PageLabelStyle::Decimal, prefix: None, first: Some(1), }, ]); builder.set_page_mode(PageMode::UseOutlines); builder.set_viewer_preferences(ViewerPreferences { display_doc_title: Some(true), ..Default::default() }); // Page 3: CFF-flavoured OpenType, embedded whole. Present only when // the fixture is there, so the sample still generates without it. if let Some(font) = &cff_font { let mut p3 = ContentWriter::new(); p3.begin_text(); p3.set_font("Helv", 12.0); p3.text_at(56.0, 740.0, "CFF (Type 1 outlines), embedded whole:"); p3.end_text(); p3.begin_text(); p3.set_font(&font.resource_name, 24.0); p3.set_text_matrix(1.0, 0.0, 0.0, 1.0, 56.0, 690.0); p3.show_glyph_hex(&font.encode(cff_text)); p3.end_text(); creator.builder().add_page(595.0, 792.0, &p3.build()); } let pdf = creator.finish(); std::fs::write(&out, &pdf).expect("write"); // A second, encrypted copy when asked for, so the external-reader // check can verify that a password-protected file we produce is one // other implementations can actually open (Phase 6, ADR 0024). if let Some(encrypted_out) = std::env::args().nth(2) { let mut enc = DocumentCreator::new(); enc.add_standard_font("Helv", "Helvetica"); let mut page = ContentWriter::new(); page.begin_text(); page.set_font("Helv", 14.0); page.text_at(40.0, 700.0, "ENCRYPTEDSAMPLE — opened with the password"); page.end_text(); enc.builder().add_page(595.0, 792.0, &page.build()); enc.builder().set_metadata(DocumentMetadata { title: Some("Nigig PDF encrypted sample".to_string()), ..Default::default() }); enc.builder() .set_encryption(EncryptionSettings::with_password("hunter2")); std::fs::write(&encrypted_out, enc.finish()).expect("write encrypted"); println!("wrote {encrypted_out} (AES-256, password hunter2)"); } println!("wrote {} ({} bytes)", out, pdf.len()); if embedded.is_none() { println!("note: {font_path} was not found, so no font was embedded"); } }