Skip to main content

pdf_ast/validation/
pdf_standards.rs

1use super::*;
2use crate::ast::{PdfDocument, PdfVersion};
3
4/// PDF/A compliance levels
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum PdfALevel {
7    PdfA1a,
8    PdfA1b,
9    PdfA2a,
10    PdfA2b,
11    PdfA2u,
12    PdfA3a,
13    PdfA3b,
14    PdfA3u,
15}
16
17impl PdfALevel {
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            PdfALevel::PdfA1a => "PDF/A-1a",
21            PdfALevel::PdfA1b => "PDF/A-1b",
22            PdfALevel::PdfA2a => "PDF/A-2a",
23            PdfALevel::PdfA2b => "PDF/A-2b",
24            PdfALevel::PdfA2u => "PDF/A-2u",
25            PdfALevel::PdfA3a => "PDF/A-3a",
26            PdfALevel::PdfA3b => "PDF/A-3b",
27            PdfALevel::PdfA3u => "PDF/A-3u",
28        }
29    }
30
31    pub fn requires_tagging(&self) -> bool {
32        matches!(
33            self,
34            PdfALevel::PdfA1a | PdfALevel::PdfA2a | PdfALevel::PdfA3a
35        )
36    }
37
38    pub fn allows_transparency(&self) -> bool {
39        !matches!(self, PdfALevel::PdfA1a | PdfALevel::PdfA1b)
40    }
41
42    pub fn allows_embedded_files(&self) -> bool {
43        matches!(
44            self,
45            PdfALevel::PdfA3a | PdfALevel::PdfA3b | PdfALevel::PdfA3u
46        )
47    }
48}
49
50/// PDF/X compliance levels
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum PdfXLevel {
53    PdfX1a,
54    PdfX3,
55    PdfX4,
56    PdfX4p,
57    PdfX5g,
58    PdfX5n,
59    PdfX5pg,
60}
61
62impl PdfXLevel {
63    pub fn as_str(&self) -> &'static str {
64        match self {
65            PdfXLevel::PdfX1a => "PDF/X-1a",
66            PdfXLevel::PdfX3 => "PDF/X-3",
67            PdfXLevel::PdfX4 => "PDF/X-4",
68            PdfXLevel::PdfX4p => "PDF/X-4p",
69            PdfXLevel::PdfX5g => "PDF/X-5g",
70            PdfXLevel::PdfX5n => "PDF/X-5n",
71            PdfXLevel::PdfX5pg => "PDF/X-5pg",
72        }
73    }
74}
75
76/// PDF/UA compliance levels
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum PdfUALevel {
79    PdfUA1,
80    PdfUA2,
81}
82
83impl PdfUALevel {
84    pub fn as_str(&self) -> &'static str {
85        match self {
86            PdfUALevel::PdfUA1 => "PDF/UA-1",
87            PdfUALevel::PdfUA2 => "PDF/UA-2",
88        }
89    }
90}
91
92/// PDF 2.0 base schema
93pub struct Pdf20Schema;
94
95impl Default for Pdf20Schema {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl Pdf20Schema {
102    pub fn new() -> Self {
103        Self
104    }
105}
106
107impl PdfSchema for Pdf20Schema {
108    fn name(&self) -> &str {
109        "PDF-2.0"
110    }
111
112    fn version(&self) -> &str {
113        "2.0"
114    }
115
116    fn description(&self) -> &str {
117        "ISO 32000-2 PDF 2.0 standard compliance"
118    }
119
120    fn reference_url(&self) -> Option<&str> {
121        Some("https://www.iso.org/standard/63534.html")
122    }
123
124    fn supports_pdf_version(&self, version: &PdfVersion) -> bool {
125        version.major >= 2 || (version.major == 1 && version.minor >= 4)
126    }
127
128    fn validate(&self, document: &PdfDocument) -> ValidationReport {
129        let mut report = ValidationReport::new(self.name().to_string(), self.version().to_string());
130        let context = ValidationContext::new(document, &mut report);
131
132        // Run all constraints
133        for constraint in self.get_constraints() {
134            if let Some(reference) = constraint.iso_reference() {
135                context
136                    .report
137                    .metadata
138                    .insert(format!("iso.{}", constraint.name()), reference.to_string());
139            }
140            constraint.check(document, context.report);
141        }
142
143        context.report.finalize();
144        report
145    }
146
147    fn get_constraints(&self) -> Vec<Box<dyn SchemaConstraint>> {
148        vec![
149            Box::new(HasCatalogConstraint),
150            Box::new(HasPagesTreeConstraint),
151            Box::new(CatalogHasPagesConstraint),
152            Box::new(PageCountConsistencyConstraint),
153            Box::new(ValidStructureConstraint),
154            Box::new(HasTrailerRootConstraint),
155            Box::new(HasTrailerSizeConstraint),
156            Box::new(TrailerIdConstraint),
157            Box::new(CatalogVersionConstraint),
158            Box::new(HasXRefEntriesConstraint),
159            Box::new(TrailerSizeConsistencyConstraint),
160            Box::new(MetadataStreamConstraint),
161            Box::new(FontCMapEncodingConstraint),
162        ]
163    }
164}
165
166/// PDF/A schema implementation
167pub struct PdfASchema {
168    level: PdfALevel,
169}
170
171impl PdfASchema {
172    pub fn new(level: PdfALevel) -> Self {
173        Self { level }
174    }
175}
176
177impl PdfSchema for PdfASchema {
178    fn name(&self) -> &str {
179        self.level.as_str()
180    }
181
182    fn version(&self) -> &str {
183        match self.level {
184            PdfALevel::PdfA1a | PdfALevel::PdfA1b => "1.4",
185            PdfALevel::PdfA2a | PdfALevel::PdfA2b | PdfALevel::PdfA2u => "1.7",
186            PdfALevel::PdfA3a | PdfALevel::PdfA3b | PdfALevel::PdfA3u => "1.7",
187        }
188    }
189
190    fn description(&self) -> &str {
191        "ISO 19005 PDF/A long-term archival standard"
192    }
193
194    fn reference_url(&self) -> Option<&str> {
195        Some("https://www.iso.org/standard/38920.html")
196    }
197
198    fn supports_pdf_version(&self, version: &PdfVersion) -> bool {
199        let required_version = self.version().parse::<f32>().unwrap_or(1.4);
200        let doc_version = format!("{}.{}", version.major, version.minor)
201            .parse::<f32>()
202            .unwrap_or(0.0);
203        doc_version >= required_version
204    }
205
206    fn validate(&self, document: &PdfDocument) -> ValidationReport {
207        if self.level == PdfALevel::PdfA1b {
208            let mut report = crate::validation::pdfa::PdfA1bValidator::new().validate(document);
209            report.schema_name = self.name().to_string();
210            report.schema_version = self.version().to_string();
211            return report;
212        }
213
214        let mut report = ValidationReport::new(self.name().to_string(), self.version().to_string());
215        let context = ValidationContext::new(document, &mut report);
216
217        // Run PDF/A specific constraints
218        for constraint in self.get_constraints() {
219            constraint.check(document, context.report);
220        }
221
222        context.report.finalize();
223        report
224    }
225
226    fn get_constraints(&self) -> Vec<Box<dyn SchemaConstraint>> {
227        let mut constraints: Vec<Box<dyn SchemaConstraint>> = vec![
228            Box::new(HasCatalogConstraint),
229            Box::new(HasPagesTreeConstraint),
230            Box::new(NoEncryptionConstraint),
231            Box::new(NoJavaScriptConstraint),
232            Box::new(NoExternalReferencesConstraint),
233            Box::new(EmbeddedFontsConstraint),
234        ];
235
236        if self.level.requires_tagging() {
237            constraints.push(Box::new(TaggedStructureConstraint));
238        }
239
240        if !self.level.allows_transparency() {
241            constraints.push(Box::new(NoTransparencyConstraint));
242        }
243
244        if !self.level.allows_embedded_files() {
245            constraints.push(Box::new(NoEmbeddedFilesConstraint));
246        }
247
248        constraints
249    }
250}
251
252/// PDF/X schema implementation
253pub struct PdfXSchema {
254    level: PdfXLevel,
255}
256
257impl PdfXSchema {
258    pub fn new(level: PdfXLevel) -> Self {
259        Self { level }
260    }
261}
262
263impl PdfSchema for PdfXSchema {
264    fn name(&self) -> &str {
265        self.level.as_str()
266    }
267
268    fn version(&self) -> &str {
269        match self.level {
270            PdfXLevel::PdfX1a => "1.3",
271            PdfXLevel::PdfX3 => "1.3",
272            PdfXLevel::PdfX4 | PdfXLevel::PdfX4p => "1.6",
273            PdfXLevel::PdfX5g | PdfXLevel::PdfX5n | PdfXLevel::PdfX5pg => "1.6",
274        }
275    }
276
277    fn description(&self) -> &str {
278        "ISO 15930 PDF/X graphics exchange standard"
279    }
280
281    fn supports_pdf_version(&self, version: &PdfVersion) -> bool {
282        let required_version = self.version().parse::<f32>().unwrap_or(1.3);
283        let doc_version = format!("{}.{}", version.major, version.minor)
284            .parse::<f32>()
285            .unwrap_or(0.0);
286        doc_version >= required_version
287    }
288
289    fn validate(&self, document: &PdfDocument) -> ValidationReport {
290        let mut report = ValidationReport::new(self.name().to_string(), self.version().to_string());
291        let context = ValidationContext::new(document, &mut report);
292
293        for constraint in self.get_constraints() {
294            constraint.check(document, context.report);
295        }
296
297        context.report.finalize();
298        report
299    }
300
301    fn get_constraints(&self) -> Vec<Box<dyn SchemaConstraint>> {
302        vec![
303            Box::new(HasCatalogConstraint),
304            Box::new(HasPagesTreeConstraint),
305            Box::new(NoEncryptionConstraint),
306            Box::new(NoJavaScriptConstraint),
307            Box::new(EmbeddedFontsConstraint),
308            Box::new(ColorSpaceConstraint),
309            Box::new(TrimBoxConstraint),
310        ]
311    }
312}
313
314/// PDF/UA schema implementation
315pub struct PdfUASchema {
316    level: PdfUALevel,
317}
318
319impl PdfUASchema {
320    pub fn new(level: PdfUALevel) -> Self {
321        Self { level }
322    }
323}
324
325impl PdfSchema for PdfUASchema {
326    fn name(&self) -> &str {
327        self.level.as_str()
328    }
329
330    fn version(&self) -> &str {
331        match self.level {
332            PdfUALevel::PdfUA1 => "1.7",
333            PdfUALevel::PdfUA2 => "2.0",
334        }
335    }
336
337    fn description(&self) -> &str {
338        "ISO 14289 PDF/UA universal accessibility standard"
339    }
340
341    fn supports_pdf_version(&self, version: &PdfVersion) -> bool {
342        let required_version = self.version().parse::<f32>().unwrap_or(1.7);
343        let doc_version = format!("{}.{}", version.major, version.minor)
344            .parse::<f32>()
345            .unwrap_or(0.0);
346        doc_version >= required_version
347    }
348
349    fn validate(&self, document: &PdfDocument) -> ValidationReport {
350        let mut report = ValidationReport::new(self.name().to_string(), self.version().to_string());
351        let context = ValidationContext::new(document, &mut report);
352
353        for constraint in self.get_constraints() {
354            constraint.check(document, context.report);
355        }
356
357        context.report.finalize();
358        report
359    }
360
361    fn get_constraints(&self) -> Vec<Box<dyn SchemaConstraint>> {
362        vec![
363            Box::new(HasCatalogConstraint),
364            Box::new(HasPagesTreeConstraint),
365            Box::new(TaggedStructureConstraint),
366            Box::new(AccessibilityMetadataConstraint),
367            Box::new(AltTextConstraint),
368            Box::new(LanguageSpecificationConstraint),
369            Box::new(LogicalReadingOrderConstraint),
370        ]
371    }
372}