use pdfluent::compliance::PdfAProfile;
use pdfluent::prelude::PdfDocument;
use pdfluent::watermark::{Layer, WatermarkOptions};
use pdfluent::ImageFormat;
use std::path::{Path, PathBuf};
use std::process::Command;
const MAX_BYTES: usize = 400 * 1024;
const CANVAS: (f64, f64) = (1200.0, 600.0);
const HERO_DPI: u32 = 144;
const PANEL_DPI: u32 = 192;
const SECRET_LINE: &str = "NL02ABNA0123456789";
const PROFILE: PdfAProfile = PdfAProfile::A1b;
const CHROME_CANDIDATES: &[&str] = &[
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
];
fn find_chrome() -> Option<String> {
CHROME_CANDIDATES
.iter()
.find(|c| {
Path::new(c).exists()
|| Command::new(c)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
.map(|c| (*c).to_string())
}
fn print_to_pdf(chrome: &str, html: &Path, pdf: &Path) -> Result<(), String> {
let out = Command::new(chrome)
.args([
"--headless",
"--disable-gpu",
"--no-sandbox",
"--no-pdf-header-footer",
"--run-all-compositor-stages-before-draw",
"--virtual-time-budget=4000",
])
.arg(format!("--print-to-pdf={}", pdf.display()))
.arg(file_url(html))
.output()
.map_err(|e| format!("could not run {chrome}: {e}"))?;
if !pdf.is_file() {
return Err(format!(
"Chrome exited {} and wrote no PDF:\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
));
}
Ok(())
}
fn file_url(path: &Path) -> String {
format!("file://{}", path.display())
}
fn render_page_1(doc: &PdfDocument, dpi: u32, to: &Path) -> Result<(), String> {
let png = doc
.render_page(1, dpi, ImageFormat::Png)
.map_err(|e| format!("render failed: {e}"))?;
std::fs::write(to, &png).map_err(|e| format!("could not write {}: {e}", to.display()))?;
Ok(())
}
fn run() -> Result<(), String> {
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.ok_or("cannot find the repository root from CARGO_MANIFEST_DIR")?
.to_path_buf();
let mut out = repo.join("docs/readme-hero.png");
let mut keep: Option<PathBuf> = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--out" => out = PathBuf::from(args.next().ok_or("--out needs a path")?),
"--keep" => keep = Some(PathBuf::from(args.next().ok_or("--keep needs a path")?)),
other => return Err(format!("unknown argument {other}")),
}
}
let chrome = find_chrome().ok_or(
"no Chrome or Chromium found. This example needs one for the two print steps; \
PDFluent does the rest.",
)?;
let work = match &keep {
Some(p) => p.clone(),
None => std::env::temp_dir().join("pdfluent-readme-hero"),
};
std::fs::create_dir_all(&work)
.map_err(|e| format!("could not make {}: {e}", work.display()))?;
let source_html = repo.join("docs/hero/hero_document.html");
let source_pdf = work.join("statement.pdf");
println!("1/6 Chrome prints {}", source_html.display());
print_to_pdf(&chrome, &source_html, &source_pdf)?;
println!("2/6 PDFluent renders the left panel");
let before = PdfDocument::open(&source_pdf).map_err(|e| format!("open failed: {e}"))?;
let before_png = work.join("panel-before.png");
render_page_1(&before, PANEL_DPI, &before_png)?;
println!("3/6 PDFluent redacts, watermarks and archives");
let mut working = PdfDocument::open(&source_pdf).map_err(|e| format!("open failed: {e}"))?;
let bank_line = working
.text_with_layout()
.map_err(|e| format!("could not read the text layout: {e}"))?
.into_iter()
.find(|b| b.text.contains(SECRET_LINE))
.ok_or("the bank line is not in the document any more -- hero_document.html changed")?;
let [x0, y0, x1, y1] = bank_line.bbox;
working
.redact_region(bank_line.page, [x0 - 2.0, y0 - 2.0, x1 + 2.0, y1 + 2.0])
.map_err(|e| format!("redaction failed: {e}"))?;
working
.add_watermark(
"SPECIMEN",
WatermarkOptions::centered()
.rotated(30.0)
.opacity(1.0)
.layer(Layer::Foreground)
.font_size(56.0)
.color(0.86, 0.86, 0.86),
)
.map_err(|e| format!("watermark failed: {e}"))?;
let archived = working
.convert_to_pdfa(PROFILE)
.map_err(|e| format!("PDF/A conversion failed: {e}"))?;
let report = archived
.validate_pdfa(PROFILE)
.map_err(|e| format!("PDF/A validation failed: {e}"))?;
if !report.is_compliant() {
let why = report
.violations
.iter()
.map(|v| format!("[{}] {}", v.rule, v.message))
.collect::<Vec<_>>()
.join("; ");
return Err(format!(
"the converted document is not PDF/A-1B compliant, so the hero image would \
be claiming something untrue. Validator said: {why}"
));
}
let verdict = "PDF/A-1B, and validate_pdfa() agrees.";
let text = archived
.text()
.map_err(|e| format!("could not read the archived text back: {e}"))?;
if text.contains(SECRET_LINE) {
return Err(format!(
"{SECRET_LINE} is still extractable from the archived document. The right \
panel would be showing a redaction that did not happen."
));
}
println!("4/6 PDFluent renders the right panel");
let after_png = work.join("panel-after.png");
render_page_1(&archived, PANEL_DPI, &after_png)?;
println!("5/6 Chrome prints the composite");
let template = repo.join("docs/hero/hero_composite.html");
let composite_html = work.join("composite.html");
let page_size = format!("{}in {}in", CANVAS.0 / 96.0, CANVAS.1 / 96.0);
let filled = std::fs::read_to_string(&template)
.map_err(|e| format!("could not read {}: {e}", template.display()))?
.replace("{{BEFORE_PNG}}", &file_url(&before_png))
.replace("{{AFTER_PNG}}", &file_url(&after_png))
.replace("{{VERDICT}}", verdict)
.replace("{{CANVAS_W}}", &format!("{}", CANVAS.0))
.replace("{{CANVAS_H}}", &format!("{}", CANVAS.1))
.replace("{{PAGE_SIZE}}", &page_size);
std::fs::write(&composite_html, filled)
.map_err(|e| format!("could not write {}: {e}", composite_html.display()))?;
let composite_pdf = work.join("composite.pdf");
print_to_pdf(&chrome, &composite_html, &composite_pdf)?;
println!("6/6 PDFluent renders the hero");
let composite = PdfDocument::open(&composite_pdf).map_err(|e| format!("open failed: {e}"))?;
let hero = composite
.render_page(1, HERO_DPI, ImageFormat::Png)
.map_err(|e| format!("render failed: {e}"))?;
if hero.len() > MAX_BYTES {
return Err(format!(
"the hero came out at {} kB, over the {} kB the README budgets for its first \
screen. Nothing is written: a file the guard rejects is worse than none.",
hero.len() / 1024,
MAX_BYTES / 1024
));
}
std::fs::write(&out, &hero).map_err(|e| format!("could not write {}: {e}", out.display()))?;
println!(
"\nwrote {} ({} kB of the {} kB budget); working files in {}",
out.display(),
hero.len() / 1024,
MAX_BYTES / 1024,
work.display()
);
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("readme_hero: {e}");
std::process::exit(1);
}
}