prawn-accessibility 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 455b571866fccd0796f13c09d4996994e802d7acdb995c8ad190f0f7a4af0638
4
- data.tar.gz: e6f77dd230dac3867dfaceb4ab5333f9b2bdaac67fd438d58281cc3fb6ee34b5
3
+ metadata.gz: 23e35addd032e68085d39678ec492b25122be0f739864f464cf26520da63ba58
4
+ data.tar.gz: 69de3798b80e4a16d8dcee828c18f29368e95afab0fb1706e3bae9d9ae5c9754
5
5
  SHA512:
6
- metadata.gz: aaf6ec947a42cc40e683192f7d7f53a0ad17097d9078338e2fde56e9037d6a560032994860dd83f1395c3b0a9efc0e45b945f7cd3ba47a501a5992d3878fe4ab
7
- data.tar.gz: 4e485adc403ff74434fd96efa854f8457892cd38788afe15c7883c36b00703cafbb14aab9d132c7985f1b0c01d79859be20565ee2a0e9d0747ef8efa774622f8
6
+ metadata.gz: 9229dd71238f4bd062aa89b09e5fa6f7747b4bd517932fff773b89270f89d7148c2232de4e89a6d0c399c8cb8357031183c83f701af4f0b728a66d196def3dae
7
+ data.tar.gz: b7671f5e4bb5d111a23715b8687066f83b8b85582e93336c9d63cb475c1619170475029f6964ec18bc4920e3b4220b34a4a4051390be5985616929c2310f3add
data/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project are documented here. This project adheres
4
4
  to [Semantic Versioning](https://semver.org).
5
5
 
6
+ ## [1.1.1] - 2026-07-06
7
+
8
+ - Simplify the gem description. Metadata only — no code changes.
9
+
10
+ ## [1.1.0] - 2026-07-05
11
+
12
+ - **Opt in with `tagged: true`.** Documents are tagged only when created with
13
+ `Prawn::Document.new(tagged: true)`, matching the `tagged?` query and standard
14
+ "tagged PDF" terminology. The `marked:` option is removed (no alias).
15
+ - Reduced the patching surface: **no `pdf-core` patches**. `MarkedContent` and
16
+ `StructureTree` now live under `Prawn::Accessibility` and use only the public
17
+ renderer API; the structure tree is owned by the document and finalized via
18
+ the existing `before_render` hook. The high-level API is mixed into
19
+ `Prawn::Document` with `include`.
20
+ - Untagged output is unchanged (byte-for-byte stock Prawn). Header cells are
21
+ flagged at construction, so `Cell#header?` is consistent before and after draw.
22
+ - Removed the low-level `@api private` symbols `renderer.marked?`,
23
+ `renderer.structure_tree`, `PDF::Core::MarkedContent`,
24
+ `PDF::Core::StructureTree`, and `PDF::Core::ObjectStore#marked?`.
25
+
6
26
  ## [1.0.0] - 2026-07-05
7
27
 
8
28
  Initial release.
@@ -21,7 +41,6 @@ Initial release.
21
41
  with header detection and `/Scope`. Activated automatically when
22
42
  `prawn-table` is present; not a required dependency.
23
43
 
24
- Repackaged from the stalled upstream PRs prawnpdf/pdf-core#67,
25
- prawnpdf/prawn#1391, and prawnpdf/prawn-table#164.
26
-
44
+ [1.1.1]: https://github.com/mes-amis/prawn-accessibility/releases/tag/v1.1.1
45
+ [1.1.0]: https://github.com/mes-amis/prawn-accessibility/releases/tag/v1.1.0
27
46
  [1.0.0]: https://github.com/mes-amis/prawn-accessibility/releases/tag/v1.0.0
data/README.md CHANGED
@@ -9,9 +9,12 @@ headings, paragraphs, figures, tables, and decorative artifacts — so Prawn can
9
9
  produce accessible, tagged PDFs that screen readers can navigate.
10
10
 
11
11
  It layers on top of the **published** `prawn`, `pdf-core`, and (optionally)
12
- `prawn-table` gems using `prepend` and additive re-opens — it does **not** fork
13
- them. When you don't opt in (`marked: true`), output is byte-for-byte identical
14
- to stock Prawn.
12
+ `prawn-table` gems — it does **not** fork them. The document API is mixed into
13
+ `Prawn::Document`; only a couple of small method wrappers remain (and none in
14
+ `pdf-core`).
15
+
16
+ Tagging is opt-in: create a document with `tagged: true`. Without it, output is
17
+ byte-for-byte identical to stock Prawn.
15
18
 
16
19
  ## Installation
17
20
 
@@ -33,7 +36,8 @@ gem 'prawn-table' # optional; enables <Table>/<TR>/<TH>/<TD> tagging
33
36
  ```ruby
34
37
  require 'prawn-accessibility'
35
38
 
36
- pdf = Prawn::Document.new(marked: true, language: 'en-US')
39
+ # Opt in with tagged: true; set language: for the document's /Lang.
40
+ pdf = Prawn::Document.new(tagged: true, language: 'en-US')
37
41
 
38
42
  pdf.heading(1, 'Annual Report') # <H1>
39
43
  pdf.paragraph('Body text that a screen reader will read aloud.') # <P>
@@ -43,12 +47,14 @@ pdf.figure(alt_text: 'Company logo') { pdf.image('logo.png') } # <Figure> wit
43
47
 
44
48
  pdf.artifact(type: :Pagination) { pdf.text('Page 1') } # decorative, not read
45
49
 
46
- # Tables auto-tag when the document is tagged:
50
+ # Tables auto-tag while the document is tagged:
47
51
  pdf.table([['Name', 'Age'], ['Alice', '30']], header: true) # <Table>/<TR>/<TH>/<TD>
48
52
 
49
53
  pdf.render_file('report.pdf')
50
54
  ```
51
55
 
56
+ A document created without `tagged: true` renders a plain, untagged PDF.
57
+
52
58
  ### API
53
59
 
54
60
  | Method | Emits |
@@ -1,35 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'prawn'
4
- require_relative 'pdf_core'
4
+ require_relative 'structure_tree'
5
5
 
6
6
  module Prawn
7
7
  module Accessibility
8
- # Instance methods mixed into {Prawn::Document} to provide the high-level
9
- # tagged-PDF (accessibility) API. When a document is created with
10
- # <tt>marked: true</tt>, content can be wrapped in structure elements that
11
- # screen readers and assistive technologies use to navigate the document.
8
+ # Instance methods included into {Prawn::Document} to provide the
9
+ # high-level tagged-PDF (accessibility) API.
12
10
  #
13
- # @example
14
- # pdf = Prawn::Document.new(marked: true, language: 'en-US')
15
- #
16
- # pdf.structure(:H1) do
17
- # pdf.text 'Document Title'
18
- # end
19
- #
20
- # pdf.structure(:P) do
21
- # pdf.text 'Body paragraph text.'
22
- # end
11
+ # Tagging is opt-in: create the document with <tt>tagged: true</tt> (see the
12
+ # initializer shim at the bottom of this file). Everything here is built on
13
+ # Prawn/pdf-core's *public* API (`renderer`, `state`, `min_version`,
14
+ # `before_render`); no core class is patched to support it.
23
15
  #
24
- # pdf.artifact do
25
- # pdf.text 'Page 1' # not read by screen readers
26
- # end
16
+ # @example
17
+ # pdf = Prawn::Document.new(tagged: true, language: 'en-US')
18
+ # pdf.structure(:H1) { pdf.text 'Document Title' }
19
+ # pdf.structure(:P) { pdf.text 'Body paragraph text.' }
20
+ # pdf.artifact { pdf.text 'Page 1' } # not read by screen readers
27
21
  module DocumentExtensions
22
+ # The structure tree for this document, or nil if not tagged.
23
+ #
24
+ # @return [Prawn::Accessibility::StructureTree, nil]
25
+ def structure_tree
26
+ @accessibility_structure_tree
27
+ end
28
+
28
29
  # Whether this document is tagged for accessibility.
29
30
  #
30
31
  # @return [Boolean]
31
32
  def tagged?
32
- renderer.marked?
33
+ !@accessibility_structure_tree.nil?
33
34
  end
34
35
 
35
36
  # Wrap content in a structure element. The block's content will be
@@ -52,7 +53,7 @@ module Prawn
52
53
  def structure(tag, attributes = {}, &block)
53
54
  return yield if !tagged? || !block
54
55
 
55
- tree = renderer.structure_tree
56
+ tree = structure_tree
56
57
  tree.begin_element(tag, attributes)
57
58
  tree.mark_content(tag, &block)
58
59
  tree.end_element
@@ -69,7 +70,7 @@ module Prawn
69
70
  def structure_container(tag, attributes = {}, &block)
70
71
  return yield if !tagged? || !block
71
72
 
72
- tree = renderer.structure_tree
73
+ tree = structure_tree
73
74
  tree.begin_element(tag, attributes)
74
75
  yield
75
76
  tree.end_element
@@ -85,7 +86,7 @@ module Prawn
85
86
  def artifact(type: nil, &block)
86
87
  return yield if !tagged? || !block
87
88
 
88
- renderer.structure_tree.mark_artifact(artifact_type: type, &block)
89
+ structure_tree.mark_artifact(artifact_type: type, &block)
89
90
  end
90
91
 
91
92
  # Render a heading at the specified level.
@@ -136,18 +137,56 @@ module Prawn
136
137
  end
137
138
  end
138
139
  end
139
- end
140
- end
141
140
 
142
- # --- Patches applied to Prawn::Document -------------------------------------
141
+ # Prepended onto {Prawn::Document#initialize} to support the
142
+ # <tt>Prawn::Document.new(tagged: true, language: 'en-US')</tt> API.
143
+ # Tagging is opt-in: a document is tagged only when <tt>tagged: true</tt> is
144
+ # passed.
145
+ #
146
+ # It strips the accessibility options before delegating to the original
147
+ # initializer (so no change to +VALID_OPTIONS+ is needed), then wires up
148
+ # tagging and runs the user block — in that order, so the block runs with
149
+ # tagging already active.
150
+ #
151
+ # @api private
152
+ module OptionInitializer
153
+ def initialize(options = {}, &block)
154
+ opts = options.dup
155
+ tagged = opts.delete(:tagged)
156
+ language = opts.delete(:language)
157
+
158
+ # Delegate to the original initializer WITHOUT the block, so we can run
159
+ # it ourselves after tagging is wired up.
160
+ super(opts)
143
161
 
144
- # Accept the accessibility options in Prawn::Document.new. VALID_OPTIONS is a
145
- # frozen constant; redefine it (remove + set avoids the redefinition warning).
146
- original_valid_options = Prawn::Document::VALID_OPTIONS
147
- Prawn::Document.send(:remove_const, :VALID_OPTIONS)
148
- Prawn::Document.const_set(
149
- :VALID_OPTIONS,
150
- (original_valid_options + %i[marked language]).freeze,
151
- )
162
+ install_accessibility(language) if tagged
163
+
164
+ return unless block
165
+
166
+ block.arity < 1 ? instance_eval(&block) : block[self]
167
+ end
168
+
169
+ private
170
+
171
+ # Set up tagged-PDF output: mark the catalog, create the structure tree,
172
+ # bump the PDF version to 1.7, and finalize the tree at render time via
173
+ # the renderer's public `before_render` hook.
174
+ #
175
+ # @param language [String, nil] optional document language (BCP 47 tag)
176
+ # @return [void]
177
+ def install_accessibility(language)
178
+ catalog = state.store.root.data
179
+ catalog[:MarkInfo] = { Marked: true }
180
+ catalog[:Lang] = language if language
181
+
182
+ @accessibility_structure_tree = Prawn::Accessibility::StructureTree.new(renderer)
183
+ renderer.min_version(1.7)
184
+ tree = @accessibility_structure_tree
185
+ renderer.before_render { |_state| tree.finalize! }
186
+ end
187
+ end
188
+ end
189
+ end
152
190
 
153
191
  Prawn::Document.include(Prawn::Accessibility::DocumentExtensions)
192
+ Prawn::Document.prepend(Prawn::Accessibility::OptionInitializer)
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module PDF
4
- module Core
5
- # Provides methods for emitting marked content operators (BMC/BDC/EMC)
6
- # in PDF content streams. These operators associate content with structure
7
- # elements for accessibility (tagged PDF).
3
+ module Prawn
4
+ module Accessibility
5
+ # Emits marked content operators (BMC/BDC/EMC) into a PDF content stream.
6
+ # These operators associate content with structure elements for
7
+ # accessibility (tagged PDF).
8
+ #
9
+ # This is a mixin: the including object must respond to `add_content(str)`
10
+ # (as {PDF::Core::Renderer} does). It therefore needs no changes to any
11
+ # pdf-core class — {StructureTree} includes it and forwards `add_content`
12
+ # to the renderer.
8
13
  #
9
14
  # @api private
10
15
  module MarkedContent
@@ -1,17 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module PDF
4
- module Core
3
+ require_relative 'marked_content'
4
+
5
+ module Prawn
6
+ module Accessibility
5
7
  # Manages the PDF structure tree for tagged/accessible PDFs.
6
8
  #
7
9
  # The structure tree provides the logical structure of a document,
8
10
  # mapping marked content sequences in page content streams to
9
11
  # structure elements (headings, paragraphs, tables, etc.).
10
12
  #
13
+ # It talks to the document exclusively through the *public* renderer API
14
+ # (`ref!`, `deref`, `add_content`, `state`), so it requires no patches to
15
+ # any pdf-core class.
16
+ #
11
17
  # PDF spec references: Section 14.7 (Logical Structure)
12
18
  #
13
19
  # @api private
14
20
  class StructureTree
21
+ include MarkedContent
22
+
15
23
  # @return [PDF::Core::Renderer] owning renderer
16
24
  attr_reader :renderer
17
25
 
@@ -148,10 +156,10 @@ module PDF
148
156
  # Track which struct element owns this MCID on this page
149
157
  @parent_tree_map[page_struct_parents][mcid] = elem_ref
150
158
 
151
- # Emit BDC/EMC in content stream
152
- renderer.begin_marked_content_with_properties(tag, { MCID: mcid })
159
+ # Emit BDC/EMC in content stream (via the MarkedContent mixin)
160
+ begin_marked_content_with_properties(tag, { MCID: mcid })
153
161
  yield if block_given?
154
- renderer.end_marked_content
162
+ end_marked_content
155
163
  end
156
164
 
157
165
  # Mark content as an artifact (decorative, not read by screen readers).
@@ -162,14 +170,12 @@ module PDF
162
170
  # @return [void]
163
171
  def mark_artifact(artifact_type: nil)
164
172
  if artifact_type
165
- renderer.begin_marked_content_with_properties(
166
- :Artifact, { Type: artifact_type },
167
- )
173
+ begin_marked_content_with_properties(:Artifact, { Type: artifact_type })
168
174
  else
169
- renderer.begin_marked_content(:Artifact)
175
+ begin_marked_content(:Artifact)
170
176
  end
171
177
  yield if block_given?
172
- renderer.end_marked_content
178
+ end_marked_content
173
179
  end
174
180
 
175
181
  # Finalize the structure tree before rendering. Called via
@@ -190,6 +196,15 @@ module PDF
190
196
 
191
197
  private
192
198
 
199
+ # Forward content emission (used by the MarkedContent mixin) to the
200
+ # renderer's public API.
201
+ #
202
+ # @param str [String]
203
+ # @return [void]
204
+ def add_content(str)
205
+ renderer.add_content(str)
206
+ end
207
+
193
208
  # The current open structure element, or nil if none.
194
209
  #
195
210
  # @return [PDF::Core::Reference, nil]
@@ -3,6 +3,17 @@
3
3
  # Table accessibility patches. This file is only loaded when Prawn::Table is
4
4
  # already defined (see prawn/accessibility.rb), so requiring prawn-accessibility
5
5
  # without prawn-table installed is safe.
6
+ #
7
+ # prawn-table hard-instantiates Prawn::Table and offers no hook around cell
8
+ # drawing, so these three `prepend`s are the irreducible patching until upstream
9
+ # grows render hooks:
10
+ #
11
+ # 1. Prawn::Table#initialize — flag header cells (at construction)
12
+ # 2. Prawn::Table#draw — wrap the whole table in <Table>
13
+ # 3. Prawn::Table::Cell.draw_cells — wrap each row/cell in <TR>/<TH>/<TD>
14
+ #
15
+ # #draw and .draw_cells delegate to `super` in the untagged path, so untagged
16
+ # tables are unchanged. Everything else here is additive (new methods only).
6
17
 
7
18
  unless defined?(Prawn::Table)
8
19
  raise 'prawn/accessibility/table requires prawn-table to be loaded first'
@@ -12,12 +23,14 @@ module Prawn
12
23
  module Accessibility
13
24
  # Patches applied to the published +prawn-table+ gem so that tables emit
14
25
  # +<Table>+/+<TR>+/+<TH>+/+<TD>+ structure elements when the owning document
15
- # is tagged. Untagged tables render exactly as upstream (via +super+).
26
+ # is tagged.
16
27
  #
17
28
  # @api private
18
29
  module TablePatch
19
- # Prepended onto {Prawn::Table#initialize} to flag header cells after
20
- # the table has been built and positioned.
30
+ # Prepended onto {Prawn::Table}. Flags header cells at construction (so
31
+ # +Cell#header?+ is consistent whether or not the table has been drawn),
32
+ # and wraps drawing in a +<Table>+ structure element when the document is
33
+ # tagged.
21
34
  #
22
35
  # @api private
23
36
  module TableInstancePatch
@@ -26,8 +39,6 @@ module Prawn
26
39
  mark_header_cells
27
40
  end
28
41
 
29
- # Draws the table, wrapping it in a +<Table>+ structure element when
30
- # the document is tagged.
31
42
  def draw
32
43
  if @pdf.respond_to?(:tagged?) && @pdf.tagged?
33
44
  @pdf.structure_container(:Table) { super() }
@@ -3,6 +3,6 @@
3
3
  module Prawn
4
4
  module Accessibility
5
5
  # Gem version. Follows Semantic Versioning (https://semver.org).
6
- VERSION = '1.0.0'
6
+ VERSION = '1.1.1'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prawn-accessibility
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mes Amis
@@ -42,11 +42,6 @@ description: |
42
42
  pdf-core, and (optionally) prawn-table gems. It adds a high-level API for
43
43
  marking document structure — headings, paragraphs, figures, tables, and
44
44
  artifacts — so Prawn can produce Section 508 / WCAG accessible PDFs.
45
-
46
- The behavior originates from the stalled upstream pull requests
47
- prawnpdf/pdf-core#67, prawnpdf/prawn#1391, and prawnpdf/prawn-table#164,
48
- repackaged here as a standalone gem that patches the released libraries via
49
- `prepend`/additive re-opens rather than forking them.
50
45
  email:
51
46
  - craig@monami.io
52
47
  executables: []
@@ -63,7 +58,6 @@ files:
63
58
  - lib/prawn/accessibility.rb
64
59
  - lib/prawn/accessibility/document.rb
65
60
  - lib/prawn/accessibility/marked_content.rb
66
- - lib/prawn/accessibility/pdf_core.rb
67
61
  - lib/prawn/accessibility/structure_tree.rb
68
62
  - lib/prawn/accessibility/table.rb
69
63
  - lib/prawn/accessibility/version.rb
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pdf/core'
4
- require_relative 'marked_content'
5
- require_relative 'structure_tree'
6
-
7
- module Prawn
8
- module Accessibility
9
- # Patches applied to the published +pdf-core+ gem to add tagged-PDF
10
- # support. All patches are additive or +prepend+-based: the original
11
- # (untagged) behavior is preserved and only diverges when a document is
12
- # created with <tt>marked: true</tt>.
13
- #
14
- # @api private
15
- module PDFCore
16
- # Prepended onto {PDF::Core::DocumentState#initialize}. Threads the
17
- # +:marked+ and +:language+ options through to the object store's
18
- # catalog after the original initializer has built the store.
19
- #
20
- # @api private
21
- module DocumentStatePatch
22
- def initialize(options)
23
- super
24
-
25
- @store.root.data[:MarkInfo] = { Marked: true } if options[:marked]
26
- @store.root.data[:Lang] = options[:language] if options[:language]
27
- end
28
- end
29
-
30
- # Prepended onto {PDF::Core::Renderer#initialize}. When the document is
31
- # marked, wires up a {PDF::Core::StructureTree}, schedules its
32
- # finalization before render, and bumps the PDF version to 1.7.
33
- #
34
- # @api private
35
- module RendererPatch
36
- def initialize(state)
37
- super
38
-
39
- return unless state.store.marked?
40
-
41
- @structure_tree = PDF::Core::StructureTree.new(self)
42
- before_render { |_doc_state| @structure_tree.finalize! }
43
- min_version(1.7)
44
- end
45
-
46
- # The structure tree for this document, or nil if not tagged.
47
- #
48
- # @return [PDF::Core::StructureTree, nil]
49
- attr_reader :structure_tree
50
-
51
- # Whether this document is marked (tagged for accessibility).
52
- #
53
- # @return [Boolean]
54
- def marked?
55
- state.store.marked?
56
- end
57
- end
58
- end
59
- end
60
- end
61
-
62
- # --- Additive re-opens ------------------------------------------------------
63
-
64
- module PDF
65
- module Core
66
- class ObjectStore
67
- # Whether this document is marked (tagged for accessibility).
68
- #
69
- # @return [Boolean]
70
- def marked?
71
- root.data.key?(:MarkInfo) && root.data[:MarkInfo][:Marked] == true
72
- end
73
- end
74
- end
75
- end
76
-
77
- PDF::Core::Renderer.include(PDF::Core::MarkedContent)
78
- PDF::Core::DocumentState.prepend(Prawn::Accessibility::PDFCore::DocumentStatePatch)
79
- PDF::Core::Renderer.prepend(Prawn::Accessibility::PDFCore::RendererPatch)