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

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