Module: Pdfrb::Conformance::TaggedPdf
- Defined in:
- lib/pdfrb/conformance/tagged_pdf.rb
Overview
Tagged PDF validation per ISO 32000-1 §14.8 (and ISO 32000-2 §14.8). Tagged PDF is the foundation of PDF/UA and PDF/A-2a/3a; this rule set checks the structural requirements that any tagged PDF must satisfy, regardless of higher-level profile.
This is intentionally a subset of PDF/UA. Use PdfUA for the full accessibility profile; use TaggedPdf for a baseline structural pass that doesn't require accessibility annotations (alt text, reading order).
Constant Summary collapse
- STANDARD_STRUCTURE_TYPES =
%i[ Document Part Div Art Sect BlockQuote Caption TOC TOCI Index NonStruct Private H H1 H2 H3 H4 H5 H6 P L LI Lbl LBody Table TR TH TD THead TBody TFoot Caption Span Quote Note Reference BibEntry Code Figure Formula Form ].freeze
- RULESET =
RuleSet.new("TaggedPDF").tap do |rs| rs.register(Rule.new( id: "tag-1", description: "/MarkInfo /Marked must be true", severity: :error, spec_clause: "ISO 32000-2 14.8.1", check: ->(doc) { mark_info = doc.catalog[:MarkInfo] mark_info = doc.object(mark_info) if mark_info.is_a?(Pdfrb::Model::Reference) next nil if mark_info && mark_info[:Marked] == true Violation.new( rule_id: "tag-1", message: "/MarkInfo /Marked true is required for tagged PDF", object: "Catalog", severity: :error, spec_clause: "ISO 32000-2 14.8.1" ) } )) rs.register(Rule.new( id: "tag-2", description: "/StructTreeRoot must be present", severity: :error, spec_clause: "ISO 32000-2 14.7.2", check: ->(doc) { next nil if doc.catalog[:StructTreeRoot] Violation.new( rule_id: "tag-2", message: "/StructTreeRoot required for tagged PDF", object: "Catalog", severity: :error, spec_clause: "ISO 32000-2 14.7.2" ) } )) rs.register(Rule.new( id: "tag-3", description: "Structure elements must use standard types", severity: :warning, spec_clause: "ISO 32000-2 14.8.4", check: ->(doc) { violations = [] walk_structure(doc) do |elem| s = elem[:S] next if s.nil? || STANDARD_STRUCTURE_TYPES.include?(s.to_sym) violations << Violation.new( rule_id: "tag-3", message: "Non-standard structure type '#{s}' on element", object: "StructElem", severity: :warning, spec_clause: "ISO 32000-2 14.8.4" ) end violations } )) rs.register(Rule.new( id: "tag-4", description: "Figure elements should have /Alt or /ActualText", severity: :warning, spec_clause: "ISO 32000-2 14.9.2", check: ->(doc) { violations = [] walk_structure(doc) do |elem| next unless elem[:S] == :Figure next if elem[:Alt] || elem[:ActualText] violations << Violation.new( rule_id: "tag-4", message: "Figure without /Alt or /ActualText", object: "StructElem/Figure", severity: :warning, spec_clause: "ISO 32000-2 14.9.2" ) end violations } )) rs.register(Rule.new( id: "tag-5", description: "Table structure should include rows", severity: :warning, spec_clause: "ISO 32000-2 14.8.5", check: ->(doc) { violations = [] walk_structure(doc) do |elem| next unless elem[:S] == :Table kids = structure_kids(elem, doc) has_tr = kids.any? { |k| k[:S] == :TR } next if has_tr violations << Violation.new( rule_id: "tag-5", message: "Table has no TR child rows", object: "StructElem/Table", severity: :warning, spec_clause: "ISO 32000-2 14.8.5" ) end violations } )) end
Class Method Summary collapse
- .structure_kids(element, document) ⇒ Object
- .validate(document) ⇒ Object
-
.walk_structure(document) ⇒ Object
Walk the structure tree starting at /StructTreeRoot, yielding each element dict.
Class Method Details
.structure_kids(element, document) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/pdfrb/conformance/tagged_pdf.rb', line 164 def structure_kids(element, document) kids = element[:K] return [] unless kids kids = kids.value if kids.is_a?(Pdfrb::Model::PdfArray) kids = [kids] unless kids.is_a?(::Array) kids.filter_map do |k| next nil unless k.is_a?(Pdfrb::Model::Reference) obj = document.object(k) next nil unless obj.is_a?(Pdfrb::Model::Cos::Dictionary) obj end end |
.validate(document) ⇒ Object
137 138 139 |
# File 'lib/pdfrb/conformance/tagged_pdf.rb', line 137 def validate(document) RULESET.validate(document) end |
.walk_structure(document) ⇒ Object
Walk the structure tree starting at /StructTreeRoot, yielding each element dict. Silently skips cycles and missing refs.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/pdfrb/conformance/tagged_pdf.rb', line 143 def walk_structure(document) seen = ::Set.new root_ref = document.catalog[:StructTreeRoot] return unless root_ref root = root_ref.is_a?(Pdfrb::Model::Reference) ? document.object(root_ref) : root_ref return unless root first_kids = structure_kids(root, document) queue = first_kids.dup until queue.empty? elem = queue.shift next unless elem next if seen.include?(elem.object_id) seen << elem.object_id yield elem queue.concat(structure_kids(elem, document)) end end |