Module: Pdfrb::Conformance::Pdf2AF
- Defined in:
- lib/pdfrb/conformance/pdf_2_af.rb
Overview
PDF 2.0 Associated Files (AF) validation per Application Note 002 (sources at ~/src/pdfa/appnote-pdf20-002-af/). The AF mechanism attaches external files to PDF objects with a typed relationship (Source, Data, Alternative, Supplement, EncryptedPayload, Unspecified).
This rule set checks:
* /AF arrays are present where required (PDF 2.0 catalog).
* Each /AF entry is a /Type /AssociatedFile or FileSpec dict
with an /AFRelationship.
* /AFRelationship is one of the six allowed values.
* FileSpec dicts carry /UF + /EF.
* EmbeddedFile streams carry /Subtype (MIME type).
Constant Summary collapse
- ALLOWED_RELATIONSHIPS =
%i[Source Data Alternative Supplement EncryptedPayload Unspecified].freeze
- RULESET =
RuleSet.new("PDF 2.0 AF").tap do |rs| rs.register(Rule.new( id: "af-1", description: "/AF entries must declare AFRelationship", severity: :error, spec_clause: "PDF 2.0 App Note 002 3.1", check: ->(doc) { violations = [] each_af_entry(doc) do |container, entry| next if relationship_of(entry) violations << Violation.new( rule_id: "af-1", message: "/AF entry on #{container_label(container)} missing /AFRelationship", object: container_label(container), severity: :error, spec_clause: "PDF 2.0 App Note 002 3.1" ) end violations } )) rs.register(Rule.new( id: "af-2", description: "/AFRelationship must be one of the allowed values", severity: :error, spec_clause: "PDF 2.0 App Note 002 3.2", check: ->(doc) { violations = [] each_af_entry(doc) do |container, entry| rel = relationship_of(entry) next unless rel next if ALLOWED_RELATIONSHIPS.include?(rel.to_sym) violations << Violation.new( rule_id: "af-2", message: "/AFRelationship '#{rel}' is not one of #{ALLOWED_RELATIONSHIPS.join(', ')}", object: container_label(container), severity: :error, spec_clause: "PDF 2.0 App Note 002 3.2" ) end violations } )) rs.register(Rule.new( id: "af-3", description: "FileSpec dicts must carry /UF and /EF", severity: :error, spec_clause: "PDF 2.0 7.11.3", check: ->(doc) { violations = [] each_filespec(doc) do |_container, fs| ok = fs[:UF] && fs[:EF] next if ok violations << Violation.new( rule_id: "af-3", message: "FileSpec missing /UF or /EF", object: "FileSpec", severity: :error, spec_clause: "PDF 2.0 7.11.3" ) end violations } )) rs.register(Rule.new( id: "af-4", description: "EmbeddedFile streams should declare /Subtype MIME", severity: :warning, spec_clause: "PDF 2.0 7.11.4", check: ->(doc) { violations = [] doc.each_indirect_object do |obj| next unless obj.value[:Type] == :EmbeddedFile next if obj.value[:Subtype] violations << Violation.new( rule_id: "af-4", message: "EmbeddedFile missing /Subtype (MIME type)", object: "EmbeddedFile", severity: :warning, spec_clause: "PDF 2.0 7.11.4" ) end violations } )) end
Class Method Summary collapse
- .container_label(obj) ⇒ Object
-
.each_af_entry(document) ⇒ Object
Walk every indirect object that has an /AF key, yielding (container_object, af_entry) for each entry in each /AF array.
-
.each_filespec(document) ⇒ Object
Walk every FileSpec dict in the document.
- .relationship_of(entry) ⇒ Object
- .validate(document) ⇒ Object
Class Method Details
.container_label(obj) ⇒ Object
176 177 178 179 180 181 |
# File 'lib/pdfrb/conformance/pdf_2_af.rb', line 176 def container_label(obj) type = obj.value[:Type] return "Object/#{obj.oid}" unless type "Object/#{type}/#{obj.oid}" end |
.each_af_entry(document) ⇒ Object
Walk every indirect object that has an /AF key, yielding (container_object, af_entry) for each entry in each /AF array. /AF entries may be References (resolves) or dicts.
126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/pdfrb/conformance/pdf_2_af.rb', line 126 def each_af_entry(document) document.each_indirect_object do |container| af = container.value[:AF] next unless af af_value = af.is_a?(Pdfrb::Model::PdfArray) ? af.value : af entries = af_value.is_a?(::Array) ? af_value : [af_value] entries.each do |e| resolved = e.is_a?(Pdfrb::Model::Reference) ? document.object(e) : e yield container, resolved if resolved end end end |
.each_filespec(document) ⇒ Object
Walk every FileSpec dict in the document.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/pdfrb/conformance/pdf_2_af.rb', line 141 def each_filespec(document) document.each_indirect_object do |container| fs = container.value[:Type] == :FileSpec ? container : nil if fs yield container, fs next end # Also pick up FileSpecs referenced from /AF arrays. af = container.value[:AF] next unless af af_value = af.is_a?(Pdfrb::Model::PdfArray) ? af.value : af entries = af_value.is_a?(::Array) ? af_value : [af_value] entries.each do |e| resolved = e.is_a?(Pdfrb::Model::Reference) ? document.object(e) : e next unless resolved && resolved.value[:Type] == :FileSpec yield container, resolved end end end |
.relationship_of(entry) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/pdfrb/conformance/pdf_2_af.rb', line 164 def relationship_of(entry) return nil unless entry value = entry.is_a?(Pdfrb::Model::Cos::Dictionary) ? entry.value : entry return nil unless value.is_a?(::Hash) # AssociatedFile dicts put the relationship on # /AFRelationship; FileSpecs put it on the same key when # they're AF-related. value[:AFRelationship] end |
.validate(document) ⇒ Object
119 120 121 |
# File 'lib/pdfrb/conformance/pdf_2_af.rb', line 119 def validate(document) RULESET.validate(document) end |