hexapdf 1.6.0 → 1.8.0

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.
Files changed (287) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/LICENSE +1 -1
  4. data/examples/005-merging.rb +2 -1
  5. data/examples/032-acro_form_list_and_fill.rb +47 -0
  6. data/examples/033-text_extraction.rb +34 -0
  7. data/lib/hexapdf/cli/batch.rb +1 -1
  8. data/lib/hexapdf/cli/command.rb +1 -1
  9. data/lib/hexapdf/cli/debug_info.rb +1 -1
  10. data/lib/hexapdf/cli/files.rb +1 -1
  11. data/lib/hexapdf/cli/fonts.rb +6 -4
  12. data/lib/hexapdf/cli/form.rb +1 -1
  13. data/lib/hexapdf/cli/image2pdf.rb +1 -1
  14. data/lib/hexapdf/cli/images.rb +17 -17
  15. data/lib/hexapdf/cli/info.rb +3 -1
  16. data/lib/hexapdf/cli/inspect.rb +1 -1
  17. data/lib/hexapdf/cli/merge.rb +14 -2
  18. data/lib/hexapdf/cli/modify.rb +1 -1
  19. data/lib/hexapdf/cli/optimize.rb +1 -1
  20. data/lib/hexapdf/cli/split.rb +1 -1
  21. data/lib/hexapdf/cli/usage.rb +1 -1
  22. data/lib/hexapdf/cli/watermark.rb +1 -1
  23. data/lib/hexapdf/cli.rb +1 -1
  24. data/lib/hexapdf/composer.rb +1 -1
  25. data/lib/hexapdf/configuration.rb +10 -1
  26. data/lib/hexapdf/content/canvas.rb +2 -2
  27. data/lib/hexapdf/content/canvas_composer.rb +1 -1
  28. data/lib/hexapdf/content/color_space.rb +1 -1
  29. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  30. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  31. data/lib/hexapdf/content/graphic_object/geom2d.rb +1 -1
  32. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  33. data/lib/hexapdf/content/graphic_object.rb +1 -1
  34. data/lib/hexapdf/content/graphics_state.rb +1 -1
  35. data/lib/hexapdf/content/operator.rb +1 -1
  36. data/lib/hexapdf/content/parser.rb +1 -1
  37. data/lib/hexapdf/content/processor.rb +1 -1
  38. data/lib/hexapdf/content/smart_text_extractor.rb +311 -0
  39. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  40. data/lib/hexapdf/content.rb +3 -1
  41. data/lib/hexapdf/data_dir.rb +1 -1
  42. data/lib/hexapdf/dictionary.rb +1 -1
  43. data/lib/hexapdf/dictionary_fields.rb +1 -1
  44. data/lib/hexapdf/digital_signature/cms_handler.rb +1 -1
  45. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  46. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +1 -1
  47. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  48. data/lib/hexapdf/digital_signature/signatures.rb +1 -1
  49. data/lib/hexapdf/digital_signature/signing/default_handler.rb +2 -16
  50. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +22 -9
  51. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +1 -1
  52. data/lib/hexapdf/digital_signature/signing.rb +1 -1
  53. data/lib/hexapdf/digital_signature/verification_result.rb +1 -1
  54. data/lib/hexapdf/digital_signature.rb +1 -1
  55. data/lib/hexapdf/document/annotations.rb +1 -1
  56. data/lib/hexapdf/document/destinations.rb +1 -1
  57. data/lib/hexapdf/document/files.rb +1 -1
  58. data/lib/hexapdf/document/fonts.rb +1 -1
  59. data/lib/hexapdf/document/images.rb +1 -1
  60. data/lib/hexapdf/document/layout.rb +1 -1
  61. data/lib/hexapdf/document/metadata.rb +1 -1
  62. data/lib/hexapdf/document/pages.rb +1 -1
  63. data/lib/hexapdf/document.rb +8 -4
  64. data/lib/hexapdf/encryption/aes.rb +1 -1
  65. data/lib/hexapdf/encryption/arc4.rb +1 -1
  66. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  67. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  68. data/lib/hexapdf/encryption/identity.rb +1 -1
  69. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  70. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  71. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  72. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  73. data/lib/hexapdf/encryption.rb +1 -1
  74. data/lib/hexapdf/error.rb +1 -1
  75. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  76. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  77. data/lib/hexapdf/filter/brotli_decode.rb +88 -0
  78. data/lib/hexapdf/filter/crypt.rb +1 -1
  79. data/lib/hexapdf/filter/encryption.rb +1 -1
  80. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  81. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  82. data/lib/hexapdf/filter/pass_through.rb +1 -1
  83. data/lib/hexapdf/filter/predictor.rb +1 -1
  84. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  85. data/lib/hexapdf/filter.rb +2 -1
  86. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  87. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  88. data/lib/hexapdf/font/cmap.rb +1 -1
  89. data/lib/hexapdf/font/encoding/base.rb +1 -1
  90. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  91. data/lib/hexapdf/font/encoding/glyph_list.rb +1 -1
  92. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  93. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  94. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  95. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  96. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  97. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  98. data/lib/hexapdf/font/encoding.rb +1 -1
  99. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  100. data/lib/hexapdf/font/true_type/builder.rb +2 -2
  101. data/lib/hexapdf/font/true_type/font.rb +14 -1
  102. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  103. data/lib/hexapdf/font/true_type/subsetter.rb +11 -6
  104. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  105. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  106. data/lib/hexapdf/font/true_type/table/directory.rb +6 -1
  107. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  108. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  109. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  110. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  111. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  112. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  113. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  114. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  115. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  116. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  117. data/lib/hexapdf/font/true_type/table.rb +1 -1
  118. data/lib/hexapdf/font/true_type.rb +2 -1
  119. data/lib/hexapdf/font/true_type_wrapper.rb +3 -3
  120. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  121. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  122. data/lib/hexapdf/font/type1/font.rb +1 -1
  123. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  124. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  125. data/lib/hexapdf/font/type1.rb +1 -1
  126. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  127. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  128. data/lib/hexapdf/font_loader/from_file.rb +5 -1
  129. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  130. data/lib/hexapdf/font_loader/variant_from_name.rb +1 -1
  131. data/lib/hexapdf/font_loader.rb +1 -1
  132. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  133. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  134. data/lib/hexapdf/image_loader/png.rb +1 -1
  135. data/lib/hexapdf/image_loader.rb +1 -1
  136. data/lib/hexapdf/importer.rb +1 -1
  137. data/lib/hexapdf/layout/box.rb +1 -1
  138. data/lib/hexapdf/layout/box_fitter.rb +1 -1
  139. data/lib/hexapdf/layout/column_box.rb +1 -1
  140. data/lib/hexapdf/layout/container_box.rb +1 -1
  141. data/lib/hexapdf/layout/frame.rb +1 -1
  142. data/lib/hexapdf/layout/image_box.rb +1 -1
  143. data/lib/hexapdf/layout/inline_box.rb +1 -1
  144. data/lib/hexapdf/layout/line.rb +1 -1
  145. data/lib/hexapdf/layout/list_box.rb +1 -1
  146. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  147. data/lib/hexapdf/layout/page_style.rb +1 -1
  148. data/lib/hexapdf/layout/style.rb +7 -3
  149. data/lib/hexapdf/layout/table_box.rb +1 -1
  150. data/lib/hexapdf/layout/text_box.rb +1 -1
  151. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  152. data/lib/hexapdf/layout/text_layouter.rb +1 -1
  153. data/lib/hexapdf/layout/text_shaper.rb +1 -1
  154. data/lib/hexapdf/layout/width_from_polygon.rb +1 -1
  155. data/lib/hexapdf/layout.rb +1 -1
  156. data/lib/hexapdf/name_tree_node.rb +1 -1
  157. data/lib/hexapdf/number_tree_node.rb +1 -1
  158. data/lib/hexapdf/object.rb +1 -1
  159. data/lib/hexapdf/parser.rb +1 -1
  160. data/lib/hexapdf/pdf_array.rb +1 -1
  161. data/lib/hexapdf/rectangle.rb +1 -1
  162. data/lib/hexapdf/reference.rb +1 -1
  163. data/lib/hexapdf/revision.rb +1 -1
  164. data/lib/hexapdf/revisions.rb +1 -1
  165. data/lib/hexapdf/serializer.rb +3 -3
  166. data/lib/hexapdf/stream.rb +1 -1
  167. data/lib/hexapdf/task/dereference.rb +1 -1
  168. data/lib/hexapdf/task/import_pages.rb +185 -0
  169. data/lib/hexapdf/task/merge_acro_form.rb +1 -1
  170. data/lib/hexapdf/task/optimize.rb +1 -1
  171. data/lib/hexapdf/task/pdfa.rb +109 -2
  172. data/lib/hexapdf/task.rb +2 -1
  173. data/lib/hexapdf/test_utils.rb +1 -1
  174. data/lib/hexapdf/tokenizer.rb +1 -1
  175. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  176. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  177. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  178. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  179. data/lib/hexapdf/type/acro_form/form.rb +5 -1
  180. data/lib/hexapdf/type/acro_form/java_script_actions.rb +1 -1
  181. data/lib/hexapdf/type/acro_form/signature_field.rb +1 -1
  182. data/lib/hexapdf/type/acro_form/text_field.rb +5 -3
  183. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  184. data/lib/hexapdf/type/acro_form.rb +1 -1
  185. data/lib/hexapdf/type/action.rb +1 -1
  186. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  187. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  188. data/lib/hexapdf/type/actions/launch.rb +1 -1
  189. data/lib/hexapdf/type/actions/set_ocg_state.rb +1 -1
  190. data/lib/hexapdf/type/actions/uri.rb +1 -1
  191. data/lib/hexapdf/type/actions.rb +1 -1
  192. data/lib/hexapdf/type/annotation.rb +1 -1
  193. data/lib/hexapdf/type/annotations/appearance_generator.rb +1 -1
  194. data/lib/hexapdf/type/annotations/border_effect.rb +1 -1
  195. data/lib/hexapdf/type/annotations/border_styling.rb +1 -1
  196. data/lib/hexapdf/type/annotations/circle.rb +1 -1
  197. data/lib/hexapdf/type/annotations/interior_color.rb +1 -1
  198. data/lib/hexapdf/type/annotations/line.rb +1 -1
  199. data/lib/hexapdf/type/annotations/line_ending_styling.rb +1 -1
  200. data/lib/hexapdf/type/annotations/link.rb +1 -1
  201. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  202. data/lib/hexapdf/type/annotations/polygon.rb +1 -1
  203. data/lib/hexapdf/type/annotations/polygon_polyline.rb +1 -1
  204. data/lib/hexapdf/type/annotations/polyline.rb +1 -1
  205. data/lib/hexapdf/type/annotations/square.rb +1 -1
  206. data/lib/hexapdf/type/annotations/square_circle.rb +1 -1
  207. data/lib/hexapdf/type/annotations/text.rb +1 -1
  208. data/lib/hexapdf/type/annotations/widget.rb +10 -1
  209. data/lib/hexapdf/type/annotations.rb +1 -1
  210. data/lib/hexapdf/type/catalog.rb +1 -1
  211. data/lib/hexapdf/type/cid_font.rb +1 -1
  212. data/lib/hexapdf/type/cmap.rb +1 -1
  213. data/lib/hexapdf/type/document_security_store.rb +80 -0
  214. data/lib/hexapdf/type/embedded_file.rb +1 -1
  215. data/lib/hexapdf/type/file_specification.rb +1 -1
  216. data/lib/hexapdf/type/font.rb +4 -4
  217. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  218. data/lib/hexapdf/type/font_simple.rb +1 -1
  219. data/lib/hexapdf/type/font_true_type.rb +1 -1
  220. data/lib/hexapdf/type/font_type0.rb +1 -1
  221. data/lib/hexapdf/type/font_type1.rb +1 -1
  222. data/lib/hexapdf/type/font_type3.rb +6 -1
  223. data/lib/hexapdf/type/form.rb +1 -1
  224. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  225. data/lib/hexapdf/type/icon_fit.rb +1 -1
  226. data/lib/hexapdf/type/image.rb +1 -1
  227. data/lib/hexapdf/type/info.rb +1 -1
  228. data/lib/hexapdf/type/mark_information.rb +1 -1
  229. data/lib/hexapdf/type/marked_content_reference.rb +1 -1
  230. data/lib/hexapdf/type/measure.rb +1 -1
  231. data/lib/hexapdf/type/metadata.rb +1 -1
  232. data/lib/hexapdf/type/names.rb +1 -1
  233. data/lib/hexapdf/type/namespace.rb +1 -1
  234. data/lib/hexapdf/type/object_reference.rb +1 -1
  235. data/lib/hexapdf/type/object_stream.rb +1 -1
  236. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  237. data/lib/hexapdf/type/optional_content_group.rb +1 -1
  238. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  239. data/lib/hexapdf/type/optional_content_properties.rb +1 -1
  240. data/lib/hexapdf/type/outline.rb +1 -1
  241. data/lib/hexapdf/type/outline_item.rb +1 -1
  242. data/lib/hexapdf/type/output_intent.rb +1 -1
  243. data/lib/hexapdf/type/page.rb +12 -1
  244. data/lib/hexapdf/type/page_label.rb +1 -1
  245. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  246. data/lib/hexapdf/type/resources.rb +1 -1
  247. data/lib/hexapdf/type/struct_elem.rb +1 -1
  248. data/lib/hexapdf/type/struct_tree_root.rb +1 -1
  249. data/lib/hexapdf/type/trailer.rb +1 -1
  250. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  251. data/lib/hexapdf/type/xref_stream.rb +1 -1
  252. data/lib/hexapdf/type.rb +2 -1
  253. data/lib/hexapdf/utils/bit_field.rb +1 -1
  254. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  255. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  256. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  257. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  258. data/lib/hexapdf/utils/object_hash.rb +1 -1
  259. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  260. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  261. data/lib/hexapdf/utils.rb +1 -1
  262. data/lib/hexapdf/version.rb +2 -2
  263. data/lib/hexapdf/writer.rb +1 -1
  264. data/lib/hexapdf/xref_section.rb +1 -1
  265. data/lib/hexapdf.rb +1 -1
  266. data/test/data/pdfa/mismatching_glyph_widths_cidfont_type2.pdf +0 -0
  267. data/test/hexapdf/content/test_smart_text_extractor.rb +129 -0
  268. data/test/hexapdf/digital_signature/common.rb +19 -5
  269. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +29 -4
  270. data/test/hexapdf/digital_signature/test_signatures.rb +3 -3
  271. data/test/hexapdf/filter/test_brotli_decode.rb +34 -0
  272. data/test/hexapdf/font/test_true_type_wrapper.rb +2 -2
  273. data/test/hexapdf/font/true_type/table/test_directory.rb +5 -3
  274. data/test/hexapdf/font/true_type/test_builder.rb +9 -0
  275. data/test/hexapdf/font/true_type/test_font.rb +17 -3
  276. data/test/hexapdf/font/true_type/test_subsetter.rb +11 -9
  277. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  278. data/test/hexapdf/task/test_import_pages.rb +126 -0
  279. data/test/hexapdf/task/test_pdfa.rb +72 -0
  280. data/test/hexapdf/test_document.rb +13 -0
  281. data/test/hexapdf/test_serializer.rb +1 -1
  282. data/test/hexapdf/type/acro_form/test_form.rb +6 -0
  283. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  284. data/test/hexapdf/type/annotations/test_widget.rb +11 -0
  285. data/test/hexapdf/type/test_font_type3.rb +4 -0
  286. data/test/hexapdf/type/test_page.rb +8 -0
  287. metadata +25 -1
