pub mod barcode;
pub mod chart;
pub mod error;
pub mod font;
pub mod image_loader;
pub mod layout;
pub mod model;
pub mod pdf;
pub mod qrcode;
pub mod style;
pub mod svg;
pub mod template;
pub mod text;
#[cfg(feature = "wasm")]
pub mod wasm;
#[cfg(feature = "wasm-raw")]
pub mod wasm_raw;
pub use error::FormeError;
pub use layout::LayoutInfo;
pub use model::{
CertificationConfig, ColumnDef, ColumnWidth, FontEntry, PatternType, RedactionPattern,
RedactionRegion, TextRun,
};
pub use model::{ChartDataPoint, ChartSeries, DotPlotGroup};
pub use model::{Document, Metadata, Node, NodeKind, PageConfig, PageSize};
pub use style::Style;
use font::FontContext;
use layout::LayoutEngine;
use pdf::PdfWriter;
pub fn certify_pdf(
pdf_bytes: &[u8],
config: &model::CertificationConfig,
) -> Result<Vec<u8>, FormeError> {
pdf::certify::certify_pdf(pdf_bytes, config)
}
pub fn redact_pdf(
pdf_bytes: &[u8],
regions: &[model::RedactionRegion],
) -> Result<Vec<u8>, FormeError> {
pdf::redaction::redact_pdf(pdf_bytes, regions)
}
pub fn find_text_regions(
pdf_bytes: &[u8],
patterns: &[model::RedactionPattern],
) -> Result<Vec<RedactionRegion>, FormeError> {
pdf::redaction::find_text_regions(pdf_bytes, patterns)
}
pub fn redact_text(
pdf_bytes: &[u8],
patterns: &[model::RedactionPattern],
) -> Result<Vec<u8>, FormeError> {
pdf::redaction::redact_text(pdf_bytes, patterns)
}
pub fn merge_pdfs(pdfs: &[&[u8]]) -> Result<Vec<u8>, FormeError> {
pdf::merge::merge_pdfs(pdfs)
}
pub fn render(document: &Document) -> Result<Vec<u8>, FormeError> {
render_with_warnings(document).map(|(pdf, _warnings)| pdf)
}
fn layout_with_sentinel_passes(
document: &Document,
) -> (
Vec<crate::layout::LayoutPage>,
FontContext,
u32,
Vec<String>,
) {
layout_with_sentinel_passes_audited(document, false)
}
fn layout_with_sentinel_passes_audited(
document: &Document,
audit: bool,
) -> (
Vec<crate::layout::LayoutPage>,
FontContext,
u32,
Vec<String>,
) {
let mut font_context = FontContext::new();
register_document_fonts(&mut font_context, &document.fonts);
let engine = LayoutEngine::new();
font_context.reset_page_sentinel();
let mut pages = engine.layout(document, &font_context);
let mut passes = 1u32;
if font_context.saw_page_sentinel() {
for _ in 0..2 {
let needed = digits_for_count(pages.len());
if needed == font_context.sentinel_digit_count() {
break;
}
font_context.set_sentinel_digit_count(needed);
pages = engine.layout(document, &font_context);
passes += 1;
}
}
let mut layout_warnings = engine.take_warnings();
if audit {
crate::layout::audit::audit_content(document, &pages, &mut layout_warnings);
}
(pages, font_context, passes, layout_warnings)
}
pub fn count_layout_passes(document: &Document) -> u32 {
layout_with_sentinel_passes(document).2
}
pub fn render_with_warnings(document: &Document) -> Result<(Vec<u8>, Vec<String>), FormeError> {
render_with_warnings_and_passes(document).map(|(pdf, warnings, _passes)| (pdf, warnings))
}
#[derive(Debug, Clone, Copy, Default, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct RenderOptions {
pub audit_content: bool,
}
pub fn render_with_warnings_and_passes(
document: &Document,
) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
render_with_options(document, RenderOptions::default())
}
fn effective_pdf_version(document: &Document) -> crate::model::PdfVersion {
use crate::model::{PdfAConformance, PdfVersion};
if document.pdf_ua2 {
return PdfVersion::V2_0;
}
match &document.pdfa {
Some(PdfAConformance::A4 | PdfAConformance::A4f) => PdfVersion::V2_0,
_ => document.pdf_version,
}
}
fn validate_pdf_version(document: &Document) -> Result<(), FormeError> {
use crate::model::{PdfAConformance, PdfVersion};
if document.pdf_version != PdfVersion::V2_0 {
return Ok(());
}
if let Some(level) = &document.pdfa {
let family = match level {
PdfAConformance::A2a | PdfAConformance::A2b | PdfAConformance::A2u => "PDF/A-2",
PdfAConformance::A3a | PdfAConformance::A3b | PdfAConformance::A3u => "PDF/A-3",
PdfAConformance::A4 | PdfAConformance::A4f => return Ok(()),
};
return Err(FormeError::RenderError(format!(
"pdfVersion \"2.0\" contradicts pdfa: {family} is defined over ISO 32000-1 (PDF 1.7). Drop the pdfa claim, or keep pdfVersion \"1.7\" (the default). PDF/A-4 is the 2.0-based archival standard."
)));
}
if document.pdf_ua {
return Err(FormeError::RenderError(
"pdfVersion \"2.0\" contradicts pdfUa: PDF/UA-1 is defined over ISO 32000-1 (PDF 1.7). Drop the pdfUa claim, or keep pdfVersion \"1.7\" (the default). PDF/UA-2 is the 2.0-based accessibility standard (pdfUa2)."
.to_string(),
));
}
Ok(())
}
fn validate_pdf_ua2(document: &Document) -> Result<(), FormeError> {
use crate::model::PdfAConformance;
if !document.pdf_ua2 {
return Ok(());
}
if document.pdf_ua {
return Err(FormeError::RenderError(
"pdfUa2 contradicts pdfUa: a file claims PDF/UA-1 (ISO 32000-1) or PDF/UA-2 \
(PDF 2.0), not both. Keep exactly one."
.to_string(),
));
}
if let Some(
PdfAConformance::A2a
| PdfAConformance::A2b
| PdfAConformance::A2u
| PdfAConformance::A3a
| PdfAConformance::A3b
| PdfAConformance::A3u,
) = document.pdfa
{
return Err(FormeError::RenderError(
"pdfUa2 contradicts a 1.7-based pdfa level (2x/3x are ISO 32000-1). \
Compose PDF/UA-2 with pdfa: \"4\" or \"4f\", or drop one claim."
.to_string(),
));
}
Ok(())
}
pub fn render_with_options(
document: &Document,
options: RenderOptions,
) -> Result<(Vec<u8>, Vec<String>, u32), FormeError> {
validate_pdf_version(document)?;
validate_pdf_ua2(document)?;
let profile = std::env::var("FORME_PROFILE").is_ok();
let t_layout = if profile {
Some(std::time::Instant::now())
} else {
None
};
let (pages, font_context, passes, layout_warnings) =
layout_with_sentinel_passes_audited(document, options.audit_content);
let layout_ms = t_layout.map(|t| t.elapsed().as_secs_f64() * 1000.0);
let writer = PdfWriter::new();
let tagged = document.tagged
|| document.pdf_ua
|| document.pdf_ua2
|| matches!(
document.pdfa,
Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
);
let t_ser = if profile {
Some(std::time::Instant::now())
} else {
None
};
let (pdf, warnings) = writer.write(
&pages,
&document.metadata,
&font_context,
tagged,
document.pdfa.as_ref(),
document.pdf_ua,
document.embedded_data.as_deref(),
&document.attachments,
document.zugferd.as_ref(),
document.flatten_forms,
effective_pdf_version(document),
document.pdf_ua2,
)?;
let warnings = {
let mut all = layout_warnings;
all.extend(warnings);
all
};
let serialize_ms = t_ser.map(|t| t.elapsed().as_secs_f64() * 1000.0);
let pdf = if let Some(ref sig_config) = document.certification {
pdf::certify::certify_pdf(&pdf, sig_config)?
} else {
pdf
};
if let (Some(l), Some(s)) = (layout_ms, serialize_ms) {
eprintln!(
"FORME_PROFILE pages={} passes={passes} layout_ms={l:.1} serialize_ms={s:.1}",
pages.len()
);
}
Ok((pdf, warnings, passes))
}
pub fn render_with_layout(
document: &Document,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
render_with_layout_and_passes(document)
.map(|(pdf, layout, warnings, _passes)| (pdf, layout, warnings))
}
pub fn render_with_layout_and_passes(
document: &Document,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
render_with_layout_and_options(document, RenderOptions::default())
}
pub fn render_with_layout_and_options(
document: &Document,
options: RenderOptions,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
validate_pdf_version(document)?;
validate_pdf_ua2(document)?;
let (pages, font_context, passes, layout_warnings) =
layout_with_sentinel_passes_audited(document, options.audit_content);
let layout_info = LayoutInfo::from_pages(&pages);
let writer = PdfWriter::new();
let tagged = document.tagged
|| document.pdf_ua
|| document.pdf_ua2
|| matches!(
document.pdfa,
Some(model::PdfAConformance::A2a) | Some(model::PdfAConformance::A3a)
);
let (pdf, warnings) = writer.write(
&pages,
&document.metadata,
&font_context,
tagged,
document.pdfa.as_ref(),
document.pdf_ua,
document.embedded_data.as_deref(),
&document.attachments,
document.zugferd.as_ref(),
document.flatten_forms,
effective_pdf_version(document),
document.pdf_ua2,
)?;
let pdf = if let Some(ref sig_config) = document.certification {
pdf::certify::certify_pdf(&pdf, sig_config)?
} else {
pdf
};
let warnings = {
let mut all = layout_warnings;
all.extend(warnings);
all
};
Ok((pdf, layout_info, warnings, passes))
}
fn digits_for_count(n: usize) -> u32 {
if n < 10 {
1
} else if n < 100 {
2
} else if n < 1000 {
3
} else {
4
}
}
fn register_document_fonts(font_context: &mut FontContext, fonts: &[FontEntry]) {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD;
for entry in fonts {
let bytes = if let Some(comma_pos) = entry.src.find(',') {
b64.decode(&entry.src[comma_pos + 1..]).ok()
} else {
b64.decode(&entry.src).ok()
};
if let Some(data) = bytes {
font_context
.registry_mut()
.register(&entry.family, entry.weight, entry.italic, data);
}
}
}
pub fn render_json(json: &str) -> Result<Vec<u8>, FormeError> {
let document: Document = serde_json::from_str(json)?;
render(&document)
}
pub fn render_json_with_layout(
json: &str,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
let document: Document = serde_json::from_str(json)?;
render_with_layout(&document)
}
pub fn render_json_with_layout_and_options(
json: &str,
options: RenderOptions,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>, u32), FormeError> {
let document: Document = serde_json::from_str(json)?;
render_with_layout_and_options(&document, options)
}
pub fn render_template(template_json: &str, data_json: &str) -> Result<Vec<u8>, FormeError> {
let template: serde_json::Value = serde_json::from_str(template_json)?;
let data: serde_json::Value = serde_json::from_str(data_json)?;
let resolved = template::evaluate_template(&template, &data)?;
let document: Document = serde_json::from_value(resolved)?;
render(&document)
}
pub fn render_template_with_layout(
template_json: &str,
data_json: &str,
) -> Result<(Vec<u8>, LayoutInfo, Vec<String>), FormeError> {
let template: serde_json::Value = serde_json::from_str(template_json)?;
let data: serde_json::Value = serde_json::from_str(data_json)?;
let resolved = template::evaluate_template(&template, &data)?;
let document: Document = serde_json::from_value(resolved)?;
render_with_layout(&document)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digits_for_count() {
assert_eq!(digits_for_count(0), 1);
assert_eq!(digits_for_count(1), 1);
assert_eq!(digits_for_count(9), 1);
assert_eq!(digits_for_count(10), 2);
assert_eq!(digits_for_count(99), 2);
assert_eq!(digits_for_count(100), 3);
assert_eq!(digits_for_count(999), 3);
assert_eq!(digits_for_count(1000), 4);
}
}