blitz-pdf 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +300 -10
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/browser.d.ts +1 -1
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/document-options.d.ts +36 -0
  6. package/dist/document-options.d.ts.map +1 -0
  7. package/dist/document-options.js +236 -0
  8. package/dist/document-options.js.map +1 -0
  9. package/dist/node.d.ts +1 -1
  10. package/dist/node.d.ts.map +1 -1
  11. package/dist/options.d.ts +35 -1
  12. package/dist/options.d.ts.map +1 -1
  13. package/dist/options.js +21 -14
  14. package/dist/options.js.map +1 -1
  15. package/dist/types.d.ts +71 -1
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/validation.d.ts +2 -0
  18. package/dist/validation.d.ts.map +1 -0
  19. package/dist/validation.js +7 -0
  20. package/dist/validation.js.map +1 -0
  21. package/examples/zugferd.mjs +41 -0
  22. package/licenses/RUST_DEPENDENCIES.txt +27 -0
  23. package/package.json +3 -2
  24. package/src/browser.ts +1 -1
  25. package/src/document-options.ts +218 -0
  26. package/src/node.ts +1 -1
  27. package/src/options.ts +18 -13
  28. package/src/types.ts +76 -1
  29. package/src/validation.ts +6 -0
  30. package/vendor/README.md +27 -2
  31. package/vendor/krilla/.cargo-ok +1 -0
  32. package/vendor/krilla/.cargo_vcs_info.json +6 -0
  33. package/vendor/krilla/Cargo.lock +1357 -0
  34. package/vendor/krilla/Cargo.toml +165 -0
  35. package/vendor/krilla/LICENSE_APACHE +202 -0
  36. package/vendor/krilla/LICENSE_MIT +21 -0
  37. package/vendor/krilla/README.md +156 -0
  38. package/vendor/krilla/examples/basic.rs +117 -0
  39. package/vendor/krilla/examples/empty_document.rs +25 -0
  40. package/vendor/krilla/examples/parley.rs +174 -0
  41. package/vendor/krilla/examples/simple_text.rs +118 -0
  42. package/vendor/krilla/examples/stream_builder.rs +93 -0
  43. package/vendor/krilla/icc/LICENSE +116 -0
  44. package/vendor/krilla/icc/README.md +1 -0
  45. package/vendor/krilla/icc/sGrey-v2-magic.icc +0 -0
  46. package/vendor/krilla/icc/sGrey-v4.icc +0 -0
  47. package/vendor/krilla/icc/sRGB-v2-magic.icc +0 -0
  48. package/vendor/krilla/icc/sRGB-v4.icc +0 -0
  49. package/vendor/krilla/src/chunk_container.rs +503 -0
  50. package/vendor/krilla/src/configure/PDF_A1.md +227 -0
  51. package/vendor/krilla/src/configure/PDF_A2.md +269 -0
  52. package/vendor/krilla/src/configure/PDF_A3.md +31 -0
  53. package/vendor/krilla/src/configure/PDF_A4.md +211 -0
  54. package/vendor/krilla/src/configure/PDF_UA1.md +202 -0
  55. package/vendor/krilla/src/configure/README.md +23 -0
  56. package/vendor/krilla/src/configure/mod.rs +197 -0
  57. package/vendor/krilla/src/configure/validate.rs +1244 -0
  58. package/vendor/krilla/src/configure/version.rs +134 -0
  59. package/vendor/krilla/src/content.rs +1352 -0
  60. package/vendor/krilla/src/data.rs +49 -0
  61. package/vendor/krilla/src/document.rs +156 -0
  62. package/vendor/krilla/src/error.rs +170 -0
  63. package/vendor/krilla/src/geom.rs +341 -0
  64. package/vendor/krilla/src/graphics/blend.rs +46 -0
  65. package/vendor/krilla/src/graphics/color.rs +552 -0
  66. package/vendor/krilla/src/graphics/graphic.rs +30 -0
  67. package/vendor/krilla/src/graphics/graphics_state.rs +251 -0
  68. package/vendor/krilla/src/graphics/icc.rs +218 -0
  69. package/vendor/krilla/src/graphics/image.rs +835 -0
  70. package/vendor/krilla/src/graphics/mask.rs +128 -0
  71. package/vendor/krilla/src/graphics/mod.rs +14 -0
  72. package/vendor/krilla/src/graphics/paint.rs +434 -0
  73. package/vendor/krilla/src/graphics/separation.rs +86 -0
  74. package/vendor/krilla/src/graphics/shading_function.rs +865 -0
  75. package/vendor/krilla/src/graphics/shading_pattern.rs +60 -0
  76. package/vendor/krilla/src/graphics/tiling_pattern.rs +119 -0
  77. package/vendor/krilla/src/graphics/xobject.rs +150 -0
  78. package/vendor/krilla/src/interactive/action.rs +68 -0
  79. package/vendor/krilla/src/interactive/annotation.rs +283 -0
  80. package/vendor/krilla/src/interactive/destination.rs +289 -0
  81. package/vendor/krilla/src/interactive/mod.rs +3 -0
  82. package/vendor/krilla/src/interchange/embed.rs +240 -0
  83. package/vendor/krilla/src/interchange/metadata.rs +564 -0
  84. package/vendor/krilla/src/interchange/mod.rs +4 -0
  85. package/vendor/krilla/src/interchange/outline.rs +263 -0
  86. package/vendor/krilla/src/interchange/tagging/fmt.rs +733 -0
  87. package/vendor/krilla/src/interchange/tagging/generate.toml +474 -0
  88. package/vendor/krilla/src/interchange/tagging/generated.rs +4754 -0
  89. package/vendor/krilla/src/interchange/tagging/mod.rs +1324 -0
  90. package/vendor/krilla/src/interchange/tagging/tag.rs +686 -0
  91. package/vendor/krilla/src/lib.rs +179 -0
  92. package/vendor/krilla/src/num.rs +26 -0
  93. package/vendor/krilla/src/page.rs +612 -0
  94. package/vendor/krilla/src/pdf.rs +313 -0
  95. package/vendor/krilla/src/resource.rs +436 -0
  96. package/vendor/krilla/src/serialize.rs +1101 -0
  97. package/vendor/krilla/src/stream.rs +368 -0
  98. package/vendor/krilla/src/surface.rs +664 -0
  99. package/vendor/krilla/src/text/cid.rs +488 -0
  100. package/vendor/krilla/src/text/font.rs +351 -0
  101. package/vendor/krilla/src/text/glyph/bitmap.rs +81 -0
  102. package/vendor/krilla/src/text/glyph/colr.rs +581 -0
  103. package/vendor/krilla/src/text/glyph/mod.rs +205 -0
  104. package/vendor/krilla/src/text/glyph/outline.rs +72 -0
  105. package/vendor/krilla/src/text/glyph/svg.rs +34 -0
  106. package/vendor/krilla/src/text/group.rs +463 -0
  107. package/vendor/krilla/src/text/mod.rs +232 -0
  108. package/vendor/krilla/src/text/shape.rs +102 -0
  109. package/vendor/krilla/src/text/type3.rs +514 -0
  110. package/vendor/krilla/src/util.rs +286 -0
  111. package/vendor/stylo/properties/longhands.toml +0 -1
  112. package/vendor/stylo/stylesheets/rule_parser.rs +1 -1
  113. package/wasm/blitz_pdf.js +10 -5
  114. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,1244 @@
