pdfrb 0.6.0 → 0.7.10

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/RELEASE.md +52 -33
  4. data/data/pdfrb/afm/Courier-Bold.afm +342 -342
  5. data/data/pdfrb/afm/Courier-BoldOblique.afm +342 -342
  6. data/data/pdfrb/afm/Courier-Oblique.afm +342 -342
  7. data/data/pdfrb/afm/Courier.afm +342 -342
  8. data/data/pdfrb/afm/Helvetica-Bold.afm +2827 -2827
  9. data/data/pdfrb/afm/Helvetica-BoldOblique.afm +2827 -2827
  10. data/data/pdfrb/afm/Helvetica-Oblique.afm +3051 -3051
  11. data/data/pdfrb/afm/Helvetica.afm +3051 -3051
  12. data/data/pdfrb/afm/Symbol.afm +213 -213
  13. data/data/pdfrb/afm/Times-Bold.afm +2588 -2588
  14. data/data/pdfrb/afm/Times-BoldItalic.afm +2384 -2384
  15. data/data/pdfrb/afm/Times-Italic.afm +2667 -2667
  16. data/data/pdfrb/afm/Times-Roman.afm +2419 -2419
  17. data/data/pdfrb/afm/ZapfDingbats.afm +225 -225
  18. data/data/pdfrb/layout/hyphenation_en.txt +408 -0
  19. data/lib/pdfrb/color/default_profile.rb +213 -0
  20. data/lib/pdfrb/color/icc_validator.rb +69 -0
  21. data/lib/pdfrb/color.rb +2 -0
  22. data/lib/pdfrb/conformance/ltv.rb +112 -0
  23. data/lib/pdfrb/conformance/pades.rb +198 -0
  24. data/lib/pdfrb/conformance/pdf_2_af.rb +185 -0
  25. data/lib/pdfrb/conformance/pdf_a.rb +123 -0
  26. data/lib/pdfrb/conformance/pdf_a4_deep.rb +94 -0
  27. data/lib/pdfrb/conformance/pdf_ua2_deep.rb +93 -0
  28. data/lib/pdfrb/conformance/pdf_ua_tagging_deep.rb +125 -0
  29. data/lib/pdfrb/conformance/pdf_vt.rb +128 -0
  30. data/lib/pdfrb/conformance/pdf_x.rb +66 -1
  31. data/lib/pdfrb/conformance/tagged_pdf.rb +182 -0
  32. data/lib/pdfrb/conformance.rb +8 -0
  33. data/lib/pdfrb/content/canvas.rb +137 -0
  34. data/lib/pdfrb/content/operators/inline_image.rb +53 -0
  35. data/lib/pdfrb/content/operators.rb +1 -0
  36. data/lib/pdfrb/content/parser.rb +82 -0
  37. data/lib/pdfrb/digital_signature/timestamp_client.rb +86 -0
  38. data/lib/pdfrb/digital_signature.rb +1 -0
  39. data/lib/pdfrb/document.rb +29 -0
  40. data/lib/pdfrb/encryption/public_key_security_handler.rb +146 -0
  41. data/lib/pdfrb/encryption/standard_security_handler.rb +98 -3
  42. data/lib/pdfrb/encryption/v5_writer.rb +108 -0
  43. data/lib/pdfrb/encryption.rb +3 -0
  44. data/lib/pdfrb/font_loader/type3.rb +65 -0
  45. data/lib/pdfrb/font_loader.rb +1 -0
  46. data/lib/pdfrb/image_loader/gif.rb +246 -0
  47. data/lib/pdfrb/image_loader/png.rb +76 -19
  48. data/lib/pdfrb/image_loader/tiff.rb +257 -0
  49. data/lib/pdfrb/image_loader.rb +2 -0
  50. data/lib/pdfrb/layout/font_fallback.rb +203 -0
  51. data/lib/pdfrb/layout/hyphenation.rb +120 -0
  52. data/lib/pdfrb/layout/justification_kashidas.rb +72 -0
  53. data/lib/pdfrb/layout/multi_cell_text_layout.rb +65 -0
  54. data/lib/pdfrb/layout/multi_page_table_box.rb +155 -0
  55. data/lib/pdfrb/layout/polygon_frame.rb +106 -0
  56. data/lib/pdfrb/layout/table_box.rb +154 -23
  57. data/lib/pdfrb/layout/text_shaper.rb +129 -0
  58. data/lib/pdfrb/layout.rb +7 -0
  59. data/lib/pdfrb/model/type/page_piece_info.rb +58 -0
  60. data/lib/pdfrb/model/type.rb +3 -0
  61. data/lib/pdfrb/source/linearization_reader.rb +76 -0
  62. data/lib/pdfrb/source/recovery.rb +65 -1
  63. data/lib/pdfrb/source/tokenizer.rb +21 -0
  64. data/lib/pdfrb/source.rb +1 -0
  65. data/lib/pdfrb/task/regenerate_appearances.rb +98 -0
  66. data/lib/pdfrb/task/thumbnail.rb +133 -0
  67. data/lib/pdfrb/task.rb +2 -0
  68. data/lib/pdfrb/version.rb +1 -1
  69. metadata +35 -3
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module Pdfrb
5
+ module Conformance
6
+ # Long-Term Validation (LTV) per ETSI EN 319 102-1 Annex B. LTV
7
+ # preserves the validity of a signature beyond the validity
8
+ # period of the signer's certificate by archiving revocation
9
+ # data and timestamping the archive over time.
10
+ #
11
+ # LTV requires:
12
+ # * At least one signature with /ByteRange and /Contents.
13
+ # * /DSS dictionary on Catalog with /Certs, /CRLs, /OCSPs as
14
+ # applicable.
15
+ # * For archival (LTA): a /DocumentTimeStamp on the catalog.
16
+ module Ltv
17
+ module_function
18
+
19
+ RULESET = RuleSet.new("LTV").tap do |rs|
20
+ rs.register(Rule.new(
21
+ id: "ltv-1",
22
+ description: "/DSS dictionary required for LTV",
23
+ severity: :error,
24
+ spec_clause: "ETSI EN 319 102-1 B.5",
25
+ check: ->(doc) {
26
+ next nil if doc.catalog[:DSS]
27
+
28
+ Violation.new(
29
+ rule_id: "ltv-1",
30
+ message: "LTV requires /DSS dictionary on Catalog",
31
+ object: "Catalog/DSS",
32
+ severity: :error,
33
+ spec_clause: "ETSI EN 319 102-1 B.5"
34
+ )
35
+ }
36
+ ))
37
+
38
+ rs.register(Rule.new(
39
+ id: "ltv-2",
40
+ description: "/DSS must contain validation material",
41
+ severity: :warning,
42
+ spec_clause: "ETSI EN 319 102-1 B.5",
43
+ check: ->(doc) {
44
+ dss = doc.catalog[:DSS]
45
+ dss = doc.object(dss) if dss.is_a?(Pdfrb::Model::Reference)
46
+ next nil unless dss
47
+
48
+ has_material = dss[:Certs] || dss[:CRLs] || dss[:OCSPs] ||
49
+ dss[:VRI]
50
+ next nil if has_material
51
+
52
+ Violation.new(
53
+ rule_id: "ltv-2",
54
+ message: "/DSS is empty (no /Certs, /CRLs, /OCSPs, or /VRI)",
55
+ object: "Catalog/DSS",
56
+ severity: :warning,
57
+ spec_clause: "ETSI EN 319 102-1 B.5"
58
+ )
59
+ }
60
+ ))
61
+
62
+ rs.register(Rule.new(
63
+ id: "ltv-3",
64
+ description: "At least one signature should be present",
65
+ severity: :warning,
66
+ spec_clause: "ETSI EN 319 102-1 B.4",
67
+ check: ->(doc) {
68
+ fields = Pdfrb::Conformance::Pades.signature_fields(doc)
69
+ next nil if fields.any?
70
+
71
+ Violation.new(
72
+ rule_id: "ltv-3",
73
+ message: "LTV document has no signatures to validate",
74
+ object: "AcroForm/Fields",
75
+ severity: :warning,
76
+ spec_clause: "ETSI EN 319 102-1 B.4"
77
+ )
78
+ }
79
+ ))
80
+
81
+ rs.register(Rule.new(
82
+ id: "ltv-4",
83
+ description: "Document Time Stamp for archival (LTA)",
84
+ severity: :warning,
85
+ spec_clause: "ETSI EN 319 102-1 B.6",
86
+ check: ->(doc) {
87
+ fields = Pdfrb::Conformance::Pades.signature_fields(doc)
88
+ has_archival = fields.any? do |field|
89
+ v = field[:V]
90
+ v_obj = v.is_a?(Pdfrb::Model::Reference) ? doc.object(v) : v
91
+ v_obj && v_obj[:Type]&.to_sym == :DocTimeStamp
92
+ end
93
+ next nil if has_archival
94
+
95
+ Violation.new(
96
+ rule_id: "ltv-4",
97
+ message: "Long-Term Archival (LTA) recommends a Document Time Stamp",
98
+ object: "AcroForm/Fields",
99
+ severity: :warning,
100
+ spec_clause: "ETSI EN 319 102-1 B.6"
101
+ )
102
+ }
103
+ ))
104
+ end
105
+
106
+ def validate(document)
107
+ RULESET.validate(document)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module Pdfrb
5
+ module Conformance
6
+ # PAdES (ETSI EN 319 142-1) signature profile validation. Four
7
+ # baseline levels build on each other:
8
+ #
9
+ # B-B: Basic signatures — just the document hash + signer cert.
10
+ # B-T: B-B + trusted timestamp (RFC 3161) on the signature.
11
+ # B-LT: B-T + long-term validation material (DSS dictionary
12
+ # with revocation data, signer certs, TSA cert chain).
13
+ # B-LTA: B-LT + archival timestamp protecting the DSS itself.
14
+ #
15
+ # Each rule set checks signature dictionaries on the document's
16
+ # /AcroForm /Fields for the required components per level.
17
+ module Pades
18
+ ALLOWED_SIGNATURE_TYPES = %i[Sig DocTimeStamp].freeze
19
+
20
+ module_function
21
+
22
+ SHARED = RuleSet.new("PAdES-shared").tap do |rs|
23
+ rs.register(Rule.new(
24
+ id: "pades-1",
25
+ description: "At least one signature field must be present",
26
+ severity: :error,
27
+ spec_clause: "ETSI EN 319 142-1 5.2",
28
+ check: ->(doc) {
29
+ next nil if signature_fields(doc).any?
30
+
31
+ Violation.new(
32
+ rule_id: "pades-1",
33
+ message: "PAdES requires at least one signature field",
34
+ object: "AcroForm/Fields",
35
+ severity: :error,
36
+ spec_clause: "ETSI EN 319 142-1 5.2"
37
+ )
38
+ }
39
+ ))
40
+ end
41
+
42
+ BB = RuleSet.new("PAdES-B-B").tap do |rs|
43
+ SHARED.rules.each { |r| rs.register(r) }
44
+ rs.register(Rule.new(
45
+ id: "bb-1",
46
+ description: "B-B: each signature must have /Type /Sig or /DocTimeStamp",
47
+ severity: :error,
48
+ spec_clause: "ETSI EN 319 142-1 5.3",
49
+ check: ->(doc) {
50
+ vs = []
51
+ signature_fields(doc).each do |field|
52
+ v = field[:V]
53
+ next unless v
54
+
55
+ v_obj = v.is_a?(Pdfrb::Model::Reference) ? doc.object(v) : v
56
+ next if v_obj && ALLOWED_SIGNATURE_TYPES.include?(v_obj[:Type]&.to_sym)
57
+
58
+ vs << Violation.new(
59
+ rule_id: "bb-1",
60
+ message: "Signature V must be /Type /Sig or /DocTimeStamp",
61
+ object: "Field/V",
62
+ severity: :error,
63
+ spec_clause: "ETSI EN 319 142-1 5.3"
64
+ )
65
+ end
66
+ vs.empty? ? nil : vs
67
+ }
68
+ ))
69
+ end
70
+
71
+ BT = RuleSet.new("PAdES-B-T").tap do |rs|
72
+ BB.rules.each { |r| rs.register(r) }
73
+ rs.register(Rule.new(
74
+ id: "bt-1",
75
+ description: "B-T: signature must include a trusted timestamp",
76
+ severity: :error,
77
+ spec_clause: "ETSI EN 319 142-1 5.4",
78
+ check: ->(doc) {
79
+ vs = []
80
+ signature_fields(doc).each do |field|
81
+ v = field[:V]
82
+ next unless v
83
+
84
+ v_obj = v.is_a?(Pdfrb::Model::Reference) ? doc.object(v) : v
85
+ has_timestamp = v_obj && (
86
+ v_obj[:Type]&.to_sym == :DocTimeStamp ||
87
+ (v_obj[:ByteRange] && timestamp_in_signature?(v_obj))
88
+ )
89
+ next if has_timestamp
90
+
91
+ vs << Violation.new(
92
+ rule_id: "bt-1",
93
+ message: "B-T requires a trusted timestamp (RFC 3161)",
94
+ object: "Field/V",
95
+ severity: :error,
96
+ spec_clause: "ETSI EN 319 142-1 5.4"
97
+ )
98
+ end
99
+ vs.empty? ? nil : vs
100
+ }
101
+ ))
102
+ end
103
+
104
+ BLT = RuleSet.new("PAdES-B-LT").tap do |rs|
105
+ BT.rules.each { |r| rs.register(r) }
106
+ rs.register(Rule.new(
107
+ id: "blt-1",
108
+ description: "B-LT: DSS dictionary required for LTV material",
109
+ severity: :error,
110
+ spec_clause: "ETSI EN 319 142-1 5.5",
111
+ check: ->(doc) {
112
+ next nil if doc.catalog[:DSS]
113
+
114
+ Violation.new(
115
+ rule_id: "blt-1",
116
+ message: "B-LT requires /DSS dictionary with validation material",
117
+ object: "Catalog/DSS",
118
+ severity: :error,
119
+ spec_clause: "ETSI EN 319 142-1 5.5"
120
+ )
121
+ }
122
+ ))
123
+ end
124
+
125
+ BLTA = RuleSet.new("PAdES-B-LTA").tap do |rs|
126
+ BLT.rules.each { |r| rs.register(r) }
127
+ rs.register(Rule.new(
128
+ id: "blta-1",
129
+ description: "B-LTA: archival Document Time Stamp required",
130
+ severity: :error,
131
+ spec_clause: "ETSI EN 319 142-1 5.6",
132
+ check: ->(doc) {
133
+ # B-LTA needs a DocTimeStamp as the last signature,
134
+ # protecting the DSS.
135
+ has_archival = signature_fields(doc).any? do |field|
136
+ v = field[:V]
137
+ v_obj = v.is_a?(Pdfrb::Model::Reference) ? doc.object(v) : v
138
+ v_obj && v_obj[:Type]&.to_sym == :DocTimeStamp
139
+ end
140
+ next nil if has_archival
141
+
142
+ Violation.new(
143
+ rule_id: "blta-1",
144
+ message: "B-LTA requires an archival Document Time Stamp",
145
+ object: "AcroForm/Fields",
146
+ severity: :error,
147
+ spec_clause: "ETSI EN 319 142-1 5.6"
148
+ )
149
+ }
150
+ ))
151
+ end
152
+
153
+ LEVEL_RULESETS = {
154
+ "B-B": BB, "B-T": BT, "B-LT": BLT, "B-LTA": BLTA
155
+ }.freeze
156
+
157
+ def profiles
158
+ LEVEL_RULESETS.dup
159
+ end
160
+
161
+ def validate(document, level: :"B-B")
162
+ rs = LEVEL_RULESETS[level] || SHARED
163
+ rs.validate(document)
164
+ end
165
+
166
+ def signature_fields(document)
167
+ acroform = document.catalog[:AcroForm]
168
+ acroform = document.object(acroform) if acroform.is_a?(Pdfrb::Model::Reference)
169
+ return [] unless acroform
170
+
171
+ fields = acroform[:Fields]
172
+ fields = fields.value if fields.is_a?(Pdfrb::Model::PdfArray)
173
+ return [] unless fields.is_a?(::Array)
174
+
175
+ fields.filter_map do |ref|
176
+ obj = ref.is_a?(Pdfrb::Model::Reference) ? document.object(ref) : ref
177
+ next nil unless obj
178
+
179
+ obj[:FT]&.to_sym == :Sig ? obj : nil
180
+ end
181
+ end
182
+
183
+ # Heuristic: look for the standard ASN.1 timestamp OID
184
+ # (1.2.840.113549.1.7.2 signedData, with a TSTInfo content
185
+ # type). Real validation requires parsing the PKCS#7 — out of
186
+ # scope for this rule set, which only checks the structural
187
+ # presence of a ByteRange + Contents long enough to hold a
188
+ # timestamp token.
189
+ def timestamp_in_signature?(signature)
190
+ contents = signature[:Contents]
191
+ return false unless contents.is_a?(::String)
192
+
193
+ contents.bytesize > 1024
194
+ end
195
+ end
196
+ end
197
+ end
198
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ module Pdfrb
5
+ module Conformance
6
+ # PDF 2.0 Associated Files (AF) validation per Application Note
7
+ # 002 (sources at ~/src/pdfa/appnote-pdf20-002-af/). The AF
8
+ # mechanism attaches external files to PDF objects with a typed
9
+ # relationship (Source, Data, Alternative, Supplement,
10
+ # EncryptedPayload, Unspecified).
11
+ #
12
+ # This rule set checks:
13
+ # * /AF arrays are present where required (PDF 2.0 catalog).
14
+ # * Each /AF entry is a /Type /AssociatedFile or FileSpec dict
15
+ # with an /AFRelationship.
16
+ # * /AFRelationship is one of the six allowed values.
17
+ # * FileSpec dicts carry /UF + /EF.
18
+ # * EmbeddedFile streams carry /Subtype (MIME type).
19
+ module Pdf2AF
20
+ ALLOWED_RELATIONSHIPS = %i[Source Data Alternative Supplement
21
+ EncryptedPayload Unspecified].freeze
22
+
23
+ module_function
24
+
25
+ RULESET = RuleSet.new("PDF 2.0 AF").tap do |rs|
26
+ rs.register(Rule.new(
27
+ id: "af-1",
28
+ description: "/AF entries must declare AFRelationship",
29
+ severity: :error,
30
+ spec_clause: "PDF 2.0 App Note 002 3.1",
31
+ check: ->(doc) {
32
+ violations = []
33
+ each_af_entry(doc) do |container, entry|
34
+ next if relationship_of(entry)
35
+
36
+ violations << Violation.new(
37
+ rule_id: "af-1",
38
+ message: "/AF entry on #{container_label(container)} missing /AFRelationship",
39
+ object: container_label(container),
40
+ severity: :error,
41
+ spec_clause: "PDF 2.0 App Note 002 3.1"
42
+ )
43
+ end
44
+ violations
45
+ }
46
+ ))
47
+
48
+ rs.register(Rule.new(
49
+ id: "af-2",
50
+ description: "/AFRelationship must be one of the allowed values",
51
+ severity: :error,
52
+ spec_clause: "PDF 2.0 App Note 002 3.2",
53
+ check: ->(doc) {
54
+ violations = []
55
+ each_af_entry(doc) do |container, entry|
56
+ rel = relationship_of(entry)
57
+ next unless rel
58
+ next if ALLOWED_RELATIONSHIPS.include?(rel.to_sym)
59
+
60
+ violations << Violation.new(
61
+ rule_id: "af-2",
62
+ message: "/AFRelationship '#{rel}' is not one of #{ALLOWED_RELATIONSHIPS.join(', ')}",
63
+ object: container_label(container),
64
+ severity: :error,
65
+ spec_clause: "PDF 2.0 App Note 002 3.2"
66
+ )
67
+ end
68
+ violations
69
+ }
70
+ ))
71
+
72
+ rs.register(Rule.new(
73
+ id: "af-3",
74
+ description: "FileSpec dicts must carry /UF and /EF",
75
+ severity: :error,
76
+ spec_clause: "PDF 2.0 7.11.3",
77
+ check: ->(doc) {
78
+ violations = []
79
+ each_filespec(doc) do |_container, fs|
80
+ ok = fs[:UF] && fs[:EF]
81
+ next if ok
82
+
83
+ violations << Violation.new(
84
+ rule_id: "af-3",
85
+ message: "FileSpec missing /UF or /EF",
86
+ object: "FileSpec",
87
+ severity: :error,
88
+ spec_clause: "PDF 2.0 7.11.3"
89
+ )
90
+ end
91
+ violations
92
+ }
93
+ ))
94
+
95
+ rs.register(Rule.new(
96
+ id: "af-4",
97
+ description: "EmbeddedFile streams should declare /Subtype MIME",
98
+ severity: :warning,
99
+ spec_clause: "PDF 2.0 7.11.4",
100
+ check: ->(doc) {
101
+ violations = []
102
+ doc.each_indirect_object do |obj|
103
+ next unless obj.value[:Type] == :EmbeddedFile
104
+ next if obj.value[:Subtype]
105
+
106
+ violations << Violation.new(
107
+ rule_id: "af-4",
108
+ message: "EmbeddedFile missing /Subtype (MIME type)",
109
+ object: "EmbeddedFile",
110
+ severity: :warning,
111
+ spec_clause: "PDF 2.0 7.11.4"
112
+ )
113
+ end
114
+ violations
115
+ }
116
+ ))
117
+ end
118
+
119
+ def validate(document)
120
+ RULESET.validate(document)
121
+ end
122
+
123
+ # Walk every indirect object that has an /AF key, yielding
124
+ # (container_object, af_entry) for each entry in each /AF
125
+ # array. /AF entries may be References (resolves) or dicts.
126
+ def each_af_entry(document)
127
+ document.each_indirect_object do |container|
128
+ af = container.value[:AF]
129
+ next unless af
130
+
131
+ af_value = af.is_a?(Pdfrb::Model::PdfArray) ? af.value : af
132
+ entries = af_value.is_a?(::Array) ? af_value : [af_value]
133
+ entries.each do |e|
134
+ resolved = e.is_a?(Pdfrb::Model::Reference) ? document.object(e) : e
135
+ yield container, resolved if resolved
136
+ end
137
+ end
138
+ end
139
+
140
+ # Walk every FileSpec dict in the document.
141
+ def each_filespec(document)
142
+ document.each_indirect_object do |container|
143
+ fs = container.value[:Type] == :FileSpec ? container : nil
144
+ if fs
145
+ yield container, fs
146
+ next
147
+ end
148
+
149
+ # Also pick up FileSpecs referenced from /AF arrays.
150
+ af = container.value[:AF]
151
+ next unless af
152
+
153
+ af_value = af.is_a?(Pdfrb::Model::PdfArray) ? af.value : af
154
+ entries = af_value.is_a?(::Array) ? af_value : [af_value]
155
+ entries.each do |e|
156
+ resolved = e.is_a?(Pdfrb::Model::Reference) ? document.object(e) : e
157
+ next unless resolved && resolved.value[:Type] == :FileSpec
158
+
159
+ yield container, resolved
160
+ end
161
+ end
162
+ end
163
+
164
+ def relationship_of(entry)
165
+ return nil unless entry
166
+
167
+ value = entry.is_a?(Pdfrb::Model::Cos::Dictionary) ? entry.value : entry
168
+ return nil unless value.is_a?(::Hash)
169
+
170
+ # AssociatedFile dicts put the relationship on
171
+ # /AFRelationship; FileSpecs put it on the same key when
172
+ # they're AF-related.
173
+ value[:AFRelationship]
174
+ end
175
+
176
+ def container_label(obj)
177
+ type = obj.value[:Type]
178
+ return "Object/#{obj.oid}" unless type
179
+
180
+ "Object/#{type}/#{obj.oid}"
181
+ end
182
+ end
183
+ end
184
+ end
185
+ # rubocop:enable Metrics/BlockLength
@@ -80,6 +80,122 @@ module Pdfrb
80
80
  ))
