use pdfox::prelude::*;
use pdfox::color::Color;
use std::fs;
fn main() {
std::thread::Builder::new()
.name("pdfox-main".into())
.stack_size(64 * 1024 * 1024)
.spawn(run)
.unwrap()
.join()
.unwrap();
}
fn run() {
println!("pdfox -- building demo PDF...");
let outline = Outline::new()
.add(OutlineItem::new("Typography & Shapes", Destination::Page(0)).bold())
.add(OutlineItem::new("Tables", Destination::Page(1)))
.add(OutlineItem::new("Fonts & Architecture", Destination::Page(2)))
.add(OutlineItem::new("Text Flow", Destination::Page(3)))
.add(OutlineItem::new("Hyperlinks & Colors", Destination::Page(4)))
.add(OutlineItem::new("Interactive Form", Destination::Page(5)))
.add(OutlineItem::new("New Features", Destination::Page(6))
.color(Color::rgb_u8(30, 120, 60)));
let form = AcroForm::new()
.add(FormField::text("full_name", FieldRect::new(160.0, 660.0, 360.0, 22.0), 5)
.tooltip("Your full name").value("Samuel Azeez"))
.add(FormField::text("email", FieldRect::new(160.0, 625.0, 360.0, 22.0), 5)
.tooltip("Email address"))
.add(FormField::multiline_text("bio", FieldRect::new(160.0, 510.0, 360.0, 100.0), 5)
.tooltip("Short bio"))
.add(FormField::checkbox("newsletter", FieldRect::new(160.0, 488.0, 16.0, 16.0), 5, true))
.add(FormField::checkbox("updates", FieldRect::new(160.0, 468.0, 16.0, 16.0), 5, false))
.add(FormField::dropdown("country",
vec!["Sweden".into(), "Kenya".into(), "Germany".into(), "USA".into(), "UK".into()],
FieldRect::new(160.0, 435.0, 200.0, 22.0), 5))
.add(FormField::radio("plan", "plan", "free", FieldRect::new(160.0, 405.0, 14.0, 14.0), 5, true))
.add(FormField::radio("plan", "plan", "pro", FieldRect::new(240.0, 405.0, 14.0, 14.0), 5, false))
.add(FormField::radio("plan", "plan", "enterprise", FieldRect::new(320.0, 405.0, 14.0, 14.0), 5, false))
.add(FormField::button("submit", "Submit", FieldRect::new(160.0, 360.0, 100.0, 28.0), 5)
.bg(Color::rgb_u8(30, 60, 140)).border(Color::rgb_u8(20, 40, 100), 1.0));
let sig = SignatureField::new("doc_signature")
.visible(SignatureAppearance::new(40.0, 40.0, 200.0, 50.0, 5)
.label("Samuel Azeez | pdfox Demo"))
.reason("Document approval")
.location("Nairobi, Kenya")
.contact("samuel@example.com");
let watermark = Watermark::diagonal("DRAFT")
.opacity(0.10)
.color(Color::rgb_u8(80, 80, 180))
.font_size(72.0);
let footer = HeaderFooter::new()
.left(HFSlot::new("pdfox -- Pure Rust PDF", HFAlign::Left)
.color(Color::rgb_u8(100, 100, 140)))
.center(HFSlot::new("https://github.com/sazalo101/pdffox", HFAlign::Center)
.color(Color::rgb_u8(80, 80, 80)).font_size(8.0))
.right(HFSlot::new("Page {page} of {total}", HFAlign::Right)
.color(Color::rgb_u8(100, 100, 140)));
let header = HeaderFooter::new()
.center(HFSlot::new("pdfox -- Pure Rust PDF Library", HFAlign::Center)
.color(Color::rgb_u8(30, 60, 140)).font_size(9.0))
.height(18.0);
let flow_pages = TextFlow::new(FlowStyle::default())
.heading("Text Flow Engine", 1)
.paragraph(
"pdfox includes a multi-page text flow engine that automatically \
reflows content across pages. You define content once and the \
engine handles pagination, margins, and line wrapping automatically."
)
.heading("Feature List", 2)
.bullets(vec![
"Pure Rust -- zero C dependencies, zero unsafe blocks",
"14 built-in Type1 fonts with per-glyph width metrics",
"FlateDecode (zlib) compression on all content streams",
"AcroForm interactive fields: text, checkbox, radio, dropdown",
"Outline bookmark tree with nested chapters and styling",
"Watermarks, header/footer with {page}/{total} macros",
"Encryption (RC4-40/128) with permission flags",
"Digital signature placeholder (adbe.pkcs7.detached)",
])
.heading("Usage Example", 2)
.code(
"let pdf = Document::new()\n \
.title(\"My Report\")\n \
.watermark(Watermark::diagonal(\"DRAFT\"))\n \
.footer(HeaderFooter::new()\n \
.right(HFSlot::new(\"Page {page} of {total}\", HFAlign::Right)))\n \
.page(|p| { /* draw content */ })\n \
.build_signed();"
)
.rule()
.paragraph(
"The library handles all PDF spec concerns: cross-reference tables, \
object streams, proper trailer dictionaries, and compressed content streams."
)
.render(595.276, 841.890);
let (pdf, sig_placeholders) = Document::new()
.title("pdfox Demo")
.author("Samuel Azeez")
.subject("pdfox library feature showcase")
.keywords("rust, pdf, pdfox, pure-rust")
.outline(outline)
.form(form)
.signature(sig)
.watermark(watermark)
.footer(footer)
.header(header)
.page(|p| {
let f = p.use_helvetica();
let reg = p.reg_key(&f);
let bold = p.bold_key(&f);
page_header(p, "pdfox -- Pure Rust PDF Library", "Page 1: Typography & Shapes", &bold, ®);
let y = 745.0;
section_title(p, "Font size showcase", y, &bold);
p.text("Helvetica 24pt -- The quick brown fox jumps over the lazy dog", 40.0, y - 25.0, ®, 24.0, Color::BLACK);
p.text("Helvetica 18pt -- The quick brown fox jumps over the lazy dog", 40.0, y - 50.0, ®, 18.0, Color::BLACK);
p.text("Helvetica 14pt -- The quick brown fox jumps over the lazy dog", 40.0, y - 72.0, ®, 14.0, Color::BLACK);
p.text("Helvetica 10pt -- The quick brown fox jumps over the lazy dog", 40.0, y - 90.0, ®, 10.0, Color::BLACK);
p.text("Helvetica Bold 12pt -- The quick brown fox", 40.0, y - 108.0, &bold, 12.0, Color::BLACK);
let cy = 600.0;
section_title(p, "Color palette", cy + 20.0, &bold);
let colors = [
(Color::rgb_u8(220, 53, 69), "Crimson"),
(Color::rgb_u8(40, 167, 69), "Emerald"),
(Color::rgb_u8(0, 123, 255), "Azure"),
(Color::rgb_u8(255, 193, 7), "Amber"),
(Color::rgb_u8(111, 66, 193), "Violet"),
(Color::rgb_u8(23, 162, 184), "Teal"),
(Color::rgb_u8(108, 117, 125), "Slate"),
(Color::rgb_u8(40, 40, 40), "Onyx"),
];
for (i, (color, name)) in colors.iter().enumerate() {
let x = 40.0 + (i as f64) * 65.0;
p.filled_rect(x, cy - 30.0, 55.0, 30.0, *color);
p.stroked_rect(x, cy - 30.0, 55.0, 30.0, Color::DARK_GRAY, 0.4);
p.text_centered(name, x + 27.5, cy - 40.0, ®, 8.0, Color::DARK_GRAY);
}
let sy = 490.0;
section_title(p, "Geometric primitives", sy + 20.0, &bold);
p.filled_rect(40.0, sy - 50.0, 80.0, 50.0, Color::rgb_u8(0, 123, 255));
p.text_centered("Filled rect", 80.0, sy - 58.0, ®, 8.0, Color::DARK_GRAY);
p.stroked_rect(145.0, sy - 50.0, 80.0, 50.0, Color::rgb_u8(220, 53, 69), 2.0);
p.text_centered("Stroked rect", 185.0, sy - 58.0, ®, 8.0, Color::DARK_GRAY);
p.circle_fill(295.0, sy - 25.0, 25.0, Color::rgb_u8(40, 167, 69));
p.text_centered("Circle", 295.0, sy - 58.0, ®, 8.0, Color::DARK_GRAY);
for (i, lw) in [0.5_f64, 1.0, 2.0, 4.0].iter().enumerate() {
let x = 350.0 + (i as f64) * 40.0;
p.line(x, sy - 5.0, x, sy - 45.0, Color::rgb_u8(30, 60, 140), *lw);
}
p.text("Lines 0.5-4pt", 350.0, sy - 58.0, ®, 8.0, Color::DARK_GRAY);
let wy = 390.0;
section_title(p, "Word-wrapped text box", wy + 20.0, &bold);
p.stroked_rect(40.0, wy - 110.0, 515.0, 110.0, Color::rgb_u8(200, 210, 230), 0.5);
let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod \
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, \
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo \
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse \
cillum dolore eu fugiat nulla pariatur.";
p.text_box(lorem, 48.0, wy - 8.0, 499.0, ®, 11.0, Color::BLACK, 16.0);
})
.page(|p| {
let f = p.use_helvetica();
let reg = p.reg_key(&f);
let bold = p.bold_key(&f);
page_header(p, "Tables", "Page 2: Table rendering", &bold, ®);
section_title(p, "Q1 2025 Sales Report", 748.0, &bold);
let mut sales = Table::new(vec![160.0, 80.0, 80.0, 80.0, 80.0, 35.0]);
sales.add_row(TableRow::header(vec![
TableCell::new("Product"),
TableCell::new("Jan").align(TextAlign::Right),
TableCell::new("Feb").align(TextAlign::Right),
TableCell::new("Mar").align(TextAlign::Right),
TableCell::new("Total").align(TextAlign::Right),
TableCell::new("Trend").align(TextAlign::Center),
]));
let rows = [
("Backpackers SaaS", "$4,200", "$5,100", "$6,800", "$16,100", "+"),
("BlogHunter Pro", "$2,800", "$3,200", "$3,600", "$9,600", "+"),
("YoutubeDownload", "$1,100", "$980", "$1,250", "$3,330", "+"),
("Proloom AI Forms", "$640", "$720", "$890", "$2,250", "+"),
("CapitalCities.se", "$320", "$290", "$410", "$1,020", "="),
("SongLyrics.se", "$180", "$160", "$200", "$540", "-"),
];
for (i, (name, jan, feb, mar, total, trend)) in rows.iter().enumerate() {
let col = match *trend {
"+" => Color::rgb_u8(40, 167, 69),
"-" => Color::rgb_u8(220, 53, 69),
_ => Color::rgb_u8(100, 100, 100),
};
let mut row = TableRow::new(vec![
TableCell::new(*name),
TableCell::new(*jan).align(TextAlign::Right),
TableCell::new(*feb).align(TextAlign::Right),
TableCell::new(*mar).align(TextAlign::Right),
TableCell::new(*total).align(TextAlign::Right).bold(),
TableCell::new(*trend).align(TextAlign::Center).color(col),
]);
if i % 2 == 0 { row = row.background(Color::rgb_u8(248, 250, 255)); }
sales.add_row(row);
}
sales.add_row(TableRow::new(vec![
TableCell::new("TOTAL").bold(),
TableCell::new("$9,240").bold().align(TextAlign::Right),
TableCell::new("$10,450").bold().align(TextAlign::Right),
TableCell::new("$13,150").bold().align(TextAlign::Right),
TableCell::new("$32,840").bold().align(TextAlign::Right),
TableCell::new("+").align(TextAlign::Center).color(Color::rgb_u8(40, 167, 69)),
]).background(Color::rgb_u8(235, 240, 255)));
p.table(&sales, 40.0, 736.0);
section_title(p, "Tech Stack", 540.0, &bold);
let mut stack = Table::new(vec![120.0, 130.0, 265.0]);
stack.add_row(TableRow::header(vec![
TableCell::new("Layer"), TableCell::new("Technology"), TableCell::new("Purpose"),
]));
let stack_rows = [
("Backend", "PHP 8.4", "Primary SaaS language"),
("Backend", "Python / Flask", "Data pipelines, pSEO tooling"),
("Backend", "Rust (pdfox)", "High-perf PDF generation"),
("Frontend", "jQuery + vanilla", "Progressive enhancement"),
("Payments", "Polar.sh", "Stripe-backed billing"),
("Infra", "VPS + Nginx", "Hetzner, SSL via Let's Encrypt"),
("DB", "MySQL / SQLite", "MySQL for SaaS, SQLite for tools"),
];
for (i, (layer, tech, purpose)) in stack_rows.iter().enumerate() {
let mut row = TableRow::new(vec![
TableCell::new(*layer).color(Color::rgb_u8(50, 50, 120)),
TableCell::new(*tech).bold(),
TableCell::new(*purpose),
]);
if i % 2 == 1 { row = row.background(Color::rgb_u8(248, 250, 255)); }
stack.add_row(row);
}
p.table(&stack, 40.0, 525.0);
})
.page(|p| {
let f = p.use_helvetica();
let t = p.use_times();
let c = p.use_courier();
let f_reg = p.reg_key(&f);
let f_bold = p.bold_key(&f);
let t_reg = p.reg_key(&t);
let t_bold = p.bold_key(&t);
let c_reg = p.reg_key(&c);
page_header(p, "Fonts & Architecture", "Page 3: Fonts, code blocks, library layout", &f_bold, &f_reg);
section_title(p, "Font comparison", 748.0, &f_bold);
p.text("Helvetica Regular: The quick brown fox jumps over the lazy dog", 40.0, 725.0, &f_reg, 12.0, Color::BLACK);
p.text("Helvetica Bold: The quick brown fox jumps over the lazy dog", 40.0, 708.0, &f_bold, 12.0, Color::BLACK);
p.text("Times Roman: The quick brown fox jumps over the lazy dog", 40.0, 691.0, &t_reg, 12.0, Color::BLACK);
p.text("Times Bold: The quick brown fox jumps over the lazy dog", 40.0, 674.0, &t_bold, 12.0, Color::BLACK);
p.text("Courier Regular: The quick brown fox jumps over the lazy dog", 40.0, 657.0, &c_reg, 12.0, Color::BLACK);
section_title(p, "Library architecture", 630.0, &f_bold);
let boxes = [
(40.0, 590.0, "Document", Color::rgb_u8(30, 60, 140)),
(210.0,590.0, "PageBuilder", Color::rgb_u8(0, 110, 80)),
(380.0,590.0, "PdfWriter", Color::rgb_u8(120, 40, 0)),
(40.0, 545.0, "ContentStream", Color::rgb_u8(80, 20, 120)),
(210.0,545.0, "Table", Color::rgb_u8(160, 80, 0)),
(380.0,545.0, "PdfImage", Color::rgb_u8(0, 80, 140)),
(40.0, 500.0, "PdfObject", Color::rgb_u8(50, 50, 50)),
(210.0,500.0, "ParsedDocument", Color::rgb_u8(50, 50, 50)),
(380.0,500.0, "BuiltinFont", Color::rgb_u8(50, 50, 50)),
];
for (x, y, label, bg) in &boxes {
p.filled_rect(*x, *y, 150.0, 28.0, *bg);
p.stroked_rect(*x, *y, 150.0, 28.0, Color::rgb_u8(180,180,180), 0.4);
p.text_centered(label, x+75.0, y+9.0, &f_reg, 10.0, Color::WHITE);
}
section_title(p, "Feature highlights", 470.0, &f_bold);
let features = [
"Pure Rust -- zero C deps, zero unsafe",
"PDF creation: text, shapes, tables, images, hyperlinks",
"PDF parsing: object tree traversal, text extraction",
"14 built-in Type1 fonts with accurate glyph widths",
"Watermarks, headers/footers with {page}/{total} macros",
"Encryption (RC4-40/128) + permission flags",
"Digital signature placeholders (adbe.pkcs7.detached)",
"AcroForm: text, checkbox, radio, dropdown, button fields",
];
for (i, feat) in features.iter().enumerate() {
p.text(&format!("+ {}", feat), 40.0, 450.0 - (i as f64)*18.0, &f_reg, 11.0, Color::rgb_u8(20, 120, 60));
}
section_title(p, "Code example", 300.0, &f_bold);
p.filled_rect(40.0, 200.0, 515.0, 90.0, Color::rgb_u8(245, 246, 250));
p.stroked_rect(40.0, 200.0, 515.0, 90.0, Color::rgb_u8(200,210,220), 0.5);
let code = [
("use pdfox::prelude::*;", Color::rgb_u8(0,100,160)),
("", Color::BLACK),
("let pdf = Document::new()", Color::BLACK),
(" .title(\"Q1 Report\").author(\"Samuel\")", Color::rgb_u8(100,50,0)),
(" .watermark(Watermark::diagonal(\"DRAFT\"))", Color::rgb_u8(50,100,0)),
(" .page(|p| { /* draw */ }).build();", Color::BLACK),
];
for (i, (line, col)) in code.iter().enumerate() {
p.text(line, 52.0, 278.0 - (i as f64)*14.0, &c_reg, 9.5, *col);
}
})
.add_pages(flow_pages)
.page(|p| {
let f = p.use_helvetica();
let reg = p.reg_key(&f);
let bold = p.bold_key(&f);
page_header(p, "Hyperlinks & Colors", "Page 5: Links, gradient bands, bar chart", &bold, ®);
section_title(p, "Hyperlink annotations", 748.0, &bold);
p.hyperlink("GitHub: sazalo101/pdffox", "https://github.com/sazalo101/pdffox", 40.0, 726.0, ®, 11.0);
p.hyperlink("Rust Programming Language", "https://www.rust-lang.org", 40.0, 708.0, ®, 11.0);
p.hyperlink("PDF 1.7 Spec (Adobe)", "https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf", 40.0, 690.0, ®, 11.0);
section_title(p, "Gradient-style bands", 660.0, &bold);
let bands = [
Color::rgb_u8(5,10,60), Color::rgb_u8(10,30,120), Color::rgb_u8(20,80,180),
Color::rgb_u8(40,130,210), Color::rgb_u8(80,170,230), Color::rgb_u8(140,200,240),
Color::rgb_u8(200,225,248), Color::rgb_u8(235,245,255),
];
for (i, col) in bands.iter().enumerate() {
p.filled_rect(40.0 + (i as f64)*64.375, 590.0, 64.375, 50.0, *col);
}
section_title(p, "Concentric circles", 565.0, &bold);
let (cx, cy2) = (130.0, 500.0);
for i in (1..=7usize).rev() {
let r = (i as f64) * 16.0;
let t = i as f64 / 7.0;
p.circle_fill(cx, cy2, r, Color::rgb_u8(
(30.0 + 190.0*(1.0-t)) as u8, (60.0+120.0*(1.0-t)) as u8, (140.0+100.0*t) as u8,
));
}
p.text_centered("Concentric", cx, 475.0, ®, 8.0, Color::DARK_GRAY);
section_title(p, "Bar chart (drawn with shapes)", 445.0, &bold);
let bars = [
("Jan", 42.0, Color::rgb_u8(30,60,140)),
("Feb", 67.0, Color::rgb_u8(40,120,200)),
("Mar", 55.0, Color::rgb_u8(60,160,220)),
("Apr", 81.0, Color::rgb_u8(80,180,100)),
("May", 74.0, Color::rgb_u8(60,160,80)),
("Jun", 93.0, Color::rgb_u8(40,140,60)),
];
let (bx, base, scale) = (200.0, 360.0, 1.8);
p.line(bx - 5.0, base, bx + 340.0, base, Color::DARK_GRAY, 0.5);
for (i, (label, val, col)) in bars.iter().enumerate() {
let x = bx + (i as f64)*55.0;
let h = val * scale;
p.filled_rect(x, base, 40.0, h, *col);
p.stroked_rect(x, base, 40.0, h, Color::rgb_u8(180,180,180), 0.3);
p.text_centered(label, x+20.0, base-10.0, ®, 8.0, Color::DARK_GRAY);
p.text_centered(&format!("{:.0}", val), x+20.0, base+h+4.0, ®, 7.0, Color::DARK_GRAY);
}
})
.page(|p| {
let f = p.use_helvetica();
let reg = p.reg_key(&f);
let bold = p.bold_key(&f);
page_header(p, "Interactive Form", "Page 6: AcroForm fields + digital signature", &bold, ®);
section_title(p, "Registration Form", 748.0, &bold);
p.text("Fill in the fields below and click Submit.", 40.0, 726.0, ®, 10.0, Color::DARK_GRAY);
for (label, y) in [("Full name", 671.0), ("Email", 636.0)] {
p.text(label, 40.0, y, &bold, 10.0, Color::rgb_u8(50, 50, 100));
p.stroked_rect(160.0, y-16.0, 360.0, 22.0, Color::rgb_u8(180,190,210), 0.8);
}
p.text("Bio", 40.0, 558.0, &bold, 10.0, Color::rgb_u8(50,50,100));
p.stroked_rect(160.0, 510.0, 360.0, 100.0, Color::rgb_u8(180,190,210), 0.8);
p.text("Country", 40.0, 447.0, &bold, 10.0, Color::rgb_u8(50,50,100));
p.stroked_rect(160.0, 428.0, 200.0, 22.0, Color::rgb_u8(180,190,210), 0.8);
p.text("Plan", 40.0, 416.0, &bold, 10.0, Color::rgb_u8(50,50,100));
for (x, label) in [(160.0,"Free"),(240.0,"Pro"),(320.0,"Enterprise")] {
p.circle_fill(x+7.0, 412.0, 7.0, Color::rgb_u8(220,230,250));
p.text(label, x+18.0, 416.0, ®, 10.0, Color::BLACK);
}
p.filled_rect(160.0, 360.0, 100.0, 28.0, Color::rgb_u8(30,60,140));
p.text_centered("Submit", 210.0, 371.0, &bold, 11.0, Color::WHITE);
section_title(p, "Digital Signature", 120.0, &bold);
p.text("Signature field below (adbe.pkcs7.detached placeholder):", 40.0, 100.0, ®, 9.0, Color::DARK_GRAY);
p.stroked_rect(40.0, 40.0, 200.0, 50.0, Color::rgb_u8(30,60,140), 0.8);
p.text("Samuel Azeez | pdfox Demo", 50.0, 66.0, ®, 8.5, Color::rgb_u8(30,60,140));
p.text("Click to sign", 50.0, 52.0, ®, 7.5, Color::DARK_GRAY);
})
.page(|p| {
let f = p.use_helvetica();
let reg = p.reg_key(&f);
let bold = p.bold_key(&f);
page_header(p, "New Features", "Page 7: Watermark, Header/Footer, Encryption, Signatures", &bold, ®);
section_title(p, "Watermark", 748.0, &bold);
p.text("Every page has a diagonal DRAFT watermark at 10% opacity.", 40.0, 726.0, ®, 11.0, Color::BLACK);
p.text("Watermark::diagonal(\"DRAFT\").opacity(0.10).color(Color::rgb_u8(80,80,180))", 40.0, 710.0, ®, 9.5, Color::rgb_u8(80,80,80));
section_title(p, "Header / Footer with {page}/{total} macros", 685.0, &bold);
p.text("Header center: pdfox -- Pure Rust PDF Library", 40.0, 663.0, ®, 10.0, Color::rgb_u8(50,50,120));
p.text("Footer left: pdfox -- Pure Rust PDF", 40.0, 647.0, ®, 10.0, Color::rgb_u8(50,50,120));
p.text("Footer center: https://github.com/sazalo101/pdffox", 40.0, 631.0, ®, 10.0, Color::rgb_u8(50,50,120));
p.text("Footer right: Page {page} of {total} <-- resolved per page automatically", 40.0, 615.0, ®, 10.0, Color::rgb_u8(50,50,120));
section_title(p, "Encryption (RC4-128)", 590.0, &bold);
p.text("Add: .encrypt(Encryption::new(\"owner_pass\", \"user_pass\"))", 40.0, 568.0, ®, 10.0, Color::BLACK);
p.text("Flags: .no_print() .no_copy() .no_modify() .view_only()", 40.0, 552.0, ®, 10.0, Color::rgb_u8(80,80,80));
section_title(p, "Digital Signature (adbe.pkcs7.detached)", 525.0, &bold);
p.text("1. Build with .signature(SignatureField::new(\"sig1\"))", 40.0, 503.0, ®, 10.0, Color::BLACK);
p.text("2. Call build_signed() -> (pdf_bytes, placeholders)", 40.0, 487.0, ®, 10.0, Color::BLACK);
p.text("3. placeholder.inject(pdf_bytes, pkcs7_der) to embed real signature", 40.0, 471.0, ®, 10.0, Color::BLACK);
section_title(p, "Image options: border, drop-shadow, crop", 445.0, &bold);
let (dx, dy, dw, dh) = (40.0, 290.0, 120.0, 80.0);
for i in 0..3u32 {
let s = (4 - i) as f64 * 2.0;
let g = (200 + i * 18) as u8;
p.filled_rect(dx+s, dy-s, dw, dh, Color::rgb_u8(g,g,g));
}
for row in 0..8u32 {
let t = row as f64 / 8.0;
p.filled_rect(dx, dy+(row as f64)*(dh/8.0), dw, dh/8.0,
Color::rgb_u8((20.0+80.0*t) as u8, (80.0+100.0*t) as u8, (180.0+60.0*(1.0-t)) as u8));
}
p.stroked_rect(dx, dy, dw, dh, Color::rgb_u8(30,60,140), 1.5);
p.text_centered("image + border", dx+dw/2.0, dy-10.0, ®, 8.0, Color::DARK_GRAY);
p.text_centered("+ drop shadow", dx+dw/2.0, dy-20.0, ®, 8.0, Color::DARK_GRAY);
})
.build_signed();
let out = "pdfox_demo.pdf";
fs::write(out, &pdf).expect("Failed to write PDF");
println!("Wrote {} bytes -> {}", pdf.len(), out);
if !sig_placeholders.is_empty() {
println!("Signature placeholders: {} (call .inject() to embed real PKCS#7)", sig_placeholders.len());
}
println!("\nParser round-trip test...");
let pdf2 = fs::read(out).unwrap();
match ParsedDocument::parse(pdf2) {
Ok(doc) => {
println!(" Parsed OK");
println!(" Objects : {}", doc.object_count());
if let Some(r) = doc.root_ref() { println!(" Root : {} {}", r.id, r.gen); }
match doc.page_refs() {
Ok(refs) => println!(" Pages : {}", refs.len()),
Err(e) => println!(" Pages err: {}", e),
}
match doc.extract_text() {
Ok(text) => {
let t = text.trim();
if !t.is_empty() {
let preview: String = t.chars().take(120).collect();
println!(" Text : {}...", preview);
} else {
println!(" Text : (empty - compressed streams require decompression on parse)");
}
}
Err(e) => println!(" Text err: {}", e),
}
}
Err(e) => eprintln!(" Parse error: {}", e),
}
println!("\nAll done. Open pdfox_demo.pdf to view the result.");
}
fn page_header(p: &mut PageBuilder, title: &str, subtitle: &str, bold: &str, reg: &str) {
p.filled_rect(0.0, 791.890, 595.276, 30.0, Color::rgb_u8(30, 60, 140));
p.text_centered(title, 297.5, 803.0, bold, 14.0, Color::WHITE);
p.filled_rect(0.0, 771.890, 595.276, 20.0, Color::rgb_u8(220, 230, 250));
p.text(subtitle, 20.0, 778.0, reg, 9.0, Color::rgb_u8(50, 50, 120));
}
fn section_title(p: &mut PageBuilder, title: &str, y: f64, bold: &str) {
p.text(title, 40.0, y, bold, 12.0, Color::rgb_u8(30, 60, 140));
p.line(40.0, y - 4.0, 555.0, y - 4.0, Color::rgb_u8(200, 210, 230), 0.5);
}