use std::ffi::{CString, c_char, c_int};
use std::sync::LazyLock;
use crate::ffi::{Handle, HandleStore};
static VALIDATORS: LazyLock<HandleStore<ConformanceValidator>> = LazyLock::new(HandleStore::new);
static VALIDATION_RESULTS: LazyLock<HandleStore<ValidationResult>> =
LazyLock::new(HandleStore::new);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfALevel {
None = 0,
A1a = 1,
A1b = 2,
A2a = 3,
A2b = 4,
A2u = 5,
A3a = 6,
A3b = 7,
A3u = 8,
A4 = 9,
A4e = 10,
A4f = 11,
}
impl PdfALevel {
pub fn iso_standard(&self) -> &'static str {
match self {
PdfALevel::None => "None",
PdfALevel::A1a | PdfALevel::A1b => "ISO 19005-1",
PdfALevel::A2a | PdfALevel::A2b | PdfALevel::A2u => "ISO 19005-2",
PdfALevel::A3a | PdfALevel::A3b | PdfALevel::A3u => "ISO 19005-3",
PdfALevel::A4 | PdfALevel::A4e | PdfALevel::A4f => "ISO 19005-4",
}
}
pub fn short_name(&self) -> &'static str {
match self {
PdfALevel::None => "None",
PdfALevel::A1a => "PDF/A-1a",
PdfALevel::A1b => "PDF/A-1b",
PdfALevel::A2a => "PDF/A-2a",
PdfALevel::A2b => "PDF/A-2b",
PdfALevel::A2u => "PDF/A-2u",
PdfALevel::A3a => "PDF/A-3a",
PdfALevel::A3b => "PDF/A-3b",
PdfALevel::A3u => "PDF/A-3u",
PdfALevel::A4 => "PDF/A-4",
PdfALevel::A4e => "PDF/A-4e",
PdfALevel::A4f => "PDF/A-4f",
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfXLevel {
None = 0,
X1a2001 = 1,
X1a2003 = 2,
X32002 = 3,
X32003 = 4,
X4 = 5,
X4p = 6,
X5g = 7,
X5n = 8,
X5pg = 9,
X6 = 10,
X6n = 11,
X6p = 12,
}
impl PdfXLevel {
pub fn iso_standard(&self) -> &'static str {
match self {
PdfXLevel::None => "None",
PdfXLevel::X1a2001 => "ISO 15930-1",
PdfXLevel::X1a2003 => "ISO 15930-4",
PdfXLevel::X32002 => "ISO 15930-3",
PdfXLevel::X32003 => "ISO 15930-6",
PdfXLevel::X4 | PdfXLevel::X4p => "ISO 15930-7",
PdfXLevel::X5g | PdfXLevel::X5n | PdfXLevel::X5pg => "ISO 15930-8",
PdfXLevel::X6 | PdfXLevel::X6n | PdfXLevel::X6p => "ISO 15930-9",
}
}
pub fn short_name(&self) -> &'static str {
match self {
PdfXLevel::None => "None",
PdfXLevel::X1a2001 => "PDF/X-1a:2001",
PdfXLevel::X1a2003 => "PDF/X-1a:2003",
PdfXLevel::X32002 => "PDF/X-3:2002",
PdfXLevel::X32003 => "PDF/X-3:2003",
PdfXLevel::X4 => "PDF/X-4",
PdfXLevel::X4p => "PDF/X-4p",
PdfXLevel::X5g => "PDF/X-5g",
PdfXLevel::X5n => "PDF/X-5n",
PdfXLevel::X5pg => "PDF/X-5pg",
PdfXLevel::X6 => "PDF/X-6",
PdfXLevel::X6n => "PDF/X-6n",
PdfXLevel::X6p => "PDF/X-6p",
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfVersion {
Unknown = 0,
V1_0 = 10,
V1_1 = 11,
V1_2 = 12,
V1_3 = 13,
V1_4 = 14,
V1_5 = 15,
V1_6 = 16,
V1_7 = 17,
V2_0 = 20,
}
impl PdfVersion {
pub fn version_string(&self) -> &'static str {
match self {
PdfVersion::Unknown => "Unknown",
PdfVersion::V1_0 => "1.0",
PdfVersion::V1_1 => "1.1",
PdfVersion::V1_2 => "1.2",
PdfVersion::V1_3 => "1.3",
PdfVersion::V1_4 => "1.4",
PdfVersion::V1_5 => "1.5",
PdfVersion::V1_6 => "1.6",
PdfVersion::V1_7 => "1.7",
PdfVersion::V2_0 => "2.0",
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueSeverity {
Info = 0,
Warning = 1,
Error = 2,
Fatal = 3,
}
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub severity: IssueSeverity,
pub code: String,
pub message: String,
pub page: i32,
pub object_num: i32,
pub clause: Option<String>,
}
impl ValidationIssue {
pub fn new(
severity: IssueSeverity,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
severity,
code: code.into(),
message: message.into(),
page: 0,
object_num: 0,
clause: None,
}
}
pub fn with_page(mut self, page: i32) -> Self {
self.page = page;
self
}
pub fn with_object(mut self, object_num: i32) -> Self {
self.object_num = object_num;
self
}
pub fn with_clause(mut self, clause: impl Into<String>) -> Self {
self.clause = Some(clause.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub pdf_version: PdfVersion,
pub pdfa_claimed: PdfALevel,
pub pdfa_valid: PdfALevel,
pub pdfx_claimed: PdfXLevel,
pub pdfx_valid: PdfXLevel,
pub pdf2_compliant: bool,
pub issues: Vec<ValidationIssue>,
pub error_count: usize,
pub warning_count: usize,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
pdf_version: PdfVersion::Unknown,
pdfa_claimed: PdfALevel::None,
pdfa_valid: PdfALevel::None,
pdfx_claimed: PdfXLevel::None,
pdfx_valid: PdfXLevel::None,
pdf2_compliant: false,
issues: Vec::new(),
error_count: 0,
warning_count: 0,
}
}
pub fn add_issue(&mut self, issue: ValidationIssue) {
match issue.severity {
IssueSeverity::Error | IssueSeverity::Fatal => self.error_count += 1,
IssueSeverity::Warning => self.warning_count += 1,
IssueSeverity::Info => {}
}
self.issues.push(issue);
}
pub fn is_valid(&self) -> bool {
self.error_count == 0
}
pub fn issue_count(&self) -> usize {
self.issues.len()
}
}
impl Default for ValidationResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ValidatorConfig {
pub check_pdfa: bool,
pub check_pdfx: bool,
pub check_pdf2: bool,
pub stop_on_error: bool,
pub max_issues: usize,
}
impl Default for ValidatorConfig {
fn default() -> Self {
Self {
check_pdfa: true,
check_pdfx: true,
check_pdf2: true,
stop_on_error: false,
max_issues: 1000,
}
}
}
pub struct ConformanceValidator {
config: ValidatorConfig,
result: ValidationResult,
pdf_data: Vec<u8>,
}
impl ConformanceValidator {
pub fn new(config: ValidatorConfig) -> Self {
Self {
config,
result: ValidationResult::new(),
pdf_data: Vec::new(),
}
}
pub fn set_document_data(&mut self, data: Vec<u8>) {
self.pdf_data = data;
}
fn pdf_text(&self) -> String {
String::from_utf8_lossy(&self.pdf_data).to_string()
}
pub fn config(&self) -> &ValidatorConfig {
&self.config
}
pub fn result(&self) -> &ValidationResult {
&self.result
}
pub fn reset(&mut self) {
self.result = ValidationResult::new();
}
fn add_issue(&mut self, issue: ValidationIssue) -> bool {
if self.result.issues.len() >= self.config.max_issues {
return false;
}
let is_error = matches!(issue.severity, IssueSeverity::Error | IssueSeverity::Fatal);
self.result.add_issue(issue);
!is_error || !self.config.stop_on_error
}
pub fn validate_pdfa(&mut self) {
if !self.config.check_pdfa {
return;
}
self.check_pdfa_metadata();
if self.result.pdfa_claimed != PdfALevel::None {
self.check_embedded_fonts();
self.check_transparency();
self.check_encryption_pdfa();
self.check_javascript_pdfa();
self.check_xmp_metadata();
self.check_colorspaces_pdfa();
if self.result.error_count == 0 {
self.result.pdfa_valid = self.result.pdfa_claimed;
}
}
}
fn check_pdfa_metadata(&mut self) {
let text = self.pdf_text();
let has_pdfaid_part = text.contains("pdfaid:part") || text.contains("pdfaid:Part");
let has_pdfaid_conf =
text.contains("pdfaid:conformance") || text.contains("pdfaid:Conformance");
if !has_pdfaid_part && !has_pdfaid_conf {
return;
}
if has_pdfaid_part {
if let Some(part) = Self::extract_xmp_value(&text, "pdfaid:part")
.or_else(|| Self::extract_xmp_value(&text, "pdfaid:Part"))
{
let conformance = Self::extract_xmp_value(&text, "pdfaid:conformance")
.or_else(|| Self::extract_xmp_value(&text, "pdfaid:Conformance"))
.unwrap_or_default()
.to_uppercase();
self.result.pdfa_claimed = match (part.as_str(), conformance.as_str()) {
("1", "A") => PdfALevel::A1a,
("1", "B") | ("1", _) => PdfALevel::A1b,
("2", "A") => PdfALevel::A2a,
("2", "B") => PdfALevel::A2b,
("2", "U") | ("2", _) => PdfALevel::A2u,
("3", "A") => PdfALevel::A3a,
("3", "B") => PdfALevel::A3b,
("3", "U") | ("3", _) => PdfALevel::A3u,
("4", _) => PdfALevel::A4,
_ => PdfALevel::None,
};
}
}
if !has_pdfaid_conf {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"PDFA_CONFORMANCE_MISSING",
"PDF/A conformance level (pdfaid:conformance) is missing from XMP metadata",
)
.with_clause("6.6.2"),
);
}
}
fn check_embedded_fonts(&mut self) {
let text = self.pdf_text();
let font_pattern = "/Type /Font";
let mut pos = 0;
while let Some(found) = text[pos..].find(font_pattern) {
let abs_pos = pos + found;
pos = abs_pos + font_pattern.len();
let window_start = abs_pos.saturating_sub(200);
let window_end = (abs_pos + 500).min(text.len());
let window = &text[window_start..window_end];
let has_fontfile = window.contains("/FontFile")
|| window.contains("/FontFile2")
|| window.contains("/FontFile3");
if !has_fontfile {
let font_name = if let Some(bf_pos) = window.find("/BaseFont") {
let after = &window[bf_pos + 9..];
let trimmed = after.trim_start();
if trimmed.starts_with('/') {
let end = trimmed[1..]
.find(|c: char| c.is_whitespace() || c == '/' || c == '>')
.map(|i| i + 1)
.unwrap_or(trimmed.len());
trimmed[1..end].to_string()
} else {
"unknown".to_string()
}
} else {
"unknown".to_string()
};
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"FONT_NOT_EMBEDDED",
format!(
"Font '{}' is not embedded (missing FontFile/FontFile2/FontFile3)",
font_name
),
)
.with_clause("6.3.5"),
);
}
}
}
fn check_transparency(&mut self) {
let text = self.pdf_text();
let is_pdfa1 = matches!(self.result.pdfa_claimed, PdfALevel::A1a | PdfALevel::A1b);
if !is_pdfa1 {
return;
}
if text.contains("/SMask") {
let has_real_smask = text.contains("/SMask") && {
let mut found_real = false;
let mut search_pos = 0;
while let Some(p) = text[search_pos..].find("/SMask") {
let after = &text[search_pos + p + 6..];
let trimmed = after.trim_start();
if !trimmed.starts_with("/None") {
found_real = true;
break;
}
search_pos += p + 6;
}
found_real
};
if has_real_smask {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"TRANSPARENCY_SMASK",
"Soft mask (/SMask) is forbidden in PDF/A-1",
)
.with_clause("6.4"),
);
}
}
let blend_modes = [
"Multiply",
"Screen",
"Overlay",
"Darken",
"Lighten",
"ColorDodge",
"ColorBurn",
"HardLight",
"SoftLight",
"Difference",
"Exclusion",
];
for bm in &blend_modes {
let pattern = format!("/BM /{}", bm);
if text.contains(&pattern) {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"TRANSPARENCY_BLEND_MODE",
format!("Non-normal blend mode /BM /{} is forbidden in PDF/A-1", bm),
)
.with_clause("6.4"),
);
break;
}
}
for key in &["/ca ", "/CA "] {
if let Some(pos) = text.find(key) {
let after = text[pos + key.len()..].trim_start();
let end = after
.find(|c: char| c.is_whitespace() || c == '/')
.unwrap_or(after.len());
if let Ok(val) = after[..end].parse::<f64>() {
if (val - 1.0).abs() > f64::EPSILON {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"TRANSPARENCY_OPACITY",
format!(
"Non-opaque {} value ({}) is forbidden in PDF/A-1",
key.trim(),
val
),
)
.with_clause("6.4"),
);
}
}
}
}
}
fn check_encryption_pdfa(&mut self) {
let text = self.pdf_text();
if text.contains("/Encrypt") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"ENCRYPTION_FORBIDDEN",
"Encryption (/Encrypt) is forbidden in PDF/A",
)
.with_clause("6.1.3"),
);
}
}
fn check_javascript_pdfa(&mut self) {
let text = self.pdf_text();
if text.contains("/JS") || text.contains("/JavaScript") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"JAVASCRIPT_FORBIDDEN",
"JavaScript (/JS or /JavaScript) is forbidden in PDF/A",
)
.with_clause("6.6.1"),
);
}
}
fn check_xmp_metadata(&mut self) {
let text = self.pdf_text();
let has_metadata = text.contains("/Metadata");
let has_xmp = text.contains("<x:xmpmeta") || text.contains("xpacket");
if !has_metadata {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"XMP_METADATA_MISSING",
"Document catalog does not contain a /Metadata entry (required for PDF/A)",
)
.with_clause("6.6.2"),
);
} else if !has_xmp {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"XMP_PACKET_MISSING",
"No XMP metadata packet found despite /Metadata reference",
)
.with_clause("6.6.2"),
);
}
}
fn check_colorspaces_pdfa(&mut self) {
let text = self.pdf_text();
let has_output_intent = text.contains("/OutputIntents");
let device_dependent = [
("/DeviceRGB", "DeviceRGB"),
("/DeviceCMYK", "DeviceCMYK"),
("/DeviceGray", "DeviceGray"),
];
if !has_output_intent {
for (pattern, name) in &device_dependent {
if text.contains(pattern) {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"COLORSPACE_DEVICE_DEPENDENT",
format!(
"Device-dependent color space {} used without an output intent",
name
),
)
.with_clause("6.2.3"),
);
}
}
}
}
fn extract_xmp_value(text: &str, tag: &str) -> Option<String> {
let open = format!("<{}>", tag);
let close = format!("</{}>", tag);
if let Some(start) = text.find(&open) {
let val_start = start + open.len();
if let Some(end) = text[val_start..].find(&close) {
let value = text[val_start..val_start + end].trim().to_string();
if !value.is_empty() {
return Some(value);
}
}
}
let attr_pat = format!("{}=\"", tag);
if let Some(start) = text.find(&attr_pat) {
let val_start = start + attr_pat.len();
if let Some(end) = text[val_start..].find('"') {
let value = text[val_start..val_start + end].trim().to_string();
if !value.is_empty() {
return Some(value);
}
}
}
None
}
pub fn validate_pdfx(&mut self) {
if !self.config.check_pdfx {
return;
}
self.check_pdfx_metadata();
if self.result.pdfx_claimed != PdfXLevel::None {
self.check_output_intent();
self.check_page_boxes();
self.check_trapped_key();
self.check_fonts_pdfx();
if self.result.error_count == 0 {
self.result.pdfx_valid = self.result.pdfx_claimed;
}
}
}
fn check_pdfx_metadata(&mut self) {
let text = self.pdf_text();
if text.contains("GTS_PDFXVersion") {
if text.contains("PDF/X-1a:2001") {
self.result.pdfx_claimed = PdfXLevel::X1a2001;
} else if text.contains("PDF/X-1a:2003") {
self.result.pdfx_claimed = PdfXLevel::X1a2003;
} else if text.contains("PDF/X-3:2002") {
self.result.pdfx_claimed = PdfXLevel::X32002;
} else if text.contains("PDF/X-3:2003") {
self.result.pdfx_claimed = PdfXLevel::X32003;
} else if text.contains("PDF/X-4") {
self.result.pdfx_claimed = PdfXLevel::X4;
} else {
self.result.pdfx_claimed = PdfXLevel::X1a2001; }
} else {
return;
}
}
fn check_output_intent(&mut self) {
let text = self.pdf_text();
if !text.contains("/OutputIntents") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"PDFX_OUTPUT_INTENT_MISSING",
"PDF/X requires /OutputIntents array in the document catalog",
)
.with_clause("6.2.2"),
);
return;
}
if !text.contains("/GTS_PDFX") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"PDFX_OUTPUT_INTENT_SUBTYPE",
"OutputIntents should contain an entry with /S /GTS_PDFX",
)
.with_clause("6.2.2"),
);
}
}
fn check_page_boxes(&mut self) {
let text = self.pdf_text();
let has_trimbox = text.contains("/TrimBox");
let has_artbox = text.contains("/ArtBox");
if !has_trimbox && !has_artbox {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"PDFX_PAGE_BOX_MISSING",
"PDF/X requires either /TrimBox or /ArtBox on each page",
)
.with_clause("6.1.3"),
);
}
if !text.contains("/BleedBox") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Info,
"PDFX_BLEEDBOX_RECOMMENDED",
"/BleedBox is recommended for PDF/X documents",
)
.with_clause("6.1.3"),
);
}
}
fn check_trapped_key(&mut self) {
let text = self.pdf_text();
if !text.contains("/Trapped") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Error,
"PDFX_TRAPPED_MISSING",
"/Trapped key is required in the Info dictionary for PDF/X",
)
.with_clause("6.1.3"),
);
} else {
let valid_values = [
"/Trapped /True",
"/Trapped /False",
"/Trapped /Unknown",
"/Trapped/True",
"/Trapped/False",
"/Trapped/Unknown",
];
let has_valid_value = valid_values.iter().any(|v| text.contains(v));
if !has_valid_value {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"PDFX_TRAPPED_INVALID",
"/Trapped key must have value /True, /False, or /Unknown",
)
.with_clause("6.1.3"),
);
}
}
}
fn check_fonts_pdfx(&mut self) {
self.check_embedded_fonts();
}
pub fn validate_pdf2(&mut self) {
if !self.config.check_pdf2 {
return;
}
self.check_pdf_version();
if self.result.pdf_version == PdfVersion::V2_0 {
self.check_deprecated_features();
self.check_pdf2_features();
if self.result.error_count == 0 {
self.result.pdf2_compliant = true;
}
}
}
fn check_pdf_version(&mut self) {
let data = &self.pdf_data;
if data.starts_with(b"%PDF-") {
let header = String::from_utf8_lossy(&data[..data.len().min(20)]);
if let Some(ver_str) = header.strip_prefix("%PDF-") {
let ver_end = ver_str
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(ver_str.len());
let ver = &ver_str[..ver_end];
self.result.pdf_version = match ver {
"1.0" => PdfVersion::V1_0,
"1.1" => PdfVersion::V1_1,
"1.2" => PdfVersion::V1_2,
"1.3" => PdfVersion::V1_3,
"1.4" => PdfVersion::V1_4,
"1.5" => PdfVersion::V1_5,
"1.6" => PdfVersion::V1_6,
"1.7" => PdfVersion::V1_7,
"2.0" => PdfVersion::V2_0,
_ => PdfVersion::Unknown,
};
}
}
let text = self.pdf_text();
if let Some(pos) = text.find("/Version") {
let after = &text[pos + 8..];
let trimmed = after.trim_start();
if trimmed.starts_with('/') {
let end = trimmed[1..]
.find(|c: char| c.is_whitespace() || c == '/' || c == '>')
.map(|i| i + 1)
.unwrap_or(trimmed.len());
let catalog_ver = &trimmed[1..end];
let parsed = match catalog_ver {
"1.0" => Some(PdfVersion::V1_0),
"1.1" => Some(PdfVersion::V1_1),
"1.2" => Some(PdfVersion::V1_2),
"1.3" => Some(PdfVersion::V1_3),
"1.4" => Some(PdfVersion::V1_4),
"1.5" => Some(PdfVersion::V1_5),
"1.6" => Some(PdfVersion::V1_6),
"1.7" => Some(PdfVersion::V1_7),
"2.0" => Some(PdfVersion::V2_0),
_ => None,
};
if let Some(ver) = parsed {
self.result.pdf_version = ver;
}
}
}
}
fn check_deprecated_features(&mut self) {
let text = self.pdf_text();
if text.contains("/LZWDecode") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"DEPRECATED_LZWDECODE",
"LZWDecode filter is deprecated in PDF 2.0; use FlateDecode instead",
)
.with_clause("7.3.4"),
);
}
if text.contains("/ASCII85Decode") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"DEPRECATED_ASCII85",
"ASCII85Decode filter is deprecated in PDF 2.0",
)
.with_clause("7.3.4"),
);
}
if text.contains("/XFA") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Warning,
"DEPRECATED_XFA",
"XFA forms are deprecated in PDF 2.0",
)
.with_clause("12.7.8"),
);
}
}
fn check_pdf2_features(&mut self) {
let text = self.pdf_text();
if text.contains("/AESV3") || text.contains("/CFM /AESV3") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Info,
"PDF2_AES256",
"Document uses AES-256 encryption (PDF 2.0 feature)",
)
.with_clause("7.6.2"),
);
}
let page_pattern = "/Type /Page";
let mut pos = 0;
while let Some(found) = text[pos..].find(page_pattern) {
let abs_pos = pos + found;
pos = abs_pos + page_pattern.len();
if text.get(abs_pos + page_pattern.len()..abs_pos + page_pattern.len() + 1) == Some("s")
{
continue;
}
let window_end = (abs_pos + 1000).min(text.len());
let window = &text[abs_pos..window_end];
if window.contains("/OutputIntents") {
self.add_issue(
ValidationIssue::new(
IssueSeverity::Info,
"PDF2_PAGE_OUTPUT_INTENT",
"Document uses page-level output intents (PDF 2.0 feature)",
)
.with_clause("14.11.5"),
);
break;
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_conformance_validator(
_ctx: Handle,
check_pdfa: c_int,
check_pdfx: c_int,
check_pdf2: c_int,
) -> Handle {
let config = ValidatorConfig {
check_pdfa: check_pdfa != 0,
check_pdfx: check_pdfx != 0,
check_pdf2: check_pdf2 != 0,
..Default::default()
};
let validator = ConformanceValidator::new(config);
VALIDATORS.insert(validator)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_conformance_validator(_ctx: Handle, validator: Handle) {
VALIDATORS.remove(validator);
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_set_document(_ctx: Handle, validator: Handle, doc: Handle) {
if let Some(doc_arc) = crate::ffi::DOCUMENTS.get(doc) {
if let Ok(doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
if let Some(varc) = VALIDATORS.get(validator) {
if let Ok(mut v) = varc.lock() {
v.set_document_data(data);
}
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_set_data(
_ctx: Handle,
validator: Handle,
data: *const u8,
len: usize,
) {
if data.is_null() || len == 0 {
return;
}
let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
if let Some(varc) = VALIDATORS.get(validator) {
if let Ok(mut v) = varc.lock() {
v.set_document_data(bytes);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_validator_reset(_ctx: Handle, validator: Handle) {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(mut v) = arc.lock() {
v.reset();
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdfa(_ctx: Handle, validator: Handle) {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(mut v) = arc.lock() {
v.validate_pdfa();
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdfx(_ctx: Handle, validator: Handle) {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(mut v) = arc.lock() {
v.validate_pdfx();
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validate_pdf2(_ctx: Handle, validator: Handle) {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(mut v) = arc.lock() {
v.validate_pdf2();
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_is_valid(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return if v.result().is_valid() { 1 } else { 0 };
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_error_count(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().error_count as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_warning_count(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().warning_count as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_issue_count(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().issue_count() as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfa_claimed(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().pdfa_claimed as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfa_valid(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().pdfa_valid as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfx_claimed(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().pdfx_claimed as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdfx_valid(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().pdfx_valid as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdf2_compliant(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return if v.result().pdf2_compliant { 1 } else { 0 };
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_conformance_pdf_version(_ctx: Handle, validator: Handle) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
return v.result().pdf_version as c_int;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_new_validation_result(_ctx: Handle) -> Handle {
let result = ValidationResult::new();
VALIDATION_RESULTS.insert(result)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_validation_result(_ctx: Handle, result: Handle) {
VALIDATION_RESULTS.remove(result);
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_message(
_ctx: Handle,
validator: Handle,
index: c_int,
) -> *mut c_char {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
if let Some(issue) = v.result().issues.get(index as usize) {
if let Ok(s) = CString::new(issue.message.as_str()) {
return s.into_raw();
}
}
}
}
std::ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_code(
_ctx: Handle,
validator: Handle,
index: c_int,
) -> *mut c_char {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
if let Some(issue) = v.result().issues.get(index as usize) {
if let Ok(s) = CString::new(issue.code.as_str()) {
return s.into_raw();
}
}
}
}
std::ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_validation_issue_severity(
_ctx: Handle,
validator: Handle,
index: c_int,
) -> c_int {
if let Some(arc) = VALIDATORS.get(validator) {
if let Ok(v) = arc.lock() {
if let Some(issue) = v.result().issues.get(index as usize) {
return issue.severity as c_int;
}
}
}
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_free_validation_string(_ctx: Handle, s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_pdfa_level_name(level: c_int) -> *const c_char {
let level = match level {
0 => PdfALevel::None,
1 => PdfALevel::A1a,
2 => PdfALevel::A1b,
3 => PdfALevel::A2a,
4 => PdfALevel::A2b,
5 => PdfALevel::A2u,
6 => PdfALevel::A3a,
7 => PdfALevel::A3b,
8 => PdfALevel::A3u,
9 => PdfALevel::A4,
10 => PdfALevel::A4e,
11 => PdfALevel::A4f,
_ => PdfALevel::None,
};
level.short_name().as_ptr() as *const c_char
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_pdfx_level_name(level: c_int) -> *const c_char {
let level = match level {
0 => PdfXLevel::None,
1 => PdfXLevel::X1a2001,
2 => PdfXLevel::X1a2003,
3 => PdfXLevel::X32002,
4 => PdfXLevel::X32003,
5 => PdfXLevel::X4,
6 => PdfXLevel::X4p,
7 => PdfXLevel::X5g,
8 => PdfXLevel::X5n,
9 => PdfXLevel::X5pg,
10 => PdfXLevel::X6,
11 => PdfXLevel::X6n,
12 => PdfXLevel::X6p,
_ => PdfXLevel::None,
};
level.short_name().as_ptr() as *const c_char
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pdfa_levels() {
assert_eq!(PdfALevel::A1a.short_name(), "PDF/A-1a");
assert_eq!(PdfALevel::A1a.iso_standard(), "ISO 19005-1");
assert_eq!(PdfALevel::A2b.short_name(), "PDF/A-2b");
assert_eq!(PdfALevel::A2b.iso_standard(), "ISO 19005-2");
assert_eq!(PdfALevel::A4.short_name(), "PDF/A-4");
assert_eq!(PdfALevel::A4.iso_standard(), "ISO 19005-4");
}
#[test]
fn test_pdfx_levels() {
assert_eq!(PdfXLevel::X1a2001.short_name(), "PDF/X-1a:2001");
assert_eq!(PdfXLevel::X1a2001.iso_standard(), "ISO 15930-1");
assert_eq!(PdfXLevel::X4.short_name(), "PDF/X-4");
assert_eq!(PdfXLevel::X4.iso_standard(), "ISO 15930-7");
}
#[test]
fn test_pdf_versions() {
assert_eq!(PdfVersion::V1_7.version_string(), "1.7");
assert_eq!(PdfVersion::V2_0.version_string(), "2.0");
}
#[test]
fn test_validation_issue() {
let issue = ValidationIssue::new(IssueSeverity::Error, "TEST_ERROR", "Test error message")
.with_page(1)
.with_object(42)
.with_clause("6.1.2");
assert_eq!(issue.severity, IssueSeverity::Error);
assert_eq!(issue.code, "TEST_ERROR");
assert_eq!(issue.page, 1);
assert_eq!(issue.object_num, 42);
assert_eq!(issue.clause, Some("6.1.2".to_string()));
}
#[test]
fn test_validation_result() {
let mut result = ValidationResult::new();
assert!(result.is_valid());
assert_eq!(result.error_count, 0);
result.add_issue(ValidationIssue::new(
IssueSeverity::Warning,
"WARN1",
"Warning",
));
assert!(result.is_valid());
assert_eq!(result.warning_count, 1);
result.add_issue(ValidationIssue::new(IssueSeverity::Error, "ERR1", "Error"));
assert!(!result.is_valid());
assert_eq!(result.error_count, 1);
}
#[test]
fn test_validator_config() {
let config = ValidatorConfig::default();
assert!(config.check_pdfa);
assert!(config.check_pdfx);
assert!(config.check_pdf2);
assert!(!config.stop_on_error);
}
#[test]
fn test_conformance_validator() {
let config = ValidatorConfig::default();
let mut validator = ConformanceValidator::new(config);
validator.validate_pdfa();
validator.validate_pdfx();
validator.validate_pdf2();
assert!(validator.result().is_valid());
}
#[test]
fn test_validator_ffi() {
let handle = fz_new_conformance_validator(0, 1, 1, 1);
assert!(handle != 0);
fz_validate_pdfa(0, handle);
fz_validate_pdfx(0, handle);
fz_validate_pdf2(0, handle);
let is_valid = fz_conformance_is_valid(0, handle);
assert_eq!(is_valid, 1);
let error_count = fz_conformance_error_count(0, handle);
assert_eq!(error_count, 0);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_validator_reset() {
let handle = fz_new_conformance_validator(0, 1, 1, 1);
fz_validate_pdfa(0, handle);
fz_conformance_validator_reset(0, handle);
let issue_count = fz_conformance_issue_count(0, handle);
assert_eq!(issue_count, 0);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_fz_conformance_set_data_null() {
let handle = fz_new_conformance_validator(0, 1, 1, 1);
fz_conformance_set_data(0, handle, std::ptr::null(), 10);
fz_conformance_set_data(0, handle, b"x".as_ptr(), 0);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_fz_conformance_set_data_valid() {
let handle = fz_new_conformance_validator(0, 1, 1, 1);
let data = b"%PDF-1.7";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdf2(0, handle);
assert_eq!(fz_conformance_pdf_version(0, handle), 17);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_fz_conformance_set_document_invalid() {
fz_conformance_set_document(0, 0, 0);
}
#[test]
fn test_validation_with_pdfa_claim() {
let handle = fz_new_conformance_validator(0, 1, 0, 0);
let data = b"%PDF-1.4\n<x:xmpmeta><rdf:RDF><rdf:Description><pdfaid:part>1</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance></rdf:Description></rdf:RDF></x:xmpmeta>";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdfa(0, handle);
assert_eq!(fz_conformance_pdfa_claimed(0, handle), 2);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_validation_with_pdfx_claim() {
let handle = fz_new_conformance_validator(0, 0, 1, 0);
let data = b"%PDF-1.4\nGTS_PDFXVersion PDF/X-1a:2001";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdfx(0, handle);
assert_eq!(fz_conformance_pdfx_claimed(0, handle), 1);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_validation_pdf2_compliant() {
let handle = fz_new_conformance_validator(0, 0, 0, 1);
let data = b"%PDF-2.0\n";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdf2(0, handle);
assert_eq!(fz_conformance_pdf2_compliant(0, handle), 1);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_fz_validation_issue_functions() {
let handle = fz_new_conformance_validator(0, 1, 0, 0);
let data = b"%PDF-1.4\n<x:xmpmeta><pdfaid:part>1</pdfaid:part></x:xmpmeta>\n/Type /Font\n/BaseFont /Helvetica";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdfa(0, handle);
let msg = fz_validation_issue_message(0, handle, 0);
if !msg.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(msg).to_str().unwrap() };
assert!(!s.is_empty());
fz_free_validation_string(0, msg);
}
let code = fz_validation_issue_code(0, handle, 0);
if !code.is_null() {
fz_free_validation_string(0, code);
}
let sev = fz_validation_issue_severity(0, handle, 0);
assert!(sev >= -1);
assert_eq!(
fz_validation_issue_message(0, handle, 999),
std::ptr::null_mut()
);
assert_eq!(fz_validation_issue_severity(0, handle, 999), -1);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_fz_free_validation_string_null() {
fz_free_validation_string(0, std::ptr::null_mut());
}
#[test]
fn test_fz_pdfa_level_names() {
let names = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
for &n in &names {
let ptr = fz_pdfa_level_name(n);
assert!(!ptr.is_null());
}
let ptr = fz_pdfa_level_name(99);
assert!(!ptr.is_null());
}
#[test]
fn test_fz_pdfx_level_names() {
for n in 0..=12 {
let ptr = fz_pdfx_level_name(n);
assert!(!ptr.is_null());
}
let ptr = fz_pdfx_level_name(99);
assert!(!ptr.is_null());
}
#[test]
fn test_invalid_validator_handles() {
assert_eq!(fz_conformance_is_valid(0, 0), 0);
assert_eq!(fz_conformance_error_count(0, 0), 0);
assert_eq!(fz_conformance_warning_count(0, 0), 0);
assert_eq!(fz_conformance_issue_count(0, 0), 0);
assert_eq!(fz_conformance_pdfa_claimed(0, 0), 0);
assert_eq!(fz_conformance_pdfa_valid(0, 0), 0);
assert_eq!(fz_conformance_pdfx_claimed(0, 0), 0);
assert_eq!(fz_conformance_pdfx_valid(0, 0), 0);
assert_eq!(fz_conformance_pdf2_compliant(0, 0), 0);
assert_eq!(fz_conformance_pdf_version(0, 0), 0);
}
#[test]
fn test_validation_result_info_severity() {
let mut result = ValidationResult::new();
result.add_issue(ValidationIssue::new(
IssueSeverity::Info,
"INFO1",
"Info message",
));
assert!(result.is_valid());
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_validator_config_disabled() {
let config = ValidatorConfig {
check_pdfa: false,
check_pdfx: false,
check_pdf2: false,
..Default::default()
};
let mut validator = ConformanceValidator::new(config);
validator.validate_pdfa();
validator.validate_pdfx();
validator.validate_pdf2();
}
#[test]
fn test_fz_new_validation_result() {
let h = fz_new_validation_result(0);
assert!(h != 0);
fz_drop_validation_result(0, h);
}
#[test]
fn test_pdf_version_from_catalog() {
let handle = fz_new_conformance_validator(0, 0, 0, 1);
let data = b"%PDF-1.0\n/Version /2.0";
fz_conformance_set_data(0, handle, data.as_ptr(), data.len());
fz_validate_pdf2(0, handle);
assert_eq!(fz_conformance_pdf_version(0, handle), 20);
fz_drop_conformance_validator(0, handle);
}
#[test]
fn test_all_pdfa_levels_iso() {
assert_eq!(PdfALevel::None.iso_standard(), "None");
assert_eq!(PdfALevel::A1a.iso_standard(), "ISO 19005-1");
assert_eq!(PdfALevel::A3a.iso_standard(), "ISO 19005-3");
}
#[test]
fn test_all_pdfx_levels_iso() {
assert_eq!(PdfXLevel::None.iso_standard(), "None");
assert_eq!(PdfXLevel::X5g.iso_standard(), "ISO 15930-8");
assert_eq!(PdfXLevel::X6.iso_standard(), "ISO 15930-9");
}
#[test]
fn test_all_pdf_versions() {
assert_eq!(PdfVersion::Unknown.version_string(), "Unknown");
assert_eq!(PdfVersion::V1_0.version_string(), "1.0");
}
}