81
81
  end
82
82
 
83
+ # Deep preflight rules common to all PDF/A levels. Each is a
84
+ # categorical ban: JavaScript, PS XObjects, non-A-3 embedded
85
+ # files, and unencoded text operators.
86
+ PREFLIGHT_DEEP = RuleSet.new("PDF/A-preflight").tap do |rs|
87
+ rs.register(Rule.new(
88
+ id: "preflight-1",
89
+ description: "JavaScript is forbidden",
90
+ severity: :error,
91
+ spec_clause: "ISO 19005-1 6.6",
92
+ check: ->(doc) {
93
+ vs = []
94
+ catalog = doc.catalog
95
+ names = catalog[:Names]
96
+ names = doc.object(names) if names.is_a?(Pdfrb::Model::Reference)
97
+ if names && (names[:JavaScript] || (names.is_a?(::Hash) && names[:JavaScript]))
98
+ vs << Violation.new(
99
+ rule_id: "preflight-1",
100
+ message: "/Names /JavaScript is forbidden in PDF/A",
101
+ object: "Catalog/Names/JavaScript",
102
+ severity: :error,
103
+ spec_clause: "ISO 19005-1 6.6"
104
+ )
105
+ end
106
+ if catalog[:AA] || catalog[:OpenAction]
107
+ vs << Violation.new(
108
+ rule_id: "preflight-1",
109
+ message: "Additional actions (/AA) and /OpenAction with JavaScript are restricted in PDF/A",
110
+ object: "Catalog",
111
+ severity: :warning,
112
+ spec_clause: "ISO 19005-1 6.6"
113
+ )
114
+ end
115
+ vs.empty? ? nil : vs
116
+ }
117
+ ))
118
+
119
+ rs.register(Rule.new(
120
+ id: "preflight-2",
121
+ description: "PostScript XObjects are forbidden",
122
+ severity: :error,
123
+ spec_clause: "ISO 19005-1 6.2.3",
124
+ check: ->(doc) {
125
+ vs = []
126
+ doc.each_indirect_object do |obj|
127
+ next unless obj.value[:Type] == :XObject
128
+ next unless obj.value[:Subtype] == :PS
129
+
130
+ vs << Violation.new(
131
+ rule_id: "preflight-2",
132
+ message: "PostScript XObjects are forbidden in PDF/A",
133
+ object: "XObject",
134
+ severity: :error,
135
+ spec_clause: "ISO 19005-1 6.2.3"
136
+ )
137
+ end
138
+ vs.empty? ? nil : vs
139
+ }
140
+ ))
141
+
142
+ rs.register(Rule.new(
143
+ id: "preflight-3",
144
+ description: "EmbeddedFile streams restricted",
145
+ severity: :error,
146
+ spec_clause: "ISO 19005-3 6.4",
147
+ check: ->(_doc) {
148
+ # Only PDF/A-3 allows EmbeddedFile. The rule
149
+ # is permissive by default; the per-level
150
+ # rule sets filter it down for A-1 and A-2.
151
+ }
152
+ ))
153
+
154
+ rs.register(Rule.new(
155
+ id: "preflight-4",
156
+ description: "Encryption is forbidden",
157
+ severity: :error,
158
+ spec_clause: "ISO 19005-1 6.1",
159
+ check: ->(doc) {
160
+ next nil unless doc.trailer && doc.trailer[:Encrypt]
161
+
162
+ Violation.new(
163
+ rule_id: "preflight-4",
164
+ message: "Encryption is forbidden in PDF/A",
165
+ object: "Trailer/Encrypt",
166
+ severity: :error,
167
+ spec_clause: "ISO 19005-1 6.1"
168
+ )
169
+ }
170
+ ))
171
+ end
172
+
173
+ # Embeds files forbidden in PDF/A-1 and PDF/A-2 (only A-3
174
+ # permits them).
175
+ PREFLIGHT_NO_EMBED = RuleSet.new("PDF/A-no-embedded").tap do |rs|
176
+ rs.register(Rule.new(
177
+ id: "preflight-3-a1a2",
178
+ description: "EmbeddedFile streams are forbidden in PDF/A-1 and A-2",
179
+ severity: :error,
180
+ spec_clause: "ISO 19005-1 6.4",
181
+ check: ->(doc) {
182
+ vs = []
183
+ doc.each_indirect_object do |obj|
184
+ next unless obj.value[:Type] == :EmbeddedFile
185
+
186
+ vs << Violation.new(
187
+ rule_id: "preflight-3-a1a2",
188
+ message: "EmbeddedFile streams are forbidden in PDF/A-1 and A-2",
189
+ object: "EmbeddedFile",
190
+ severity: :error,
191
+ spec_clause: "ISO 19005-1 6.4"
192
+ )
193
+ end
194
+ vs.empty? ? nil : vs
195
+ }
196
+ ))
197
+ end
198
+
83
199
  A1_SPECIFIC = RuleSet.new("PDF/A-1-specific").tap do |rs|