@@ -0,0 +1,311 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ module HexaPDF
38
+ module Content
39
+
40
+ # This module converts the glyphs on a page to a single text string while preserving the layout.
41
+ #
42
+ # The general algorithm is:
43
+ #
44
+ # 1. Collect all individual glyphs with their user space coordinates in
45
+ # TextRunCollector::TextRun objects.
46
+ #
47
+ # 2. Sort text runs top to bottom and then left to right.
48
+ #
49
+ # 3. Group those text runs into lines based on a "baseline" while also combining neighboring
50
+ # text runs into larger runs.
51
+ #
52
+ # 4. Render each line into a string by taking into account the page size and the median glyph
53
+ # width for a text run to column mapping.
54
+ #
55
+ # 5. Add blank lines between text lines based on the page's normal line spacing.
56
+ module SmartTextExtractor
57
+
58
+ # This module provides the functionality for collecting the necessary TextRun instances for
59
+ # layouting the text.
60
+ #
61
+ # To use this module include it in a processor class. Then invoke the #collect_text_runs
62
+ # method in the #show_text and #show_text_with_positioning methods.
63
+ #
64
+ # Example:
65
+ #
66
+ # class CustomProcessor < HexaPDF::Content::Processor
67
+ # include TextRunCollector
68
+ #
69
+ # def show_text(str)
70
+ # collect_text_runs(decode_text_with_positioning(str))
71
+ # end
72
+ # alias show_text_with_positioning show_text
73
+ #
74
+ # end
75
+ #
76
+ # Once the processor has done its job, the collected text runs are available via the
77
+ # #text_runs method. Use them as input for SmartTextExtractor.layout_text_runs.
78
+ module TextRunCollector
79
+
80
+ # Represents a single run of continuous glyphs and their combined bounding box in user
81
+ # space.
82
+ TextRun = Struct.new(:string, :left, :bottom, :right, :top) do
83
+ # The "baseline" is approximated with the bottom of the bounding box.
84
+ #
85
+ # This works because HexaPDF uses a font's bounding box instead of the glyph's bounding
86
+ # box for each glyph. So while differently sized glyphs will have different "baseline"
87
+ # values, this is taken into account in the algorithm in the same way as subscript and
88
+ # superscript.
89
+ #
90
+ # Using this "fake" baseline works well enough and avoids additional calculations.
91
+ def baseline
92
+ bottom
93
+ end
94
+
95
+ # The height of the text run's bounding box.
96
+ def height
97
+ top - bottom
98
+ end
99
+
100
+ # The width of the text run's bounding box.
101
+ def width
102
+ right - left
103
+ end
104
+ end
105
+
106
+ # Array with all collected TextRun instances.
107
+ attr_reader :text_runs
108
+
109
+ def initialize # :nodoc:
110
+ super
111
+ @text_runs = []
112
+ end
113
+
114
+ private
115
+
116
+ # Collects all text runs from the glyphs in the +boxes+ array.
117
+ def collect_text_runs(boxes)
118
+ boxes.each do |box|
119
+ llx, lly, lrx, lry, urx, ury, ulx, uly = *box.points
120
+ x_min, x_max = [llx, lrx, ulx, urx].minmax
121
+ y_min, y_max = [lly, lry, uly, ury].minmax
122
+ @text_runs << TextRun.new(+box.string, x_min, y_min, x_max, y_max)
123
+ end
124
+ end
125
+ end
126
+
127
+ # This processor class is used when layouting the text through
128
+ # HexaPDF::Type::Page#extract_text.
129
+ class TextRunProcessor < HexaPDF::Content::Processor
130
+
131
+ include TextRunCollector
132
+
133
+ def show_text(str)
134
+ collect_text_runs(decode_text_with_positioning(str))
135
+ end
136
+ alias show_text_with_positioning show_text
137
+
138
+ end
139
+
140
+ # Converts an array of TextRun objects into a single string representation, preserving the
141
+ # visual layout.
142
+ #
143
+ # The +page_width+ and +page_height+ arguments specify the width and height of the page from
144
+ # which the text runs were extracted.
145
+ #
146
+ # The remaining keyword arguments can be used to fine-tune the algorithm for one's needs:
147
+ #
148
+ # +line_tolerance_factor+::
149
+ # The tolerance factor is applied to the median text run height to determine the range
150
+ # within which two text runs are considered to be on the same line. This ensures that
151
+ # small differences in the baseline due to, for example, subscript or superscript parts
152
+ # don't result in multiple lines.
153
+ #
154
+ # The factor should not be too large to avoid forcing separate visual lines into one line
155
+ # but also not too small to avoid subscript/superscript begin on separate lines. The
156
+ # default seems to work quite well.
157
+ #
158
+ # +paragraph_distance_threshold+::
159
+ # If the number of normal line spacings between two adjacent baselines is at least this
160
+ # large (but smaller than +large_distance_threshold+), the gap is interpreted as a
161
+ # paragraph break and a single blank line is inserted.
162
+ #
163
+ # +large_distance_threshold+::
164
+ # Works like +paragraph_distance_threshold+ and indicates if a number of normal line
165
+ # spacings is too large for being a paragraph break. A proportional number of blank lines
166
+ # is inserted in this case.
167
+ #
168
+ # This is used to represent large parts with non-text content like images.
169
+ def self.layout_text_runs(text_runs, page_width, page_height,
170
+ line_tolerance_factor: 0.4, paragraph_distance_threshold: 1.35,
171
+ large_distance_threshold: 3.0)
172
+ return '' if text_runs.empty?
173
+
174
+ # Use the median height of all text runs as an approximation of the main font size used on
175
+ # the page. The line tolerance uses a hard floor for small fonts.
176
+ median_height = median(text_runs.map(&:height).sort)
177
+ line_tolerance = [median_height * line_tolerance_factor, 2].max
178
+
179
+ # Group the text runs into lines which are sorted top to bottom. Text runs are pre-sorted by
180
+ # baseline from top to bottom and left to right (the latter is done so that consecutive text
181
+ # runs can be combined).
182
+ sorted = text_runs.sort_by {|run| [-run.baseline, run.left] }
183
+ lines = group_into_lines(sorted, line_tolerance)
184
+
185
+ # Calculate the normal line spacing, excluding anything too small/big.
186
+ line_distances = lines.map {|l| l.baseline }.each_cons(2).map {|a, b| a - b }.
187
+ select {|d| d >= median_height * 0.5 && d <= median_height * 2 }.sort
188
+ normal_line_spacing = line_distances.empty? ? median_height * 1.2 : median(line_distances)
189
+
190
+ # Convert the lines into actual text strings. Blank lines are inserted between the lines
191
+ # based on the normal line spacing.
192
+ output_lines = []
193
+ left_margin = lines.map {|line| line.text_runs[0].left }.min
194
+ glyph_widths = lines.flat_map do |line|
195
+ line.text_runs.flat_map {|run| [run.width.to_f / run.string.length] * run.string.length }
196
+ end.sort
197
+ median_glyph_width = median(glyph_widths)
198
+
199
+ lines.each_with_index do |line, index|
200
+ output_lines << text_runs_to_string(line.text_runs, median_glyph_width, left_margin)
201
+ next if index == lines.length - 1
202
+
203
+ # Add blank lines as needed.
204
+ ratio = (line.baseline - lines[index + 1].baseline) / normal_line_spacing
205
+ if ratio >= large_distance_threshold
206
+ # Subtract 1 because the newline after the output line already counts as one
207
+ # newline. Also cap at a maximum of 40 to avoid huge gaps.
208
+ [ratio.round - 1, 40].min.times { output_lines << '' }
209
+ elsif ratio >= paragraph_distance_threshold
210
+ output_lines << ''
211
+ end
212
+ end
213
+
214
+ output_lines.join("\n")
215
+ end
216
+
217
+ # Holds an array of TextRun objects and their median baseline.
218
+ Line = Struct.new(:text_runs, :baseline)
219
+
220
+ # Groups a sorted list of TextRuns (sorted by baseline, then left) into lines.
221
+ #
222
+ # Since the text_runs are already sorted, a single run through +sorted_text_runs+ is
223
+ # sufficient. A new line is created if a text run's baseline differs by more than +tolerance+
224
+ # from the current line's (median) baseline.
225
+ #
226
+ # The result is a list of Line objects with their contents sorted left to right.
227
+ def self.group_into_lines(sorted_text_runs, tolerance)
228
+ lines = []
229
+ current_line = []
230
+ current_baseline = sorted_text_runs[0].baseline
231
+ current_baselines = [current_baseline]
232
+
233
+ sorted_text_runs.each do |text_run|
234
+ # Try to combine text_runs that share exactly the same height and are next to each
235
+ # other. This avoids potentially garbled output because if two text parts are above each
236
+ # other but end up on the same line, the text runs would be mixed up (think: centered
237
+ # table header where some cells contain two lines).
238
+ if (last = current_line[-1]) && last.bottom == text_run.bottom &&
239
+ last.top == text_run.top && text_run.left - last.right < 1
240
+ last.string << text_run.string
241
+ last.right = text_run.right
242
+ elsif (current_baseline - text_run.baseline).abs <= tolerance
243
+ current_line << text_run
244
+ current_baselines << text_run.baseline
245
+ current_baseline = median(current_baselines)
246
+ else
247
+ lines << Line.new(current_line.sort_by!(&:left), current_baseline)
248
+ current_line = [text_run]
249
+ current_baseline = text_run.baseline
250
+ current_baselines.clear
251
+ current_baselines << current_baseline
252
+ end
253
+ end
254
+ lines << Line.new(current_line.sort_by!(&:left), current_baseline)
255
+ end
256
+ private_class_method :group_into_lines
257
+
258
+ # Returns the median value of the given sorted array of numerics.
259
+ def self.median(sorted_array)
260
+ mid = sorted_array.length / 2
261
+ sorted_array.length.odd? ? sorted_array[mid] : (sorted_array[mid - 1] + sorted_array[mid]) / 2.0
262
+ end
263
+ private_class_method :median
264
+
265
+ # Renders an array of TextRun objects representing one line to a single string.
266
+ #
267
+ # +median_glyph_width+:: Is used to determine the column for each text run.
268
+ # +left_margin+:: Is removed from the left side to avoid unnecessary indentation.
269
+ def self.text_runs_to_string(text_runs, median_glyph_width, left_margin)
270
+ # Minimum gap to classify as a word boundary
271
+ space_threshold = median_glyph_width * 0.5
272
+
273
+ result = +''
274
+ # The column where the last text run ended. Can be different from result.size due to fitting
275
+ # proportional-width fonts to a fixed-column output.
276
+ cursor = 0
277
+
278
+ text_runs.each_with_index do |text_run, index|
279
+ target_col = ((text_run.left - left_margin) / median_glyph_width).round
280
+ advance = target_col - cursor
281
+
282
+ if advance > 0
283
+ result << ' ' * advance
284
+ cursor += advance
285
+ elsif index >= 1 && text_run.left - text_runs[index - 1].right > space_threshold &&
286
+ result[-1] != ' '
287
+ # Force space even if advance < 0 when the actual spacing between text runs is large
288
+ # enough. This might happen because we are projecting proportional-width fonts to a
289
+ # fixed-column output.
290
+ cursor = target_col
291
+ result << ' '
292
+ end
293
+
294
+ result << text_run.string
295
+
296
+ # Move cursor to the text run's right edge but at least the text run's character count
297
+ # from the current position. This avoids gaps when there is too much difference between
298
+ # the on-page position and the approximated cursor. However, a one column difference is
299
+ # ignored to account for rounding errors.
300
+ cursor += text_run.string.size
301
+ text_run_right_edge_cursor = ((text_run.right - left_margin) / median_glyph_width).round
302
+ cursor = [text_run_right_edge_cursor, cursor].max if text_run_right_edge_cursor != cursor + 1
303
+ end
304
+
305
+ result.rstrip
306
+ end
307
+ private_class_method :text_runs_to_string
308
+
309
+ end
310
+ end
311
+ end
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -44,6 +44,7 @@ module HexaPDF
44
44
  #
