Class: Pdfrb::Document::Structure

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/document/structure.rb

Overview

Facade for authoring tagged PDF structure trees (PDF/UA §7.9, ISO 32000-2 §14.8).

A tagged PDF has:

* /StructTreeRoot on the Catalog — root of the structure hierarchy.
* /MarkInfo /Marked true on the Catalog — declares the PDF as tagged.
* /ParentTree on StructTreeRoot — maps marked-content BDC/EMC
references (via /MCID on BDC) back to their parent StructElem.

Structure elements form a tree: a document has sections, sections have headings/paragraphs/figures, etc. Each element has a /S (structure type — :H1, :P, :Figure, :Table, ...) and optional /T (title), /Alt (alternate text), /Lang (language).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Structure

Returns a new instance of Structure.



20
21
22
23
24
# File 'lib/pdfrb/document/structure.rb', line 20

def initialize(document)
  @document = document
  @elements = []
  @current_index = 0
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



18
19
20
# File 'lib/pdfrb/document/structure.rb', line 18

def document
  @document
end

#rootObject (readonly)

Returns the value of attribute root.



18
19
20
# File 'lib/pdfrb/document/structure.rb', line 18

def root
  @root
end

Instance Method Details

#add_child(parent, type, **attrs) ⇒ Pdfrb::Model::Cos::Dictionary

Add a child element to a parent element.

Parameters:

Returns:



77
78
79
80
81
82
# File 'lib/pdfrb/document/structure.rb', line 77

def add_child(parent, type, **attrs)
  elem = create_element(type, **attrs)
  append_child(parent, elem)
  @elements << elem
  elem
end

#add_element(type, page: nil, mcid: nil, **attrs) ⇒ Pdfrb::Model::Cos::Dictionary

Add a top-level child element to the structure tree.

Parameters:

  • type (Symbol)

    structure type (:Document, :Part, :H1, :P, ...).

  • attrs (Hash)

    additional attributes (:T, :Alt, :Lang, ...).

Returns:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/pdfrb/document/structure.rb', line 53

def add_element(type, page: nil, mcid: nil, **attrs)
  ensure_root
  elem = create_element(type, **attrs)
  append_child(@root, elem)

  if page
    page_ref = if page.is_a?(Pdfrb::Model::Reference)
                 page
               else
                 Pdfrb::Model::Reference.new(page.oid, page.gen)
               end
    elem.value[:Pg] = page_ref
  end
  elem.value[:K] = { MCID: mcid } if mcid

  @elements << elem
  elem
end

#build!Object

Build the structure tree, ParentTree, and set up /MarkInfo. Idempotent — safe to call multiple times.



234
235
236
237
238
239
# File 'lib/pdfrb/document/structure.rb', line 234

def build!
  enable! if @root.nil?
  return if @elements.empty?

  build_parent_tree
end

#each_element(&block) ⇒ Object

Walk all structure elements depth-first.



130
131
132
133
134
# File 'lib/pdfrb/document/structure.rb', line 130

def each_element(&block)
  return enum_for(:each_element) unless block

  @elements.each(&block)
end

#enable!Pdfrb::Model::Cos::Dictionary

Enable tagged PDF: create /StructTreeRoot, set /MarkInfo. Idempotent — returns the existing root if already enabled.

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/pdfrb/document/structure.rb', line 29

def enable!
  return @root if @root

  root_elem = document.add(
    { Type: :StructTreeRoot },
    type: Pdfrb::Model::Type::StructTreeRoot
  )
  catalog = document.catalog
  catalog.value[:StructTreeRoot] =
    Pdfrb::Model::Reference.new(root_elem.oid, root_elem.gen)
  mark_info = catalog.value[:MarkInfo]
  if mark_info.nil?
    mark_info = document.add({ Marked: true },
                             type: Pdfrb::Model::Cos::Dictionary)
    catalog.value[:MarkInfo] =
      Pdfrb::Model::Reference.new(mark_info.oid, mark_info.gen)
  end
  @root = root_elem
end

#has_alt_text?(element) ⇒ Boolean

Check if a structure element has alt text (required for /Figure).

Parameters:

Returns:

  • (Boolean)


110
111
112
113
# File 'lib/pdfrb/document/structure.rb', line 110

def has_alt_text?(element)
  alt = element.value[:Alt]
  alt && !alt.to_s.empty?
end

#map_role(custom, standard) ⇒ Object

Define a role mapping: maps a custom structure type name to a standard one. Required for PDF/UA when custom types are used.

Parameters:

  • custom (Symbol)

    the custom type name.

  • standard (Symbol)

    the standard type (:H1, :P, :Figure, ...).



140
141
142
143
144
145
146
147
148
149
# File 'lib/pdfrb/document/structure.rb', line 140

def map_role(custom, standard)
  ensure_root
  role_map = @root.value[:RoleMap]
  if role_map.nil?
    role_map = {}
    @root.value[:RoleMap] = role_map
  end
  role_map[custom] = standard
  role_map
end

#set_actual_text(element, text) ⇒ Object

Set actual text on a structure element. Overrides the visual text for screen readers (e.g., expanding abbreviations).

Parameters:



96
97
98
# File 'lib/pdfrb/document/structure.rb', line 96

def set_actual_text(element, text)
  element.value[:ActualText] = text.to_s
end

#set_alt_text(element, text) ⇒ Object

Set alternate text on a structure element for screen readers. Required for /Figure elements per PDF/UA-1 Tech Note 001.

Parameters:



88
89
90
# File 'lib/pdfrb/document/structure.rb', line 88

def set_alt_text(element, text)
  element.value[:Alt] = text.to_s
end

#set_language(element, lang) ⇒ Object

Set language on a structure element (overrides document /Lang).

Parameters:



103
104
105
# File 'lib/pdfrb/document/structure.rb', line 103

def set_language(element, lang)
  element.value[:Lang] = lang.to_s
end

#validate_alt_text!Array<Hash>

Validate that all /Figure elements have /Alt text per PDF/UA-1.

Returns:

  • (Array<Hash>)

    list of violations (element + reason).



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/pdfrb/document/structure.rb', line 117

def validate_alt_text!
  violations = []
  each_element do |elem|
    next unless elem.value[:S]&.to_sym == :Figure

    unless has_alt_text?(elem)
      violations << { element: elem, reason: "Figure missing /Alt text" }
    end
  end
  violations
end