84
200
  rs.register(Rule.new(
85
201
  id: "a1-1", description: "JPEG2000 not allowed in A-1",
@@ -295,21 +411,28 @@ module Pdfrb
295
411
  A1 = RuleSet.new("PDF/A-1").tap do |rs|
296
412
  SHARED.rules.each { |r| rs.register(r) }
297
413
  A1_SPECIFIC.rules.each { |r| rs.register(r) }
414
+ PREFLIGHT_DEEP.rules.each { |r| rs.register(r) }
415
+ PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) }
298
416
  end
299
417
 
300
418
  A2 = RuleSet.new("PDF/A-2").tap do |rs|
301
419
  SHARED.rules.each { |r| rs.register(r) }
302
420
  A2_SPECIFIC.rules.each { |r| rs.register(r) }
421
+ PREFLIGHT_DEEP.rules.each { |r| rs.register(r) }
422
+ PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) }
303
423
  end
304
424
 
305
425
  A3 = RuleSet.new("PDF/A-3").tap do |rs|
306
426
  SHARED.rules.each { |r| rs.register(r) }
307
427
  A3_SPECIFIC.rules.each { |r| rs.register(r) }
428
+ PREFLIGHT_DEEP.rules.each { |r| rs.register(r) }
308
429
  end
309
430
 
310
431
  A4 = RuleSet.new("PDF/A-4").tap do |rs|
311
432
  SHARED.rules.each { |r| rs.register(r) }
312
433
  A4_SPECIFIC.rules.each { |r| rs.register(r) }
434
+ PREFLIGHT_DEEP.rules.each { |r| rs.register(r) }
435
+ PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) }
313
436
  end
314
437
 
315
438
  LEVEL_RULESETS = {