45
45
  # * The Canvas class which provides an interface for drawing graphics and text.
46
46
  # * The Parser and Processor classes for processing an existing content stream.
47
+ # * SmartTextExtractor for extracting layouted text from a page.
47
48
  module Content
48
49
 
49
50
  autoload(:Canvas, 'hexapdf/content/canvas')
@@ -52,6 +53,7 @@ module HexaPDF
52
53
  autoload(:ColorSpace, 'hexapdf/content/color_space')
53
54
  autoload(:Operator, 'hexapdf/content/operator')
54
55
  autoload(:CanvasComposer, 'hexapdf/content/canvas_composer')
56
+ autoload(:SmartTextExtractor, 'hexapdf/content/smart_text_extractor')
55
57
 
56
58
  end
57
59
 
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -52,9 +52,6 @@ module HexaPDF
52
52
  # The signing handler is used by default by all methods that need a signing handler. Therefore
53
53
  # it is usually only necessary to provide the actual attribute values.
54
54
  #
55
- # *Note*: Currently only RSA is supported, DSA and ECDSA are not. See the examples below for
56
- # how to handle them using external signing.
57
- #
58
55
  #
59
56
  # == CMS and PAdES Signatures
60
57
  #
@@ -131,17 +128,6 @@ module HexaPDF
131
128
  # document.sign("output.pdf", certificate: my_cert, certificate_chain: my_chain,