1
+ //! Exporting with a specific PDF conformance level.
2
+ //!
3
+ //! PDF defines a number of additional conformance levels that restrict the features of PDF that
4
+ //! can be used to a specific subset.
5
+ //!
6
+ //! You can use a [`Validator`] by creating a corresponding [`Configuration`]
7
+ //! you want to build the document with. There are three important aspects that play into this:
8
+ //! - krilla will internally write the file in a way that conforms to the given standard, i.e.
9
+ //! by settings appropriate metadata. This happens under-the-hood and is completely abstracted
10
+ //! away from the user.
11
+ //! - For aspects that are out of control of krilla and dependent on the input, krilla will perform
12
+ //! a validation that the input is compatible with the standard. krilla will record all violations,
13
+ //! and when calling `document.finish()`, in case there is at least one violation, krilla will
14
+ //! return them as an error, instead of returning the finished document. See [`ValidationError`].
15
+ //! - Finally, some standards have requirements that cannot possibly be validated by krilla, as
16
+ //! they are semantic in nature. It is upon you, as a user of that library, to ensure that those
17
+ //! requirements are fulfilled. Therefore, while krilla tries to make it as easy as possible
18
+ //! to generate compliant PDFs, it is still highly recommended that you familiarize yourself
19
+ //! with the PDF specification as well as the specifications for the substandards. This is
20
+ //! especially true for standards related to universal accessibility.
21
+ //!
22
+ //! You can find some requirements below **Requirements** for each [`Validator`].
23
+ //!
24
+ //! [`Configuration`]: crate::configure::Configuration
25
+
26
+ use std::collections::HashMap;
27
+ use std::fmt::Debug;
28
+
29
+ use pdf_writer::types::OutputIntentSubtype;
30
+ use xmp_writer::pdfa::PdfAExtSchemasWriter;
31
+ use xmp_writer::XmpWriter;
32
+
33
+ use crate::color::separation::SeparationColorant;
34
+ use crate::color::separation::SeparationSpace;
35
+ use crate::color::RegularColor;
36
+ use crate::configure::PdfVersion;
37
+ use crate::interchange::embed::EmbedError;
38
+ use crate::surface::Location;
39
+ use crate::text::Font;
40
+ use crate::text::GlyphId;
41
+
42
+ /// An error that occurred during validation/
43
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
44
+ pub enum ValidationError {
45
+ /// There was a string that was longer than the maximum allowed length (32767).
46
+ ///
47
+ /// Can for example occur if you set a title or an author that is longer than
48
+ /// the given length.
49
+ TooLongString,
50
+ /// There was a name that was longer than the maximum allowed length (127).
51
+ ///
52
+ /// Can for example occur if the font name is too long.
53
+ TooLongName,
54
+ /// There was an array that was longer than the maximum allowed length (8191).
55
+ /// Can only occur for PDF 1.4.
56
+ ///
57
+ /// Can for example occur if a text too long was written.
58
+ TooLongArray,
59
+ /// There was a dictionary with more entries than the maximum allowed (4095).
60
+ /// Can only occur for PDF 1.4.
61
+ ///
62
+ /// Can for example occur if too many annotations are added to a page.
63
+ TooLongDictionary,
64
+ /// There was a float that is higher than the maximum allowed (32767).
65
+ /// Can only occur for PDF 1.4.
66
+ TooLargeFloat,
67
+ /// The PDF exceeds the upper limit for indirect objects (8388607).
68
+ ///
69
+ /// Occurs if the PDF is simply too long.
70
+ TooManyIndirectObjects,
71
+ /// The PDF contains a content stream that exceeds maximum allowed q/Q nesting level (28).
72
+ ///
73
+ /// Can only occur if the user stacks many clip paths.
74
+ TooHighQNestingLevel,
75
+ /// The PDF contains PostScript code, which is forbidden by some export formats.
76
+ ///
77
+ /// Occurs if a gradient with spread method `Repeat`/`Reflect` or a sweep gradient was used.
78
+ ContainsPostScript(Option<Location>),
79
+ /// No CMYK ICC profile was provided, even though one is necessary.
80
+ ///
81
+ /// Occurs if the export format requires a device-independent color representation,
82
+ /// and a CMYK color was used in the document.
83
+ MissingCMYKProfile,
84
+ /// The same Separation colorant was used with multiple different fallback colors.
85
+ ///
86
+ /// Occurs if the user specified multiple Separation color spaces with the same colorant but a different fallback color.
87
+ InconsistentSeparationFallback(SeparationColorant),
88
+ /// The `.notdef` glyph was used, which is forbidden by some export formats.
89
+ ///
90
+ /// Can occur if a glyph could not be found in the font for a corresponding codepoint
91
+ /// in the input text, or if it was explicitly mapped that way.
92
+ ///
93
+ /// The third argument contains the text range of the glyph.
94
+ ContainsNotDefGlyph(Font, Option<Location>, String),
95
+ /// A glyph was mapped to no codepoint at all, which is forbidden by some
96
+ /// standards.
97
+ NoCodepointMapping(Font, GlyphId, Option<Location>),
98
+ /// A glyph was mapped either to the codepoint 0x0, 0xFEFF or 0xFFFE, which
99
+ /// is forbidden by some standards.
100
+ ///
101
+ /// Can occur if those codepoints appeared in the input text, or were
102
+ /// explicitly mapped to that glyph.
103
+ InvalidCodepointMapping(Font, GlyphId, char, Option<Location>),
104
+ /// A glyph was mapped to a codepoint in the Unicode private use area, which is forbidden
105
+ /// by some standards, like for example PDF/A-2a.
106
+ // Note that the standard doesn't explicitly forbid it, but instead requires an ActualText
107
+ // attribute to be present. But we just completely forbid it, for simplicity.
108
+ UnicodePrivateArea(Font, GlyphId, char, Option<Location>),
109
+ /// A font has a license that requires explicit permission of the legal owner for embedding
110
+ /// but the standard requires font programs to be legally embeddable for universal rendering.
111
+ RestrictedLicense(Font),
112
+ /// No document language was set via the metadata, even though it is required
113
+ /// by the standard.
114
+ NoDocumentLanguage,
115
+ /// No title was provided for the document, even though it is required by
116
+ /// the standard.
117
+ NoDocumentTitle,
118
+ /// A figure or formula is missing an alt text.
119
+ MissingAltText(Option<Location>),
120
+ /// A heading is missing a title.
121
+ MissingHeadingTitle,
122
+ /// The document does not contain an outline.
123
+ MissingDocumentOutline,
124
+ /// An annotation is missing an alt text.
125
+ MissingAnnotationAltText(Option<Location>),
126
+ /// The date of the document is missing.
127
+ // We need this because for some standards we need to add the
128
+ // xmp:History attribute.
129
+ MissingDocumentDate,
130
+ /// The PDF contains transparency, which is forbidden by some standards (e.g. PDF/A-1).
131
+ Transparency(Option<Location>),
132
+ /// The PDF contains an image with `interpolate` set to `true`.
133
+ ImageInterpolation(Option<Location>),
134
+ /// The PDF contains an embedded file.
135
+ EmbeddedFile(EmbedError, Option<Location>),
136
+ /// The PDF contains no tagging.
137
+ MissingTagging,
138
+ /// The PDF contains another embedded PDF.
139
+ ///
140
+ /// This is currently forbidden in validated export because we cannot manually verify
141
+ /// whether the file actually fulfills all the criteria for the export mode.
142
+ EmbeddedPDF(Option<Location>),
143
+ /// A feature only available in a later PDF version was required.
144
+ RequiresNewerPdfVersion(VersionedFeature, Option<Location>),
145
+ }
146
+
147
+ /// Features that may require a later PDF version than the current one.
148
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
149
+ pub enum VersionedFeature {
150
+ /// Tabbing through the document according to the structure order.
151
+ StructureOrderTabbing,
152
+ /// Header and footer artifact subtypes.
153
+ HeaderFooterArtifactSubtypes,
154
+ /// Scope attribute for table header cells.
155
+ TableHeaderScope,
156
+ }
157
+
158
+ impl VersionedFeature {
159
+ /// Get the minimum PDF version required for this feature.
160
+ pub fn minimum_pdf_version(&self) -> PdfVersion {
161
+ match self {
162
+ VersionedFeature::StructureOrderTabbing => PdfVersion::Pdf15,
163
+ VersionedFeature::HeaderFooterArtifactSubtypes => PdfVersion::Pdf17,
164
+ VersionedFeature::TableHeaderScope => PdfVersion::Pdf15,
165
+ }
166
+ }
167
+ }
168
+
169
+ /// Collection of validators with at most one validator for each standard.
170
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
171
+ pub struct Validators {
172
+ a: Option<Archival>,
173
+ ua: Option<Accessibility>,
174
+ }
175
+
176
+ impl Validators {
177
+ /// Returns a filtered `Validators` containing only validators that prohibit the given error,
178
+ /// or `None` if no validator prohibits it.
179
+ pub fn prohibits(self, error: &ValidationError) -> Option<Self> {
180
+ let a = self.a.filter(|v| v.prohibits(error));
181
+ let ua = self.ua.filter(|v| v.prohibits(error));
182
+
183
+ let any = a.is_some() || ua.is_some();
184
+ any.then_some(Self { a, ua })
185
+ }
186
+
187
+ /// Returns `true` if no validators are set.
188
+ pub fn is_empty(self) -> bool {
189
+ self.a.is_none() && self.ua.is_none()
190
+ }
191
+
192
+ /// Returns the number of set validators.
193
+ pub fn len(self) -> usize {
194
+ (if self.a.is_some() { 1 } else { 0 }) + (if self.ua.is_some() { 1 } else { 0 })
195
+ }
196
+
197
+ /// Returns the PDF/A validator, if set.
198
+ pub fn archival(self) -> Option<Archival> {
199
+ self.a
200
+ }
201
+
202
+ /// Returns the PDF/UA accessibility validator, if set.
203
+ pub fn accessibility(self) -> Option<Accessibility> {
204
+ self.ua
205
+ }
206
+
207
+ /// Whether the font must supply valid Unicode code points for each of the
208
+ /// drawn glyphs.
209
+ pub(crate) fn requires_codepoint_mappings(self) -> bool {
210
+ self.into_iter().any(Validator::requires_codepoint_mappings)
211
+ }
212
+
213
+ /// Force the `DisplayDocTitle` flag set.
214
+ pub(crate) fn requires_display_doc_title(self) -> bool {
215
+ self.ua
216
+ .is_some_and(Accessibility::requires_display_doc_title)
217
+ }
218
+
219
+ /// Force sRGB profiles for `DeviceGray` and `DeviceRgb` colorspaces.
220
+ pub(crate) fn requires_no_device_cs(self) -> bool {
221
+ self.a.is_some_and(Archival::requires_no_device_cs)
222
+ }
223
+
224
+ /// Force the `Print` flag set and the `Hidden`, `Invisible`,
225
+ /// `ToggleNoView`, and `NoView` flags unset.
226
+ pub(crate) fn requires_annotation_flags(self) -> bool {
227
+ self.a.is_some_and(Archival::requires_annotation_flags)
228
+ }
229
+
230
+ /// Whether Tagged PDF must be enabled.
231
+ pub(crate) fn requires_tagging(self) -> bool {
232
+ self.into_iter().any(Validator::requires_tagging)
233
+ }
234
+
235
+ /// Whether XMP metadata must be written.
236
+ pub(crate) fn requires_xmp_metadata(self) -> bool {
237
+ self.into_iter().any(Validator::requires_xmp_metadata)
238
+ }
239
+
240
+ /// Whether any extension schemata should be descibed using the "pdfaSchema"
241
+ /// namespace.
242
+ pub(crate) fn requires_xmp_metadata_extension_schema(self) -> bool {
243
+ self.a
244
+ .is_some_and(Archival::requires_xmp_metadata_extension_schema)
245
+ }
246
+
247
+ /// Whether the `instanceID` field is allowed in XMP.
248
+ pub(crate) fn prohibits_instance_id_in_xmp_metadata(self) -> bool {
249
+ self.a
250
+ .is_some_and(Archival::prohibits_instance_id_in_xmp_metadata)
251
+ }
252
+
253
+ /// Whether the xmpMM:History entry is required.
254
+ pub(crate) fn requires_file_provenance_information(self) -> bool {
255
+ self.a
256
+ .is_some_and(Archival::requires_file_provenance_information)
257
+ }
258
+
259
+ /// Whether the `/Info` dictionary is allowed in the file trailer.
260
+ pub(crate) fn prohibits_info_dict(self) -> bool {
261
+ self.a.is_some_and(Archival::prohibits_info_dict)
262
+ }
263
+
264
+ /// Whether a non-printable file header is mandatory.
265
+ pub(crate) fn requires_binary_header(self) -> bool {
266
+ self.a.is_some_and(Archival::requires_binary_header)
267
+ }
268
+
269
+ /// Whether the `EmbeddedFiles` key in the name dictionary of the document
270
+ /// catalog dictionary should be written even if empty.
271
+ pub(crate) fn requires_embedded_files_when_empty(self) -> bool {
272
+ self.a
273
+ .is_some_and(Archival::requires_embedded_files_when_empty)
274
+ }
275
+
276
+ /// Whether any of these standards explicitly specifies the `/AF` key.
277
+ ///
278
+ /// The `/AF` key may be supported by the underlying PDF version instead:
279
+ /// Starting at PDF 2.0, the key is specified by ISO 32000 and does not need
280
+ /// to be added by PDF/A.
281
+ pub(crate) fn specifies_associated_files(self) -> bool {
282
+ self.a.is_some_and(Archival::specifies_associated_files)
283
+ }
284
+
285
+ pub(crate) fn output_intent(self) -> Option<OutputIntentSubtype<'static>> {
286
+ self.a.map(Archival::output_intent)
287
+ }
288
+
289
+ pub(crate) fn write_xmp<'a>(
290
+ self,
291
+ xmp: &mut XmpWriter<'a>,
292
+ extensions: &'a [crate::metadata::XmpExtension],
293
+ ) {
294
+ if self.requires_xmp_metadata_extension_schema() || !extensions.is_empty() {
295
+ let mut extension_schemas = xmp.extension_schemas();
296
+ if let Some(a) = self.a {
297
+ a.write_xmp_extension_schema_description(&mut extension_schemas);
298
+ }
299
+ if let Some(ua) = self.ua {
300
+ ua.write_xmp_extension_schema_description(&mut extension_schemas);
301
+ }
302
+ for extension in extensions {
303
+ extension.serialize_schema(&mut extension_schemas);
304
+ }
305
+ }
306
+
307
+ if let Some(a) = self.a {
308
+ a.write_xmp(xmp);
309
+ }
310
+
311
+ if let Some(ua) = self.ua {
312
+ ua.write_xmp(xmp);
313
+ }
314
+ }
315
+
316
+ /// Returns the maximum PDF version allowed by all active validators.
317
+ pub fn max(self) -> PdfVersion {
318
+ self.a
319
+ .map_or(PdfVersion::MAX, |v| v.max())
320
+ .min(self.ua.map_or(PdfVersion::MAX, |v| v.max()))
321
+ }
322
+
323
+ /// Returns the minimum PDF version required by all active validators, if any.
324
+ pub fn min(self) -> Option<PdfVersion> {
325
+ self.a
326
+ .and_then(|v| v.min())
327
+ .max(self.ua.and_then(|v| v.min()))
328
+ }
329
+ }
330
+
331
+ impl IntoIterator for Validators {
332
+ type Item = Validator;
333
+ type IntoIter = std::iter::Flatten<std::array::IntoIter<Option<Validator>, 2>>;
334
+
335
+ fn into_iter(self) -> Self::IntoIter {
336
+ [self.a.map(Validator::A), self.ua.map(Validator::Ua)]
337
+ .into_iter()
338
+ .flatten()
339
+ }
340
+ }
341
+
342
+ /// A builder for constructing a [`Validators`] collection.
343
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
344
+ pub struct ValidatorsBuilder(Validators);
345
+
346
+ impl ValidatorsBuilder {
347
+ /// Set a validator, overwriting the current one if the same standard family is already set.
348
+ pub fn set_validator(self, validator: Validator) -> Self {
349
+ match validator {
350
+ Validator::A(a) => self.with_archival_validator(a),
351
+ Validator::Ua(ua) => self.with_accessibility_validator(ua),
352
+ }
353
+ }
354
+
355
+ /// Set the PDF/A validator, overwriting the current one if already set.
356
+ pub fn with_archival_validator(mut self, archival: Archival) -> Self {
357
+ self.0.a = Some(archival);
358
+ self
359
+ }
360
+
361
+ /// Set the PDF/UA accessibility validator, overwriting the current one if already set.
362
+ pub fn with_accessibility_validator(mut self, accessibility: Accessibility) -> Self {
363
+ self.0.ua = Some(accessibility);
364
+ self
365
+ }
366
+
367
+ pub(crate) fn finish(self) -> Result<Validators, Validators> {
368
+ let min = self.0.min().unwrap_or(PdfVersion::MIN);
369
+ let max = self.0.max();
370
+
371
+ if min > max {
372
+ Err(self.0)
373
+ } else {
374
+ Ok(self.0)
375
+ }
376
+ }
377
+ }
378
+
379
+ /// A PDF validator for a specific conformance standard.
380
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
381
+ pub enum Validator {
382
+ /// A PDF/A validator.
383
+ A(Archival),
384
+ /// A PDF/UA accessibility validator.
385
+ Ua(Accessibility),
386
+ }
387
+
388
+ impl Validator {
389
+ fn requires_codepoint_mappings(self) -> bool {
390
+ match self {
391
+ Self::A(a) => a.requires_codepoint_mappings(),
392
+ Self::Ua(ua) => ua.requires_codepoint_mappings(),
393
+ }
394
+ }
395
+
396
+ fn requires_tagging(self) -> bool {
397
+ match self {
398
+ Self::A(a) => a.requires_tagging(),
399
+ Self::Ua(ua) => ua.requires_tagging(),
400
+ }
401
+ }
402
+
403
+ fn requires_xmp_metadata(self) -> bool {
404
+ match self {
405
+ Self::A(a) => a.requires_xmp_metadata(),
406
+ Self::Ua(ua) => ua.requires_xmp_metadata(),
407
+ }
408
+ }
409
+
410
+ /// Minimum PDF version required to use this validator, if any.
411
+ pub fn min(self) -> Option<PdfVersion> {
412
+ match self {
413
+ Self::A(a) => a.min(),
414
+ Self::Ua(ua) => ua.min(),
415
+ }
416
+ }
417
+
418
+ /// Maximum PDF version this standard can be used with.
419
+ pub fn max(self) -> PdfVersion {
420
+ match self {
421
+ Self::A(a) => a.max(),
422
+ Self::Ua(ua) => ua.max(),
423
+ }
424
+ }
425
+
426
+ /// Returns a human-readable string representation of the validator.
427
+ pub fn as_str(self) -> &'static str {
428
+ match self {
429
+ Self::A(a) => a.as_str(),
430
+ Self::Ua(ua) => ua.as_str(),
431
+ }
432
+ }
433
+ }
434
+
435
+ impl From<Archival> for Validator {
436
+ fn from(a: Archival) -> Self {
437
+ Self::A(a)
438
+ }
439
+ }
440
+
441
+ impl From<Accessibility> for Validator {
442
+ fn from(ua: Accessibility) -> Self {
443
+ Self::Ua(ua)
444
+ }
445
+ }
446
+
447
+ /// A PDF/A conformance level.
448
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
449
+ #[allow(non_camel_case_types)]
450
+ pub enum Archival {
451
+ /// The validator for the PDF/A-1a standard.
452
+ ///
453
+ /// **Requirements**:
454
+ /// - All requirements of PDF/A-1b.
455
+ /// - You need to follow all requirements outlined in the _Other Notes_ section of the
456
+ /// [`tagging`] module.
457
+ /// - You need to follow all best practices when using [tags](`crate::interchange::tagging::Tag`), as outlined in the documentation
458
+ /// of each tag.
459
+ /// - Artifacts such as page numbers, backgrounds, cut marks and color bars should be specified
460
+ /// correspondingly as artifacts.
461
+ /// - Word boundaries need to be explicitly specified with a space. The same applies to words at
462
+ /// the end of a line that are not followed by punctuation.
463
+ /// - To the fullest extent possible, the logical structure of the document should be encoded
464
+ /// correspondingly in the tag tree using appropriate grouping tags.
465
+ /// - Language identifiers used must be valid according to RFC 3066.
466
+ /// - You should provide an alternate text to span content tags, if applicable.
467
+ /// - You should provide the expansion of abbreviations to span content tags, if applicable.
468
+ ///
469
+ /// [`tagging`]: crate::interchange::tagging
470
+ A1_A,
471
+ /// The validator for the PDF/A-1b standard.
472
+ ///
473
+ /// **Requirements**: -
474
+ A1_B,
475
+ /// The validator for the PDF/A-2a standard.
476
+ ///
477
+ /// **Requirements**:
478
+ /// - All requirements of PDF/A-2b.
479
+ /// - You need to follow all requirements outlined in the _Other Notes_ section of the
480
+ /// [`tagging`] module.
481
+ /// - You need to follow all best practices when using [tags](`crate::interchange::tagging::Tag`), as outlined in the documentation
482
+ /// of each tag.
483
+ /// - Artifacts such as page numbers, backgrounds, cut marks and color bars should be specified
484
+ /// correspondingly as artifacts.
485
+ /// - Word boundaries need to be explicitly specified with a space. The same applies to words at
486
+ /// the end of a line that are not followed by punctuation.
487
+ /// - To the fullest extent possible, the logical structure of the document should be encoded
488
+ /// correspondingly in the tag tree using appropriate grouping tags.
489
+ /// - Language identifiers used must be valid according to RFC 3066.
490
+ /// - You should provide an alternate text to span content tags, if applicable.
491
+ /// - You should provide the expansion of abbreviations to span content tags, if applicable.
492
+ ///
493
+ /// [`tagging`]: crate::interchange::tagging
494
+ A2_A,
495
+ /// The validator for the PDF/A-2b standard.
496
+ ///
497
+ /// **Requirements**:
498
+ /// - You should only use fonts that are legally embeddable in a file for unlimited,
499
+ /// universal rendering.
500
+ A2_B,
501
+ /// The validator for the PDF/A-2u standard.
502
+ ///
503
+ /// **Requirements**:
504
+ /// - All requirements of PDF/A-2b
505
+ A2_U,
506
+ /// The validator for the PDF/A-3a standard.
507
+ ///
508
+ /// **Requirements**:
509
+ /// - All requirements of PDF/A-2a
510
+ A3_A,
511
+ /// The validator for the PDF/A-3b standard.
512
+ ///
513
+ /// **Requirements**:
514
+ /// - All requirements of PDF/A-2b
515
+ A3_B,
516
+ /// The validator for the PDF/A-3u standard.
517
+ ///
518
+ /// **Requirements**:
519
+ /// - All requirements of PDF/A-2b
520
+ A3_U,
521
+ /// The validator for the PDF/A-4 standard.
522
+ ///
523
+ /// **Requirements**:
524
+ /// - While not required, it's recommended to enable tagging.
525
+ A4,
526
+ /// The validator for the PDF/A-4f standard.
527
+ ///
528
+ /// **Requirements**:
529
+ /// - All requirements of PDF/A-4
530
+ A4F,
531
+ /// The validator for the PDF/A-4e standard.
532
+ ///
533
+ /// **Requirements**:
534
+ /// - All requirements of PDF/A-4
535
+ A4E,
536
+ }
537
+
538
+ impl Archival {
539
+ fn prohibits(self, error: &ValidationError) -> bool {
540
+ match (self, error) {
541
+ // Forbidden under all PDF/A-1 profiles.
542
+ (
543
+ Self::A1_A | Self::A1_B,
544
+ ValidationError::TooLongString
545
+ | ValidationError::TooLongName
546
+ | ValidationError::TooLongArray
547
+ | ValidationError::TooLongDictionary
548
+ | ValidationError::TooLargeFloat
549
+ | ValidationError::TooManyIndirectObjects
550
+ | ValidationError::TooHighQNestingLevel
551
+ | ValidationError::ContainsPostScript(_)
552
+ | ValidationError::MissingCMYKProfile
553
+ | ValidationError::RestrictedLicense(_)
554
+ | ValidationError::MissingDocumentDate
555
+ | ValidationError::Transparency(_)
556
+ | ValidationError::ImageInterpolation(_)
557
+ | ValidationError::EmbeddedFile(EmbedError::Existence, _)
558
+ | ValidationError::EmbeddedPDF(_),
559
+ ) => true,
560
+ // Allowed under all PDF/A-1 profiles.
561
+ (
562
+ Self::A1_A | Self::A1_B,
563
+ ValidationError::InconsistentSeparationFallback(_)
564
+ | ValidationError::InvalidCodepointMapping(_, _, _, _)
565
+ | ValidationError::UnicodePrivateArea(_, _, _, _)
566
+ | ValidationError::NoDocumentTitle
567
+ | ValidationError::MissingHeadingTitle
568
+ | ValidationError::MissingDocumentOutline
569
+ | ValidationError::EmbeddedFile(_, _)
570
+ | ValidationError::RequiresNewerPdfVersion(
571
+ VersionedFeature::HeaderFooterArtifactSubtypes
572
+ | VersionedFeature::StructureOrderTabbing
573
+ | VersionedFeature::TableHeaderScope,
574
+ _,
575
+ ),
576
+ ) => false,
577
+ // Forbidden under PDF/A-1a but allowed under PDF/A-1b.
578
+ (
579
+ Self::A1_A | Self::A1_B,
580
+ ValidationError::ContainsNotDefGlyph(_, _, _)
581
+ | ValidationError::NoCodepointMapping(_, _, _)
582
+ | ValidationError::NoDocumentLanguage
583
+ | ValidationError::MissingAltText(_)
584
+ | ValidationError::MissingAnnotationAltText(_)
585
+ | ValidationError::MissingTagging,
586
+ ) => self == Self::A1_A,
587
+
588
+ // Forbidden under all PDF/A-2 and PDF/A-3 profiles.
589
+ (
590
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
591
+ ValidationError::TooLongString
592
+ | ValidationError::TooLongName
593
+ | ValidationError::TooManyIndirectObjects
594
+ | ValidationError::TooHighQNestingLevel
595
+ | ValidationError::ContainsPostScript(_)
596
+ | ValidationError::MissingCMYKProfile
597
+ | ValidationError::InconsistentSeparationFallback(_)
598
+ | ValidationError::ContainsNotDefGlyph(_, _, _)
599
+ | ValidationError::RestrictedLicense(_)
600
+ | ValidationError::MissingDocumentDate
601
+ | ValidationError::ImageInterpolation(_)
602
+ | ValidationError::EmbeddedPDF(_),
603
+ ) => true,
604
+ // Allowed under all PDF/A-2 and PDF/A-3 profiles.
605
+ (
606
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
607
+ ValidationError::TooLongArray
608
+ | ValidationError::TooLongDictionary
609
+ | ValidationError::TooLargeFloat
610
+ | ValidationError::NoDocumentTitle
611
+ | ValidationError::Transparency(_)
612
+ | ValidationError::MissingHeadingTitle
613
+ | ValidationError::MissingDocumentOutline
614
+ | ValidationError::RequiresNewerPdfVersion(
615
+ VersionedFeature::HeaderFooterArtifactSubtypes
616
+ | VersionedFeature::StructureOrderTabbing
617
+ | VersionedFeature::TableHeaderScope,
618
+ _,
619
+ ),
620
+ ) => false,
621
+ // Forbidden under PDF/A-2 but allowed under PDF/A-3.
622
+ (
623
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
624
+ ValidationError::EmbeddedFile(EmbedError::Existence, _),
625
+ ) => self == Self::A2_A || self == Self::A2_B || self == Self::A2_U,
626
+ // Forbidden under PDF/A-3 but allowed under PDF/A-2.
627
+ (
628
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
629
+ ValidationError::EmbeddedFile(
630
+ EmbedError::MissingDate
631
+ | EmbedError::MissingDescription
632
+ | EmbedError::MissingMimeType,
633
+ _,
634
+ ),
635
+ ) => self == Self::A3_A || self == Self::A3_B || self == Self::A3_U,
636
+ // Forbidden under PDF/A-2 and PDF/A-3 accessible profiles.
637
+ (
638
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
639
+ ValidationError::UnicodePrivateArea(_, _, _, _)
640
+ | ValidationError::NoDocumentLanguage
641
+ | ValidationError::MissingAltText(_)
642
+ | ValidationError::MissingAnnotationAltText(_)
643
+ | ValidationError::MissingTagging,
644
+ ) => self == Self::A2_A || self == Self::A3_A,
645
+ // Forbidden under PDF/A-2 and PDF/A-3 accessible and Unicode profiles.
646
+ (
647
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U,
648
+ ValidationError::NoCodepointMapping(_, _, _)
649
+ | ValidationError::InvalidCodepointMapping(_, _, _, _),
650
+ ) => {
651
+ self == Self::A2_A || self == Self::A2_U || self == Self::A3_A || self == Self::A3_U
652
+ }
653
+
654
+ // Forbidden under all PDF/A-4 profiles.
655
+ (
656
+ Self::A4 | Self::A4F | Self::A4E,
657
+ ValidationError::MissingCMYKProfile
658
+ | ValidationError::InconsistentSeparationFallback(_)
659
+ | ValidationError::ContainsNotDefGlyph(_, _, _)
660
+ | ValidationError::NoCodepointMapping(_, _, _)
661
+ | ValidationError::InvalidCodepointMapping(_, _, _, _)
662
+ | ValidationError::UnicodePrivateArea(_, _, _, _)
663
+ | ValidationError::RestrictedLicense(_)
664
+ | ValidationError::MissingDocumentDate
665
+ | ValidationError::ImageInterpolation(_)
666
+ | ValidationError::EmbeddedPDF(_),
667
+ ) => true,
668
+ // Allowed under all PDF/A-4 profiles.
669
+ (
670
+ Self::A4 | Self::A4F | Self::A4E,
671
+ ValidationError::TooLongString
672
+ | ValidationError::TooLongName
673
+ | ValidationError::TooLongArray
674
+ | ValidationError::TooLongDictionary
675
+ | ValidationError::TooLargeFloat
676
+ | ValidationError::TooManyIndirectObjects
677
+ | ValidationError::TooHighQNestingLevel
678
+ | ValidationError::ContainsPostScript(_)
679
+ | ValidationError::NoDocumentLanguage
680
+ | ValidationError::NoDocumentTitle
681
+ | ValidationError::MissingAltText(_)
682
+ | ValidationError::MissingHeadingTitle
683
+ | ValidationError::MissingDocumentOutline
684
+ | ValidationError::MissingAnnotationAltText(_)
685
+ | ValidationError::Transparency(_)
686
+ | ValidationError::EmbeddedFile(
687
+ EmbedError::MissingDate | EmbedError::MissingMimeType,
688
+ _,
689
+ )
690
+ | ValidationError::MissingTagging
691
+ | ValidationError::RequiresNewerPdfVersion(
692
+ VersionedFeature::HeaderFooterArtifactSubtypes
693
+ | VersionedFeature::StructureOrderTabbing
694
+ | VersionedFeature::TableHeaderScope,
695
+ _,
696
+ ),
697
+ ) => false,
698
+ // Forbidden under PDF/A-4 but allowed under other PDF/A-4 profiles.
699
+ (
700
+ Self::A4 | Self::A4F | Self::A4E,
701
+ ValidationError::EmbeddedFile(EmbedError::Existence, _),
702
+ ) => self == Self::A4,
703
+ // Allowed under PDF/A-4 but forbidden under other profiles.
704
+ (
705
+ Self::A4 | Self::A4F | Self::A4E,
706
+ ValidationError::EmbeddedFile(EmbedError::MissingDescription, _),
707
+ ) => self == Self::A4,
708
+ }
709
+ }
710
+
711
+ fn requires_codepoint_mappings(self) -> bool {
712
+ match self {
713
+ Self::A1_A
714
+ | Self::A2_A
715
+ | Self::A2_U
716
+ | Self::A3_A
717
+ | Self::A3_U
718
+ | Self::A4
719
+ | Self::A4F
720
+ | Self::A4E => true,
721
+ Self::A1_B | Self::A2_B | Self::A3_B => false,
722
+ }
723
+ }
724
+
725
+ fn requires_no_device_cs(self) -> bool {
726
+ match self {
727
+ Self::A1_A
728
+ | Self::A1_B
729
+ | Self::A2_A
730
+ | Self::A2_B
731
+ | Self::A2_U
732
+ | Self::A3_A
733
+ | Self::A3_B
734
+ | Self::A3_U
735
+ | Self::A4
736
+ | Self::A4F
737
+ | Self::A4E => true,
738
+ }
739
+ }
740
+
741
+ fn requires_annotation_flags(self) -> bool {
742
+ match self {
743
+ Self::A1_A
744
+ | Self::A1_B
745
+ | Self::A2_A
746
+ | Self::A2_B
747
+ | Self::A2_U
748
+ | Self::A3_A
749
+ | Self::A3_B
750
+ | Self::A3_U
751
+ | Self::A4
752
+ | Self::A4F
753
+ | Self::A4E => true,
754
+ }
755
+ }
756
+
757
+ fn requires_tagging(self) -> bool {
758
+ match self {
759
+ Self::A1_A | Self::A2_A | Self::A3_A => true,
760
+ Self::A1_B
761
+ | Self::A2_B
762
+ | Self::A2_U
763
+ | Self::A3_B
764
+ | Self::A3_U
765
+ | Self::A4
766
+ | Self::A4F
767
+ | Self::A4E => false,
768
+ }
769
+ }
770
+
771
+ fn requires_xmp_metadata(self) -> bool {
772
+ match self {
773
+ Self::A1_A
774
+ | Self::A1_B
775
+ | Self::A2_A
776
+ | Self::A2_B
777
+ | Self::A2_U
778
+ | Self::A3_A
779
+ | Self::A3_B
780
+ | Self::A3_U
781
+ | Self::A4
782
+ | Self::A4F
783
+ | Self::A4E => true,
784
+ }
785
+ }
786
+
787
+ fn requires_xmp_metadata_extension_schema(self) -> bool {
788
+ match self {
789
+ Self::A1_A
790
+ | Self::A1_B
791
+ | Self::A2_A
792
+ | Self::A2_B
793
+ | Self::A2_U
794
+ | Self::A3_A
795
+ | Self::A3_B
796
+ | Self::A3_U => true,
797
+ // Clause 6.7.2.3 of PDF/A-4 recommends ("should") a RELAX NG
798
+ // definition of its metadata contents to be embedded as an
799
+ // associated file. It no longer uses the inline schema definition
800
+ // using the "pdfaSchema" namespaces for extension schemata.
801
+ Self::A4 | Self::A4F | Self::A4E => false,
802
+ }
803
+ }
804
+
805
+ fn prohibits_instance_id_in_xmp_metadata(self) -> bool {
806
+ match self {
807
+ Self::A1_A | Self::A1_B => true,
808
+ Self::A2_A
809
+ | Self::A2_B
810
+ | Self::A2_U
811
+ | Self::A3_A
812
+ | Self::A3_B
813
+ | Self::A3_U
814
+ | Self::A4
815
+ | Self::A4F
816
+ | Self::A4E => false,
817
+ }
818
+ }
819
+
820
+ fn requires_file_provenance_information(self) -> bool {
821
+ match self {
822
+ Self::A1_A
823
+ | Self::A1_B
824
+ | Self::A2_A
825
+ | Self::A2_B
826
+ | Self::A2_U
827
+ | Self::A3_A
828
+ | Self::A3_B
829
+ | Self::A3_U
830
+ | Self::A4
831
+ | Self::A4F
832
+ | Self::A4E => true,
833
+ }
834
+ }
835
+
836
+ fn prohibits_info_dict(self) -> bool {
837
+ match self {
838
+ Self::A1_A
839
+ | Self::A1_B
840
+ | Self::A2_A
841
+ | Self::A2_B
842
+ | Self::A2_U
843
+ | Self::A3_A
844
+ | Self::A3_B
845
+ | Self::A3_U => false,
846
+ Self::A4 | Self::A4F | Self::A4E => true,
847
+ }
848
+ }
849
+
850
+ fn requires_binary_header(self) -> bool {
851
+ match self {
852
+ Self::A1_A
853
+ | Self::A1_B
854
+ | Self::A2_A
855
+ | Self::A2_B
856
+ | Self::A2_U
857
+ | Self::A3_A
858
+ | Self::A3_B
859
+ | Self::A3_U
860
+ | Self::A4
861
+ | Self::A4F
862
+ | Self::A4E => true,
863
+ }
864
+ }
865
+
866
+ fn requires_embedded_files_when_empty(self) -> bool {
867
+ match self {
868
+ Self::A1_A
869
+ | Self::A1_B
870
+ | Self::A2_A
871
+ | Self::A2_B
872
+ | Self::A2_U
873
+ | Self::A3_A
874
+ | Self::A3_B
875
+ | Self::A3_U
876
+ | Self::A4
877
+ | Self::A4E => false,
878
+ Self::A4F => true,
879
+ }
880
+ }
881
+
882
+ /// Whether this standard explicitly specifies the `/AF` key.
883
+ ///
884
+ /// The `/AF` key may be supported by the underlying PDF version instead:
885
+ /// Starting at PDF 2.0, the key is specified by ISO 32000 and does not need
886
+ /// to be added by PDF/A.
887
+ fn specifies_associated_files(self) -> bool {
888
+ match self {
889
+ Self::A3_A | Self::A3_B | Self::A3_U => true,
890
+ Self::A1_A
891
+ | Self::A1_B
892
+ | Self::A2_A
893
+ | Self::A2_B
894
+ | Self::A2_U
895
+ | Self::A4
896
+ | Self::A4F
897
+ | Self::A4E => false,
898
+ }
899
+ }
900
+
901
+ fn output_intent(self) -> OutputIntentSubtype<'static> {
902
+ match self {
903
+ Self::A1_A
904
+ | Self::A1_B
905
+ | Self::A2_A
906
+ | Self::A2_B
907
+ | Self::A2_U
908
+ | Self::A3_A
909
+ | Self::A3_B
910
+ | Self::A3_U
911
+ | Self::A4
912
+ | Self::A4F
913
+ | Self::A4E => OutputIntentSubtype::PDFA,
914
+ }
915
+ }
916
+
917
+ fn write_xmp(self, xmp: &mut XmpWriter) {
918
+ match self {
919
+ Self::A1_A => {
920
+ xmp.pdfa_part(1);
921
+ xmp.pdfa_conformance("A");
922
+ }
923
+ Self::A1_B => {
924
+ xmp.pdfa_part(1);
925
+ xmp.pdfa_conformance("B");
926
+ }
927
+ Self::A2_A => {
928
+ xmp.pdfa_part(2);
929
+ xmp.pdfa_conformance("A");
930
+ }
931
+ Self::A2_B => {
932
+ xmp.pdfa_part(2);
933
+ xmp.pdfa_conformance("B");
934
+ }
935
+ Self::A2_U => {
936
+ xmp.pdfa_part(2);
937
+ xmp.pdfa_conformance("U");
938
+ }
939
+ Self::A3_A => {
940
+ xmp.pdfa_part(3);
941
+ xmp.pdfa_conformance("A");
942
+ }
943
+ Self::A3_B => {
944
+ xmp.pdfa_part(3);
945
+ xmp.pdfa_conformance("B");
946
+ }
947
+ Self::A3_U => {
948
+ xmp.pdfa_part(3);
949
+ xmp.pdfa_conformance("U");
950
+ }
951
+ Self::A4 => {
952
+ xmp.pdfa_part(4);
953
+ xmp.pdfa_rev(2020);
954
+ }
955
+ Self::A4F => {
956
+ xmp.pdfa_part(4);
957
+ xmp.pdfa_rev(2020);
958
+ xmp.pdfa_conformance("F");
959
+ }
960
+ Self::A4E => {
961
+ xmp.pdfa_part(4);
962
+ xmp.pdfa_rev(2020);
963
+ xmp.pdfa_conformance("E");
964
+ }
965
+ }
966
+ }
967
+
968
+ fn write_xmp_extension_schema_description(
969
+ self,
970
+ extension_schemas: &mut PdfAExtSchemasWriter<'_, '_>,
971
+ ) {
972
+ if !self.requires_xmp_metadata_extension_schema() {
973
+ return;
974
+ }
975
+
976
+ extension_schemas
977
+ .xmp_media_management()
978
+ .properties()
979
+ .describe_instance_id();
980
+ extension_schemas.pdf().properties().describe_all();
981
+ }
982
+
983
+ /// Returns a human-readable string representation of the conformance level.
984
+ pub fn as_str(self) -> &'static str {
985
+ match self {
986
+ Self::A1_A => "PDF/A-1a",
987
+ Self::A1_B => "PDF/A-1b",
988
+ Self::A2_A => "PDF/A-2a",
989
+ Self::A2_B => "PDF/A-2b",
990
+ Self::A2_U => "PDF/A-2u",
991
+ Self::A3_A => "PDF/A-3a",
992
+ Self::A3_B => "PDF/A-3b",
993
+ Self::A3_U => "PDF/A-3u",
994
+ Self::A4 => "PDF/A-4",
995
+ Self::A4F => "PDF/A-4f",
996
+ Self::A4E => "PDF/A-4e",
997
+ }
998
+ }
999
+
1000
+ /// Minimum PDF version required to use this standard, if any.
1001
+ pub const fn min(self) -> Option<PdfVersion> {
1002
+ match self {
1003
+ // PDF/A-1 through 3 require XMP `/Metadata` streams, which require PDF 1.4.
1004
+ Self::A1_A | Self::A1_B => Some(PdfVersion::Pdf14),
1005
+ Self::A2_A | Self::A2_B | Self::A2_U => Some(PdfVersion::Pdf14),
1006
+ Self::A3_A | Self::A3_B | Self::A3_U => Some(PdfVersion::Pdf14),
1007
+ Self::A4 | Self::A4F | Self::A4E => Some(PdfVersion::Pdf20),
1008
+ }
1009
+ }
1010
+
1011
+ /// Maximum PDF version this standard can be used with.
1012
+ pub const fn max(self) -> PdfVersion {
1013
+ match self {
1014
+ Self::A1_A | Self::A1_B => PdfVersion::Pdf14,
1015
+ Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U => {
1016
+ PdfVersion::Pdf17
1017
+ }
1018
+ Self::A4 | Self::A4F | Self::A4E => PdfVersion::Pdf20,
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ /// A validator for exporting PDF documents to a specific subset of PDF.
1024
+ #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1025
+ #[allow(non_camel_case_types)]
1026
+ pub enum Accessibility {
1027
+ /// The validator for the PDF/UA-1 standard.
1028
+ ///
1029
+ /// **Requirements**:
1030
+ ///
1031
+ /// General:
1032
+ /// - All real content should be tagged accordingly.
1033
+ /// - All artifacts should be marked accordingly.
1034
+ /// - The tag tree should reflect the logical reading order of the
1035
+ /// document.
1036
+ /// - Information should not be conveyed by contrast, color, format
1037
+ /// or layout.
1038
+ /// - All "best practice" notes in [`TagKind`] need to be complied with.
1039
+ ///
1040
+ /// Text:
1041
+ /// - You should make use of the `Alt`, `ActualText`, `Lang` and `Expansion` attributes
1042
+ /// whenever possible.
1043
+ /// - Usually, you can provide an empty string as `Lang` to indicate that a language is unknown.
1044
+ /// You should not do that in PDF/UA.
1045
+ /// - Stretchable characters (such as brackets, which often consist of several glyphs)
1046
+ /// should be marked accordingly with `ActualText`.
1047
+ ///
1048
+ /// Graphics:
1049
+ /// - Graphics should be tagged as figures (unless they are an artifact).
1050
+ /// - Graphics need to be followed by a caption.
1051
+ /// - Graphics that possess semantic values only in combination with other graphics
1052
+ /// should be tagged with a single Figure tag for each figure.
1053
+ /// - If a more accessible representation exists, it should be used over graphics.
1054
+ ///
1055
+ /// Headings:
1056
+ /// - Headings should be tagged as such.
1057
+ /// - For not strongly structured documents, H1 should be the first
1058
+ /// heading.
1059
+ ///
1060
+ /// Tables:
1061
+ /// - Tables should include headers and be tagged accordingly.
1062
+ /// - Tables should only be used to represent content within logical row/column relationship.
1063
+ ///
1064
+ /// Lists:
1065
+ /// - List items should be tagged with Li tags, if necessary also with
1066
+ /// Lbl and LBody tags.
1067
+ /// - Lists should only be used when the content is intended to be read
1068
+ /// as a list.
1069
+ ///
1070
+ /// Mathematical expressions:
1071
+ /// - All mathematical expressions should be enclosed with
1072
+ /// a `Formula` tag.
1073
+ ///
1074
+ /// Headers and footers:
1075
+ /// - Headers and footers should be marked as corresponding
1076
+ /// artifacts.
1077
+ ///
1078
+ /// Notes and references:
1079
+ /// - Footnotes, endnotes, note labels and references should be
1080
+ /// tagged accordingly and use tagged annotations.
1081
+ /// - Footnotes and end notes should use the `Note` tag.
1082
+ ///
1083
+ /// Navigation:
1084
+ /// - The document must contain an outline, and it should reflect
1085
+ /// the reading order of the document.
1086
+ /// - Page labels should be semantically appropriate.
1087
+ ///
1088
+ /// Annotations:
1089
+ /// - Annotations should be present in the tag tree in the correct
1090
+ /// reading order.
1091
+ ///
1092
+ /// Fonts:
1093
+ /// - You should only use fonts that are legally embeddable in a file for unlimited,
1094
+ /// universal rendering.
1095
+ ///
1096
+ /// [`TagKind`]: crate::interchange::tagging::TagKind
1097
+ UA1,
1098
+ }
1099
+
1100
+ impl Accessibility {
1101
+ fn prohibits(self, error: &ValidationError) -> bool {
1102
+ match (self, error) {
1103
+ (
1104
+ Self::UA1,
1105
+ ValidationError::ContainsNotDefGlyph(_, _, _)
1106
+ | ValidationError::NoCodepointMapping(_, _, _)
1107
+ | ValidationError::InvalidCodepointMapping(_, _, _, _)
1108
+ | ValidationError::RestrictedLicense(_)
1109
+ | ValidationError::NoDocumentTitle
1110
+ | ValidationError::MissingAltText(_)
1111
+ | ValidationError::MissingHeadingTitle
1112
+ | ValidationError::MissingDocumentOutline
1113
+ | ValidationError::MissingAnnotationAltText(_)
1114
+ | ValidationError::EmbeddedFile(EmbedError::MissingDescription, _)
1115
+ | ValidationError::MissingTagging
1116
+ | ValidationError::EmbeddedPDF(_)
1117
+ | ValidationError::RequiresNewerPdfVersion(
1118
+ VersionedFeature::HeaderFooterArtifactSubtypes
1119
+ | VersionedFeature::StructureOrderTabbing
1120
+ | VersionedFeature::TableHeaderScope,
1121
+ _,
1122
+ ),
1123
+ ) => true,
1124
+ (
1125
+ Self::UA1,
1126
+ ValidationError::TooLongString
1127
+ | ValidationError::TooLongName
1128
+ | ValidationError::TooLongArray
1129
+ | ValidationError::TooLongDictionary
1130
+ | ValidationError::TooLargeFloat
1131
+ | ValidationError::TooManyIndirectObjects
1132
+ | ValidationError::TooHighQNestingLevel
1133
+ | ValidationError::ContainsPostScript(_)
1134
+ | ValidationError::MissingCMYKProfile
1135
+ | ValidationError::InconsistentSeparationFallback(_)
1136
+ | ValidationError::UnicodePrivateArea(_, _, _, _)
1137
+ | ValidationError::NoDocumentLanguage
1138
+ | ValidationError::Transparency(_)
1139
+ | ValidationError::ImageInterpolation(_)
1140
+ | ValidationError::EmbeddedFile(
1141
+ EmbedError::Existence | EmbedError::MissingDate | EmbedError::MissingMimeType,
1142
+ _,
1143
+ )
1144
+ | ValidationError::MissingDocumentDate,
1145
+ ) => false,
1146
+ }
1147
+ }
1148
+
1149
+ fn requires_codepoint_mappings(self) -> bool {
1150
+ match self {
1151
+ Self::UA1 => true,
1152
+ }
1153
+ }
1154
+
1155
+ fn requires_display_doc_title(self) -> bool {
1156
+ match self {
1157
+ Self::UA1 => true,
1158
+ }
1159
+ }
1160
+
1161
+ const fn requires_tagging(self) -> bool {
1162
+ true
1163
+ }
1164
+
1165
+ fn requires_xmp_metadata(self) -> bool {
1166
+ match self {
1167
+ Self::UA1 => true,
1168
+ }
1169
+ }
1170
+
1171
+ fn write_xmp(self, xmp: &mut XmpWriter) {
1172
+ match self {
1173
+ Self::UA1 => {
1174
+ xmp.pdfua_part(1);
1175
+ }
1176
+ }
1177
+ }
1178
+
1179
+ fn write_xmp_extension_schema_description(
1180
+ self,
1181
+ extension_schemas: &mut PdfAExtSchemasWriter<'_, '_>,
1182
+ ) {
1183
+ // Needs to be updated if [`Self::write_xmp`] gains more properties.
1184
+ extension_schemas.pdfua_id().properties().describe_part();
1185
+ }
1186
+
1187
+ /// Returns a human-readable string representation of the accessibility level.
1188
+ pub fn as_str(self) -> &'static str {
1189
+ match self {
1190
+ Self::UA1 => "PDF/UA-1",
1191
+ }
1192
+ }
1193
+
1194
+ /// Minimum PDF version required to use this standard, if any.
1195
+ pub const fn min(self) -> Option<PdfVersion> {
1196
+ match self {
1197
+ // PDF/UA-1 requires Tagged PDF and XMP `/Metadata` streams, which both require PDF 1.4.
1198
+ Self::UA1 => Some(PdfVersion::Pdf14),
1199
+ }
1200
+ }
1201
+
1202
+ /// Maximum PDF version this standard can be used with.
1203
+ pub const fn max(self) -> PdfVersion {
1204
+ match self {
1205
+ // PDF/UA-1 is specified against PDF 1.7.
1206
+ Self::UA1 => PdfVersion::Pdf17,
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ #[derive(Debug, Default, Clone, PartialEq)]
1212
+ pub(crate) struct ValidationStore {
1213
+ /// Maps from the name of a Separation colorant to a hash of its fallback
1214
+ /// color. Used to track that a name is only ever matched with a single
1215
+ /// fallback color. Since Krilla manages the `tintTransform` functions,
1216
+ /// those are always equivalent.
1217
+ separation_fallback_map: HashMap<SeparationColorant, RegularColor>,
1218
+ }
1219
+
1220
+ impl ValidationStore {
1221
+ pub(crate) fn new() -> Self {
1222
+ Default::default()
1223
+ }
1224
+
1225
+ /// Register a colorant and its fallback and raise an error if it already
1226
+ /// exists.
1227
+ pub(crate) fn validate_separation(
1228
+ &mut self,
1229
+ separation: &SeparationSpace,
1230
+ ) -> Result<(), ValidationError> {
1231
+ if self
1232
+ .separation_fallback_map
1233
+ .entry(separation.colorant.clone())
1234
+ .or_insert(separation.fallback)
1235
+ == &separation.fallback
1236
+ {
1237
+ Ok(())
1238
+ } else {
1239
+ Err(ValidationError::InconsistentSeparationFallback(
1240
+ separation.colorant.clone(),
1241
+ ))
1242
+ }
1243
+ }
1244
+ }