Module: Pdfrb::Conformance::PdfA
- Defined in:
- lib/pdfrb/conformance/pdf_a.rb
Constant Summary collapse
- SHARED =
RuleSet.new("PDF/A-shared").tap do |rs| rs.register(Rule.new( id: "6.1-1", description: "Encryption prohibited", severity: :error, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { next nil unless doc.trailer && doc.trailer[:Encrypt] Violation.new(rule_id: "6.1-1", message: "PDF/A prohibits encryption", object: "trailer", severity: :error, spec_clause: "ISO 19005-1 6.1") } )) rs.register(Rule.new( id: "6.1-2", description: "XMP Metadata required", severity: :error, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { next nil if doc.catalog[:Metadata] Violation.new(rule_id: "6.1-2", message: "PDF/A requires /Catalog/Metadata XMP stream", object: "Catalog", severity: :error, spec_clause: "ISO 19005-1 6.1") } )) rs.register(Rule.new( id: "6.1-4", description: "JavaScript prohibited", severity: :error, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) if obj.value[:S] == :JavaScript return Violation.new(rule_id: "6.1-4", message: "PDF/A prohibits JavaScript actions", object: "JavaScript action", severity: :error, spec_clause: "ISO 19005-1 6.1") end end nil } )) rs.register(Rule.new( id: "6.1-7", description: "Language recommended", severity: :warning, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { next nil if doc.catalog[:Lang] Violation.new(rule_id: "6.1-7", message: "PDF/A recommends /Catalog/Lang", object: "Catalog", severity: :warning, spec_clause: "ISO 19005-1 6.1") } )) rs.register(Rule.new( id: "embedded-fonts", description: "All fonts must be embedded", severity: :error, spec_clause: "ISO 19005-1 6.2", check: ->(doc) { vs = [] doc.pages.each do |page| res = page.value[:Resources] next unless res fonts = res.is_a?(Pdfrb::Model::Cos::Dictionary) ? res.value[:Font] : res[:Font] next unless fonts fonts.each_value do |ref| f = ref.is_a?(Pdfrb::Model::Reference) ? doc.object(ref) : ref next unless f next if f[:FontDescriptor] vs << Violation.new(rule_id: "embedded-fonts", message: "Font /#{f[:BaseFont]} missing FontDescriptor", object: f[:BaseFont]&.to_s, severity: :error, spec_clause: "ISO 19005-1 6.2") end end vs.empty? ? nil : vs } )) end
- PREFLIGHT_DEEP =
Deep preflight rules common to all PDF/A levels. Each is a categorical ban: JavaScript, PS XObjects, non-A-3 embedded files, and unencoded text operators.
RuleSet.new("PDF/A-preflight").tap do |rs| rs.register(Rule.new( id: "preflight-1", description: "JavaScript is forbidden", severity: :error, spec_clause: "ISO 19005-1 6.6", check: ->(doc) { vs = [] catalog = doc.catalog names = catalog[:Names] names = doc.object(names) if names.is_a?(Pdfrb::Model::Reference) if names && (names[:JavaScript] || (names.is_a?(::Hash) && names[:JavaScript])) vs << Violation.new( rule_id: "preflight-1", message: "/Names /JavaScript is forbidden in PDF/A", object: "Catalog/Names/JavaScript", severity: :error, spec_clause: "ISO 19005-1 6.6" ) end if catalog[:AA] || catalog[:OpenAction] vs << Violation.new( rule_id: "preflight-1", message: "Additional actions (/AA) and /OpenAction with JavaScript are restricted in PDF/A", object: "Catalog", severity: :warning, spec_clause: "ISO 19005-1 6.6" ) end vs.empty? ? nil : vs } )) rs.register(Rule.new( id: "preflight-2", description: "PostScript XObjects are forbidden", severity: :error, spec_clause: "ISO 19005-1 6.2.3", check: ->(doc) { vs = [] doc.each_indirect_object do |obj| next unless obj.value[:Type] == :XObject next unless obj.value[:Subtype] == :PS vs << Violation.new( rule_id: "preflight-2", message: "PostScript XObjects are forbidden in PDF/A", object: "XObject", severity: :error, spec_clause: "ISO 19005-1 6.2.3" ) end vs.empty? ? nil : vs } )) rs.register(Rule.new( id: "preflight-3", description: "EmbeddedFile streams restricted", severity: :error, spec_clause: "ISO 19005-3 6.4", check: ->(_doc) { # Only PDF/A-3 allows EmbeddedFile. The rule # is permissive by default; the per-level # rule sets filter it down for A-1 and A-2. } )) rs.register(Rule.new( id: "preflight-4", description: "Encryption is forbidden", severity: :error, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { next nil unless doc.trailer && doc.trailer[:Encrypt] Violation.new( rule_id: "preflight-4", message: "Encryption is forbidden in PDF/A", object: "Trailer/Encrypt", severity: :error, spec_clause: "ISO 19005-1 6.1" ) } )) end
- PREFLIGHT_NO_EMBED =
Embeds files forbidden in PDF/A-1 and PDF/A-2 (only A-3 permits them).
RuleSet.new("PDF/A-no-embedded").tap do |rs| rs.register(Rule.new( id: "preflight-3-a1a2", description: "EmbeddedFile streams are forbidden in PDF/A-1 and A-2", severity: :error, spec_clause: "ISO 19005-1 6.4", check: ->(doc) { vs = [] doc.each_indirect_object do |obj| next unless obj.value[:Type] == :EmbeddedFile vs << Violation.new( rule_id: "preflight-3-a1a2", message: "EmbeddedFile streams are forbidden in PDF/A-1 and A-2", object: "EmbeddedFile", severity: :error, spec_clause: "ISO 19005-1 6.4" ) end vs.empty? ? nil : vs } )) end
- A1_SPECIFIC =
RuleSet.new("PDF/A-1-specific").tap do |rs| rs.register(Rule.new( id: "a1-1", description: "JPEG2000 not allowed in A-1", severity: :error, spec_clause: "ISO 19005-1 6.2.4", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Stream) next unless obj[:Filter] == :JPXDecode return Violation.new(rule_id: "a1-1", message: "JPEG2000 not allowed in PDF/A-1", object: "XObject", severity: :error, spec_clause: "ISO 19005-1 6.2.4") end nil } )) rs.register(Rule.new( id: "a1-2", description: "Object streams not allowed in A-1", severity: :error, spec_clause: "ISO 19005-1 6.2.4", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) next unless obj.value[:Type] == :ObjStm return Violation.new(rule_id: "a1-2", message: "Object streams not allowed in PDF/A-1", object: "ObjStm", severity: :error, spec_clause: "ISO 19005-1 6.2.4") end nil } )) rs.register(Rule.new( id: "a1-3", description: "PDF version must not exceed 1.4", severity: :error, spec_clause: "ISO 19005-1 6.1", check: ->(doc) { parts = doc.version.to_s.split(".") major = parts[0].to_i minor = (parts[1] || "0").to_i next nil if major < 1 || (major == 1 && minor <= 4) Violation.new(rule_id: "a1-3", message: "PDF/A-1 requires version <= 1.4, got #{doc.version}", object: "header", severity: :error, spec_clause: "ISO 19005-1 6.1") } )) rs.register(Rule.new( id: "a1-4", description: "LZWDecode not allowed in A-1", severity: :error, spec_clause: "ISO 19005-1 6.2.4", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Stream) f = obj[:Filter] fs = f.is_a?(::Array) ? f : [f] next unless fs.include?(:LZWDecode) return Violation.new(rule_id: "a1-4", message: "LZWDecode not allowed in PDF/A-1", object: "Stream", severity: :error, spec_clause: "ISO 19005-1 6.2.4") end nil } )) end
- A2_SPECIFIC =
RuleSet.new("PDF/A-2-specific").tap do |rs| rs.register(Rule.new( id: "a2-1", description: "PostScript XObjects prohibited", severity: :warning, spec_clause: "ISO 19005-2 6.2.4", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Stream) next unless [:PS, :PSXObject].include?(obj[:Subtype]) return Violation.new(rule_id: "a2-1", message: "PostScript XObjects not allowed in PDF/A-2", object: "XObject", severity: :warning, spec_clause: "ISO 19005-2 6.2.4") end nil } )) rs.register(Rule.new( id: "a2-2", description: "Embedded files not allowed in A-2", severity: :error, spec_clause: "ISO 19005-2 6.2", check: ->(doc) { doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) next unless obj.value[:Type] == :EmbeddedFile return Violation.new(rule_id: "a2-2", message: "Embedded files not allowed in PDF/A-2 (use PDF/A-3)", object: "EmbeddedFile", severity: :error, spec_clause: "ISO 19005-2 6.2") end nil } )) rs.register(Rule.new( id: "a2-3", description: "PDF 2.0 not allowed", severity: :error, spec_clause: "ISO 19005-2 6.1", check: ->(doc) { next nil unless doc.version.to_s.start_with?("2.") Violation.new(rule_id: "a2-3", message: "PDF/A-2 does not allow PDF 2.0, got #{doc.version}", object: "header", severity: :error, spec_clause: "ISO 19005-2 6.1") } )) end
- A3_SPECIFIC =
RuleSet.new("PDF/A-3-specific").tap do |rs| rs.register(Rule.new( id: "a3-1", description: "Filespecs must have AFRelationship", severity: :error, spec_clause: "ISO 19005-3 6.2", check: ->(doc) { vs = [] doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) next unless obj.value[:Type] == :Filespec next if obj.value[:AFRelationship] vs << Violation.new(rule_id: "a3-1", message: "Filespec missing /AFRelationship", object: "Filespec", severity: :error, spec_clause: "ISO 19005-3 6.2") end vs.empty? ? nil : vs } )) end
- A4_SPECIFIC =
RuleSet.new("PDF/A-4-specific").tap do |rs| rs.register(Rule.new( id: "a4-1", description: "PDF 2.0 required", severity: :error, spec_clause: "ISO 19005-4 6.1", check: ->(doc) { next nil if doc.version.to_s.start_with?("2.0") Violation.new(rule_id: "a4-1", message: "PDF/A-4 requires PDF 2.0, got #{doc.version}", object: "header", severity: :error, spec_clause: "ISO 19005-4 6.1") } )) rs.register(Rule.new( id: "a4-2", description: "Filespecs must have AFRelationship", severity: :error, spec_clause: "ISO 19005-4 6.2", check: ->(doc) { vs = [] doc.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) next unless obj.value[:Type] == :Filespec next if obj.value[:AFRelationship] vs << Violation.new(rule_id: "a4-2", message: "Filespec missing /AFRelationship", object: "Filespec", severity: :error, spec_clause: "ISO 19005-4 6.2") end vs.empty? ? nil : vs } )) rs.register(Rule.new( id: "a4-3", description: "Annotations need appearance streams", severity: :error, spec_clause: "ISO 19005-4 6.2.3", check: ->(doc) { vs = [] doc.each_indirect_object do |annot| next unless annot.is_a?(Pdfrb::Model::Cos::Dictionary) next unless annot.value[:Type] == :Annot st = annot.value[:Subtype] next if st == :Link r = annot.value[:Rect] next if r.is_a?(::Array) && r.length == 4 && r[0] == r[2] && r[1] == r[3] next if annot.value[:AP] vs << Violation.new(rule_id: "a4-3", message: "Annotation /#{st} missing /AP", object: "Annot", severity: :error, spec_clause: "ISO 19005-4 6.2.3") end vs.empty? ? nil : vs } )) rs.register(Rule.new( id: "a4-4", description: "Trailer /ID recommended", severity: :warning, spec_clause: "ISO 19005-4 6.1", check: ->(doc) { next nil if doc.trailer && doc.trailer[:ID] Violation.new(rule_id: "a4-4", message: "PDF/A-4 recommends /ID in trailer", object: "trailer", severity: :warning, spec_clause: "ISO 19005-4 6.1") } )) rs.register(Rule.new( id: "a4-5", description: "Metadata XMP recommended", severity: :warning, spec_clause: "ISO 19005-4 6.1", check: ->(doc) { next nil if doc.catalog[:Metadata] Violation.new(rule_id: "a4-5", message: "PDF/A-4 recommends /Catalog/Metadata XMP stream", object: "Catalog", severity: :warning, spec_clause: "ISO 19005-4 6.1") } )) end
- A1 =
RuleSet.new("PDF/A-1").tap do |rs| SHARED.rules.each { |r| rs.register(r) } A1_SPECIFIC.rules.each { |r| rs.register(r) } PREFLIGHT_DEEP.rules.each { |r| rs.register(r) } PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) } end
- A2 =
RuleSet.new("PDF/A-2").tap do |rs| SHARED.rules.each { |r| rs.register(r) } A2_SPECIFIC.rules.each { |r| rs.register(r) } PREFLIGHT_DEEP.rules.each { |r| rs.register(r) } PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) } end
- A3 =
RuleSet.new("PDF/A-3").tap do |rs| SHARED.rules.each { |r| rs.register(r) } A3_SPECIFIC.rules.each { |r| rs.register(r) } PREFLIGHT_DEEP.rules.each { |r| rs.register(r) } end
- A4 =
RuleSet.new("PDF/A-4").tap do |rs| SHARED.rules.each { |r| rs.register(r) } A4_SPECIFIC.rules.each { |r| rs.register(r) } PREFLIGHT_DEEP.rules.each { |r| rs.register(r) } PREFLIGHT_NO_EMBED.rules.each { |r| rs.register(r) } end
- LEVEL_RULESETS =
{ a1b: A1, a1a: A1, a2b: A2, a2a: A2, a3b: A3, a3a: A3, a4: A4 }.freeze
Class Method Summary collapse
Class Method Details
.profiles ⇒ Object
445 446 447 448 |
# File 'lib/pdfrb/conformance/pdf_a.rb', line 445 def profiles { a1b: A1, a1a: A1, a2b: A2, a2a: A2, a3b: A3, a3a: A3, a4: A4 } end |
.validate(document, level: :a2b) ⇒ Object
450 451 452 453 |
# File 'lib/pdfrb/conformance/pdf_a.rb', line 450 def validate(document, level: :a2b) rs = LEVEL_RULESETS[level] || SHARED rs.validate(document) end |