132
129
  # external_signing: signing_proc)
133
130
  #
134
- # # Signing with DSA or ECDSA certificate/keys
135
- # signing_proc = lambda do |io, byte_range|
136
- # io.pos = byte_range[0]
137
- # data = io.read(byte_range[1])
138
- # io.pos = byte_range[2]
139
- # data << io.read(byte_range[3])
140
- # OpenSSL::PKCS7.sign(certificate, key, data, certificate_chain,
141
- # OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
142
- # end
143
- # document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc)
144
- #
145
131
  #
146
132
  # == Implementing a Signing Handler
147
133
  #
@@ -277,7 +263,7 @@ module HexaPDF
277
263
  # If a custom size is set using #signature_size=, it used. Otherwise the size is determined
278
264
  # by using #sign to sign an empty string.
279
265
  def signature_size
280
- @signature_size || sign(StringIO.new, [0, 0, 0, 0]).size
266
+ @signature_size || sign(StringIO.new, [0, 0, 0, 0]).size + 5
281
267
  end
282
268
 
283
269
  # Finalizes the signature field as well as the signature dictionary before writing.
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -121,7 +121,7 @@ module HexaPDF
121
121
  private
122
122
 
123
123
  # Creates the set of signed attributes for the signer information structure.
124
- def create_signed_attrs(data, signing_time: true)
124
+ def create_signed_attrs(data, ess_cert_hash: 'sha256', signing_time: true)
125
125
  signing_time = (self.signing_time || Time.now).utc if signing_time
126
126
  set(
127
127
  attribute('content-type', oid('id-data')),
@@ -132,12 +132,13 @@ module HexaPDF
132
132
  ),
133
133
  attribute(
134
134
  'id-aa-signingCertificateV2',
135
- sequence( # SigningCertificateV2
135
+ sequence( # SigningCertificateV2, see RFC5035
136
136
  sequence( # Seq of ESSCertIDv2
137
137
  sequence( # ESSCertIDv2
138
- #TODO: Does not validate on ETSI checker if used, doesn't matter if SHA256 or 512
139
- #oid('sha512'),
140
- binary(OpenSSL::Digest.digest('sha256', @certificate.to_der)), # certHash
138
+ (sequence( # AlgorithmIdentifier RFC3280 4.1.1.2
139
+ oid(ess_cert_hash) # algorithm
140
+ ) unless ess_cert_hash == 'sha256'),
141
+ binary(OpenSSL::Digest.digest(ess_cert_hash, @certificate.to_der)), # certHash
141
142
  sequence( # issuerSerial
142
143
  sequence( # issuer
143
144
  implicit(4, sequence(@certificate.issuer)) # choice 4 directoryName
@@ -184,13 +185,19 @@ module HexaPDF
184
185
  # Creates a signer information structure containing the actual meat of the whole CMS object.
185
186
  def create_signer_info(signature, signed_attrs, unsigned_attrs = nil)
186
187
  certificate_pkey_algorithm = @certificate.public_key.oid
187
- signature_algorithm = if certificate_pkey_algorithm == 'rsaEncryption'
188
+ signature_algorithm = case certificate_pkey_algorithm
189
+ when 'rsaEncryption'
188
190
  sequence( # signatureAlgorithm
189
191
  oid('rsaEncryption'), # algorithmID
190
192
  null # params
191
193
  )
192
- else
193
- raise HexaPDF::Error, "Unsupported key type/signature algorithm"
194
+ when 'DSA'
195
+ unless @digest_algorithm == 'sha256'
196
+ raise HexaPDF::Error, "Only SHA256 supported with DSA"
197
+ end
198
+ sequence(oid('id-dsa-with-sha256'), null)
199
+ when 'id-ecPublicKey'
200
+ sequence(oid("ecdsa-with-#{@digest_algorithm.upcase}"), null)
194
201
  end
195
202
 
196
203
  sequence(
@@ -273,6 +280,12 @@ module HexaPDF
273
280
  'sha384' => '2.16.840.1.101.3.4.2.2',
274
281
  'sha512' => '2.16.840.1.101.3.4.2.3',
275
282
  'rsaEncryption' => '1.2.840.113549.1.1.1',
283
+ 'id-dsa-with-sha1' => '1.2.840.10040.4.3',
284
+ 'id-dsa-with-sha256' => '2.16.840.1.101.3.4.3.2',
285
+ 'ecdsa-with-SHA1' => '1.2.840.10045.4.1',
286
+ 'ecdsa-with-SHA256' => '1.2.840.10045.4.3.2',
287
+ 'ecdsa-with-SHA384' => '1.2.840.10045.4.3.3',
288
+ 'ecdsa-with-SHA512' => '1.2.840.10045.4.3.4',
276
289
  'id-aa-signingCertificate' => '1.2.840.113549.1.9.16.2.12',
277
290
  'id-aa-timeStampToken' => '1.2.840.113549.1.9.16.2.14',
278
291
  'id-aa-signingCertificateV2' => '1.2.840.113549.1.9.16.2.47',
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -4,7 +4,7 @@
4
4
  # This file is part of HexaPDF.
5
5
  #
6
6
  # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
- # Copyright (C) 2014-2025 Thomas Leitner
7
+ # Copyright (C) 2014-2026 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as