hexapdf 1.7.0 → 1.9.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 (294) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/LICENSE +1 -1
  4. data/README.md +3 -0
  5. data/Rakefile +1 -1
  6. data/data/hexapdf/fonts/Inter-Bold.ttf +0 -0
  7. data/data/hexapdf/fonts/Inter-BoldItalic.ttf +0 -0
  8. data/data/hexapdf/fonts/Inter-Italic.ttf +0 -0
  9. data/data/hexapdf/fonts/Inter-Regular.ttf +0 -0
  10. data/data/hexapdf/fonts/OFL.txt +92 -0
  11. data/examples/005-merging.rb +2 -1
  12. data/examples/019-acro_form.rb +3 -1
  13. data/examples/030-pdfa.rb +9 -16
  14. data/examples/034-text_shaping.rb +37 -0
  15. data/lib/hexapdf/cli/batch.rb +1 -1
  16. data/lib/hexapdf/cli/command.rb +1 -1
  17. data/lib/hexapdf/cli/debug_info.rb +1 -1
  18. data/lib/hexapdf/cli/files.rb +1 -1
  19. data/lib/hexapdf/cli/fonts.rb +6 -4
  20. data/lib/hexapdf/cli/form.rb +1 -1
  21. data/lib/hexapdf/cli/image2pdf.rb +1 -1
  22. data/lib/hexapdf/cli/images.rb +17 -17
  23. data/lib/hexapdf/cli/info.rb +1 -1
  24. data/lib/hexapdf/cli/inspect.rb +1 -1
  25. data/lib/hexapdf/cli/merge.rb +14 -2
  26. data/lib/hexapdf/cli/modify.rb +1 -1
  27. data/lib/hexapdf/cli/optimize.rb +1 -1
  28. data/lib/hexapdf/cli/split.rb +1 -1
  29. data/lib/hexapdf/cli/usage.rb +1 -1
  30. data/lib/hexapdf/cli/watermark.rb +1 -1
  31. data/lib/hexapdf/cli.rb +1 -1
  32. data/lib/hexapdf/composer.rb +1 -1
  33. data/lib/hexapdf/configuration.rb +15 -2
  34. data/lib/hexapdf/content/canvas.rb +1 -1
  35. data/lib/hexapdf/content/canvas_composer.rb +1 -1
  36. data/lib/hexapdf/content/color_space.rb +1 -1
  37. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  38. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  39. data/lib/hexapdf/content/graphic_object/geom2d.rb +1 -1
  40. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  41. data/lib/hexapdf/content/graphic_object.rb +1 -1
  42. data/lib/hexapdf/content/graphics_state.rb +1 -1
  43. data/lib/hexapdf/content/operator.rb +1 -1
  44. data/lib/hexapdf/content/parser.rb +1 -1
  45. data/lib/hexapdf/content/processor.rb +1 -1
  46. data/lib/hexapdf/content/smart_text_extractor.rb +10 -4
  47. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  48. data/lib/hexapdf/content.rb +1 -1
  49. data/lib/hexapdf/data_dir.rb +1 -1
  50. data/lib/hexapdf/dictionary.rb +1 -1
  51. data/lib/hexapdf/dictionary_fields.rb +1 -1
  52. data/lib/hexapdf/digital_signature/cms_handler.rb +1 -1
  53. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  54. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +1 -1
  55. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  56. data/lib/hexapdf/digital_signature/signatures.rb +1 -1
  57. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -1
  58. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  59. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +1 -1
  60. data/lib/hexapdf/digital_signature/signing.rb +1 -1
  61. data/lib/hexapdf/digital_signature/verification_result.rb +1 -1
  62. data/lib/hexapdf/digital_signature.rb +1 -1
  63. data/lib/hexapdf/document/annotations.rb +26 -1
  64. data/lib/hexapdf/document/destinations.rb +1 -1
  65. data/lib/hexapdf/document/files.rb +1 -1
  66. data/lib/hexapdf/document/fonts.rb +1 -1
  67. data/lib/hexapdf/document/images.rb +1 -1
  68. data/lib/hexapdf/document/layout.rb +1 -1
  69. data/lib/hexapdf/document/metadata.rb +1 -1
  70. data/lib/hexapdf/document/pages.rb +1 -1
  71. data/lib/hexapdf/document.rb +1 -1
  72. data/lib/hexapdf/encryption/aes.rb +1 -1
  73. data/lib/hexapdf/encryption/arc4.rb +1 -1
  74. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  75. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  76. data/lib/hexapdf/encryption/identity.rb +1 -1
  77. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  78. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  79. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  80. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  81. data/lib/hexapdf/encryption.rb +1 -1
  82. data/lib/hexapdf/error.rb +1 -1
  83. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  84. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  85. data/lib/hexapdf/filter/brotli_decode.rb +1 -1
  86. data/lib/hexapdf/filter/crypt.rb +1 -1
  87. data/lib/hexapdf/filter/encryption.rb +1 -1
  88. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  89. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  90. data/lib/hexapdf/filter/pass_through.rb +1 -1
  91. data/lib/hexapdf/filter/predictor.rb +1 -1
  92. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  93. data/lib/hexapdf/filter.rb +1 -1
  94. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  95. data/lib/hexapdf/font/cmap/writer.rb +16 -10
  96. data/lib/hexapdf/font/cmap.rb +1 -1
  97. data/lib/hexapdf/font/encoding/base.rb +1 -1
  98. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  99. data/lib/hexapdf/font/encoding/glyph_list.rb +1 -1
  100. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  101. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  102. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  103. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  104. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  105. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  106. data/lib/hexapdf/font/encoding.rb +1 -1
  107. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  108. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  109. data/lib/hexapdf/font/true_type/font.rb +1 -1
  110. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  111. data/lib/hexapdf/font/true_type/subsetter.rb +4 -4
  112. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  113. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  114. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  115. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  116. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  117. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  118. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  119. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  120. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  121. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  122. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  123. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  124. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  125. data/lib/hexapdf/font/true_type/table.rb +1 -1
  126. data/lib/hexapdf/font/true_type.rb +1 -1
  127. data/lib/hexapdf/font/true_type_wrapper.rb +9 -6
  128. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  129. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  130. data/lib/hexapdf/font/type1/font.rb +1 -1
  131. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  132. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  133. data/lib/hexapdf/font/type1.rb +1 -1
  134. data/lib/hexapdf/font/type1_wrapper.rb +1 -1
  135. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  136. data/lib/hexapdf/font_loader/from_file.rb +5 -1
  137. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  138. data/lib/hexapdf/font_loader/variant_from_name.rb +1 -1
  139. data/lib/hexapdf/font_loader.rb +48 -1
  140. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  141. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  142. data/lib/hexapdf/image_loader/png.rb +1 -1
  143. data/lib/hexapdf/image_loader.rb +1 -1
  144. data/lib/hexapdf/importer.rb +1 -1
  145. data/lib/hexapdf/layout/box.rb +1 -1
  146. data/lib/hexapdf/layout/box_fitter.rb +1 -1
  147. data/lib/hexapdf/layout/column_box.rb +1 -1
  148. data/lib/hexapdf/layout/container_box.rb +3 -5
  149. data/lib/hexapdf/layout/frame.rb +1 -1
  150. data/lib/hexapdf/layout/image_box.rb +1 -1
  151. data/lib/hexapdf/layout/inline_box.rb +1 -1
  152. data/lib/hexapdf/layout/line.rb +1 -1
  153. data/lib/hexapdf/layout/list_box.rb +1 -1
  154. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  155. data/lib/hexapdf/layout/page_style.rb +1 -1
  156. data/lib/hexapdf/layout/style.rb +67 -5
  157. data/lib/hexapdf/layout/table_box.rb +97 -14
  158. data/lib/hexapdf/layout/text_box.rb +1 -1
  159. data/lib/hexapdf/layout/text_fragment.rb +14 -8
  160. data/lib/hexapdf/layout/text_layouter.rb +1 -1
  161. data/lib/hexapdf/layout/text_shaper.rb +163 -11
  162. data/lib/hexapdf/layout/width_from_polygon.rb +1 -1
  163. data/lib/hexapdf/layout.rb +1 -1
  164. data/lib/hexapdf/name_tree_node.rb +1 -1
  165. data/lib/hexapdf/number_tree_node.rb +1 -1
  166. data/lib/hexapdf/object.rb +1 -1
  167. data/lib/hexapdf/parser.rb +1 -1
  168. data/lib/hexapdf/pdf_array.rb +1 -1
  169. data/lib/hexapdf/rectangle.rb +1 -1
  170. data/lib/hexapdf/reference.rb +1 -1
  171. data/lib/hexapdf/revision.rb +1 -1
  172. data/lib/hexapdf/revisions.rb +1 -1
  173. data/lib/hexapdf/serializer.rb +3 -3
  174. data/lib/hexapdf/stream.rb +1 -1
  175. data/lib/hexapdf/task/dereference.rb +1 -1
  176. data/lib/hexapdf/task/import_pages.rb +185 -0
  177. data/lib/hexapdf/task/merge_acro_form.rb +1 -1
  178. data/lib/hexapdf/task/optimize.rb +1 -1
  179. data/lib/hexapdf/task/pdfa.rb +1 -1
  180. data/lib/hexapdf/task.rb +2 -1
  181. data/lib/hexapdf/test_utils.rb +1 -1
  182. data/lib/hexapdf/tokenizer.rb +1 -1
  183. data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
  184. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  185. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  186. data/lib/hexapdf/type/acro_form/field.rb +1 -1
  187. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  188. data/lib/hexapdf/type/acro_form/java_script_actions.rb +1 -1
  189. data/lib/hexapdf/type/acro_form/signature_field.rb +1 -1
  190. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  191. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  192. data/lib/hexapdf/type/acro_form.rb +1 -1
  193. data/lib/hexapdf/type/action.rb +1 -1
  194. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  195. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  196. data/lib/hexapdf/type/actions/launch.rb +1 -1
  197. data/lib/hexapdf/type/actions/set_ocg_state.rb +1 -1
  198. data/lib/hexapdf/type/actions/uri.rb +1 -1
  199. data/lib/hexapdf/type/actions.rb +1 -1
  200. data/lib/hexapdf/type/annotation.rb +1 -1
  201. data/lib/hexapdf/type/annotations/appearance_generator.rb +43 -1
  202. data/lib/hexapdf/type/annotations/border_effect.rb +1 -1
  203. data/lib/hexapdf/type/annotations/border_styling.rb +1 -1
  204. data/lib/hexapdf/type/annotations/circle.rb +1 -1
  205. data/lib/hexapdf/type/annotations/ink.rb +107 -0
  206. data/lib/hexapdf/type/annotations/interior_color.rb +1 -1
  207. data/lib/hexapdf/type/annotations/line.rb +1 -1
  208. data/lib/hexapdf/type/annotations/line_ending_styling.rb +1 -1
  209. data/lib/hexapdf/type/annotations/link.rb +1 -1
  210. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  211. data/lib/hexapdf/type/annotations/polygon.rb +1 -1
  212. data/lib/hexapdf/type/annotations/polygon_polyline.rb +1 -1
  213. data/lib/hexapdf/type/annotations/polyline.rb +1 -1
  214. data/lib/hexapdf/type/annotations/square.rb +1 -1
  215. data/lib/hexapdf/type/annotations/square_circle.rb +1 -1
  216. data/lib/hexapdf/type/annotations/text.rb +1 -1
  217. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  218. data/lib/hexapdf/type/annotations.rb +2 -1
  219. data/lib/hexapdf/type/catalog.rb +1 -1
  220. data/lib/hexapdf/type/cid_font.rb +1 -1
  221. data/lib/hexapdf/type/cmap.rb +1 -1
  222. data/lib/hexapdf/type/document_security_store.rb +1 -1
  223. data/lib/hexapdf/type/embedded_file.rb +1 -1
  224. data/lib/hexapdf/type/file_specification.rb +1 -1
  225. data/lib/hexapdf/type/font.rb +4 -4
  226. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  227. data/lib/hexapdf/type/font_simple.rb +1 -1
  228. data/lib/hexapdf/type/font_true_type.rb +1 -1
  229. data/lib/hexapdf/type/font_type0.rb +1 -1
  230. data/lib/hexapdf/type/font_type1.rb +1 -1
  231. data/lib/hexapdf/type/font_type3.rb +6 -1
  232. data/lib/hexapdf/type/form.rb +1 -1
  233. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  234. data/lib/hexapdf/type/icon_fit.rb +1 -1
  235. data/lib/hexapdf/type/image.rb +1 -1
  236. data/lib/hexapdf/type/info.rb +1 -1
  237. data/lib/hexapdf/type/mark_information.rb +1 -1
  238. data/lib/hexapdf/type/marked_content_reference.rb +1 -1
  239. data/lib/hexapdf/type/measure.rb +1 -1
  240. data/lib/hexapdf/type/metadata.rb +1 -1
  241. data/lib/hexapdf/type/names.rb +1 -1
  242. data/lib/hexapdf/type/namespace.rb +1 -1
  243. data/lib/hexapdf/type/object_reference.rb +1 -1
  244. data/lib/hexapdf/type/object_stream.rb +1 -1
  245. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  246. data/lib/hexapdf/type/optional_content_group.rb +1 -1
  247. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  248. data/lib/hexapdf/type/optional_content_properties.rb +1 -1
  249. data/lib/hexapdf/type/outline.rb +1 -1
  250. data/lib/hexapdf/type/outline_item.rb +1 -1
  251. data/lib/hexapdf/type/output_intent.rb +1 -1
  252. data/lib/hexapdf/type/page.rb +1 -1
  253. data/lib/hexapdf/type/page_label.rb +1 -1
  254. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  255. data/lib/hexapdf/type/resources.rb +1 -1
  256. data/lib/hexapdf/type/struct_elem.rb +1 -1
  257. data/lib/hexapdf/type/struct_tree_root.rb +1 -1
  258. data/lib/hexapdf/type/trailer.rb +1 -1
  259. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  260. data/lib/hexapdf/type/xref_stream.rb +1 -1
  261. data/lib/hexapdf/type.rb +1 -1
  262. data/lib/hexapdf/utils/bit_field.rb +1 -1
  263. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  264. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  265. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  266. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  267. data/lib/hexapdf/utils/object_hash.rb +1 -1
  268. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  269. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  270. data/lib/hexapdf/utils.rb +1 -1
  271. data/lib/hexapdf/version.rb +2 -2
  272. data/lib/hexapdf/writer.rb +1 -1
  273. data/lib/hexapdf/xref_section.rb +1 -1
  274. data/lib/hexapdf.rb +1 -1
  275. data/test/hexapdf/digital_signature/common.rb +5 -5
  276. data/test/hexapdf/digital_signature/test_cms_handler.rb +1 -1
  277. data/test/hexapdf/document/test_annotations.rb +10 -0
  278. data/test/hexapdf/document/test_layout.rb +6 -3
  279. data/test/hexapdf/filter/test_brotli_decode.rb +1 -1
  280. data/test/hexapdf/font/cmap/test_writer.rb +8 -6
  281. data/test/hexapdf/font/test_true_type_wrapper.rb +6 -2
  282. data/test/hexapdf/font/true_type/test_subsetter.rb +7 -6
  283. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  284. data/test/hexapdf/layout/test_container_box.rb +3 -1
  285. data/test/hexapdf/layout/test_style.rb +4 -0
  286. data/test/hexapdf/layout/test_table_box.rb +117 -1
  287. data/test/hexapdf/layout/test_text_fragment.rb +18 -8
  288. data/test/hexapdf/layout/test_text_shaper.rb +55 -5
  289. data/test/hexapdf/task/test_import_pages.rb +126 -0
  290. data/test/hexapdf/test_serializer.rb +1 -1
  291. data/test/hexapdf/type/annotations/test_appearance_generator.rb +63 -0
  292. data/test/hexapdf/type/annotations/test_ink.rb +31 -0
  293. data/test/hexapdf/type/test_font_type3.rb +4 -0
  294. metadata +26 -2
@@ -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
@@ -111,20 +111,24 @@ module HexaPDF
111
111
  font = style.font
112
112
  text.each_codepoint do |codepoint|
113
113
  glyph = font.decode_codepoint(codepoint)
114
- if glyph.valid? || glyph.control_char?
114
+ if glyph.valid?
115
115
  items << glyph
116
116
  else
117
117
  unless items.empty?
118
- result << shaper.shape_text(new(items, style))
118
+ result.append(*shaper.shape_text(new(items, style)))
119
119
  items = []
120
120
  end
121
- fallback = yield(codepoint, glyph)
122
- unless fallback.empty?
123
- result << shaper.shape_text(new(fallback, styles[fallback.first.font_wrapper]))
121
+ if glyph.control_char?
122
+ result.append(new([glyph], style))
123
+ else
124
+ fallback = yield(codepoint, glyph)
125
+ unless fallback.empty?
126
+ result.append(*shaper.shape_text(new(fallback, styles[fallback.first.font_wrapper])))
127
+ end
124
128
  end
125
129
  end
126
130
  end
127
- result << shaper.shape_text(new(items, style)) unless items.empty?
131
+ result.append(*shaper.shape_text(new(items, style))) unless items.empty?
128
132
  result
129
133
  end
130
134
 
@@ -251,7 +255,9 @@ module HexaPDF
251
255
  tx = x - tlm.e
252
256
  ty = y - tlm.f
253
257
  if tx.abs < PRECISION
254
- if (ty + canvas.graphics_state.leading).abs < PRECISION
258
+ if ty.abs < PRECISION
259
+ # do nothing
260
+ elsif (ty + canvas.graphics_state.leading).abs < PRECISION
255
261
  canvas.move_text_cursor
256
262
  else
257
263
  canvas.move_text_cursor(offset: [0, ty], absolute: false)
@@ -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
@@ -34,8 +34,71 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
+ require 'hexapdf/error'
37
38
  require 'hexapdf/layout/numeric_refinements'
38
39
 
40
+ HARFBUZZ_AVAILABLE = begin
41
+ require 'harfbuzz'
42
+ true
43
+ rescue LoadError
44
+ end
45
+
46
+ if HARFBUZZ_AVAILABLE
47
+ class HarfBuzz::Buffer #:nodoc:
48
+
49
+ GLYPH_INFO_SIZE = HarfBuzz::C::HbGlyphInfoT.size
50
+ GLYPH_INFO_CODEPOINT_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:codepoint)
51
+ GLYPH_INFO_CLUSTER_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:cluster)
52
+ GLYPH_POS_SIZE = HarfBuzz::C::HbGlyphPositionT.size
53
+ GLYPH_POS_XADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_advance)
54
+ GLYPH_POS_YADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_advance)
55
+ GLYPH_POS_XOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_offset)
56
+ GLYPH_POS_YOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_offset)
57
+
58
+ # Iterates efficiently over the shaping result without creating intermediary objects.
59
+ def each_result
60
+ return enum_for(__method__) unless block_given?
61
+
62
+ length_ptr = FFI::MemoryPointer.new(:uint)
63
+ infos_ptr = HarfBuzz::C.hb_buffer_get_glyph_infos(@ptr, length_ptr)
64
+ length_ptr = FFI::MemoryPointer.new(:uint)
65
+ positions_ptr = HarfBuzz::C.hb_buffer_get_glyph_positions(@ptr, length_ptr)
66
+ length = length_ptr.read_uint
67
+
68
+ return if infos_ptr.null? || positions_ptr.null? || length.zero?
69
+
70
+ last_info_cluster_offset = (length - 1) * GLYPH_INFO_SIZE + GLYPH_INFO_CLUSTER_OFFSET
71
+ i = 0
72
+ while i < length
73
+ info_offset = i * GLYPH_INFO_SIZE
74
+ pos_offset = i * GLYPH_POS_SIZE
75
+
76
+ glyph_id = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CODEPOINT_OFFSET)
77
+ cluster = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CLUSTER_OFFSET)
78
+
79
+ next_cluster = nil
80
+ tmp_offset = info_offset + GLYPH_INFO_CLUSTER_OFFSET + GLYPH_INFO_SIZE
81
+ while tmp_offset <= last_info_cluster_offset &&
82
+ (next_cluster = infos_ptr.get_uint32(tmp_offset)) == cluster
83
+ tmp_offset += GLYPH_INFO_SIZE
84
+ next_cluster = nil
85
+ end
86
+
87
+ x_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_XADVANCE_OFFSET)
88
+ y_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_YADVANCE_OFFSET)
89
+ x_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_XOFFSET_OFFSET)
90
+ y_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_YOFFSET_OFFSET)
91
+
92
+ yield(glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset)
93
+
94
+ i += 1
95
+ end
96
+
97
+ self
98
+ end
99
+ end
100
+ end
101
+
39
102
  module HexaPDF
40
103
  module Layout
41
104
 
@@ -44,23 +107,33 @@ module HexaPDF
44
107
  # This class is used to perform text shaping, i.e. changing the position of glyphs (e.g. for
45
108
  # kerning) or substituting one or more glyphs for other glyphs (e.g. for ligatures).
46
109
  #
47
- # Status of the implementation:
110
+ # The class contains two shaping engines: A very limited custom one and one based on
111
+ # HarfBuzz. Which one is used for shaping can be selected via the Style#shaping_engine property.
48
112
  #
49
- # * All text shaping functionality possible for Type1 fonts is implemented, i.e. kerning and
50
- # ligature substitution.
113
+ # The custom implementation is always used for Type1 fonts and supports kerning and ligature
114
+ # substitution. It also supports the 'kern' table for TrueType fonts if HarfBuzz is not used.
51
115
  #
52
- # * For TrueType fonts only kerning via the 'kern' table is implemented.
116
+ # For complex scripts or the need of special font features it is recommended to use the shaping
117
+ # engine based on HarfBuzz, even though it is slightly slower. For it to work the
118
+ # +harfbuzz-ruby+ gem needs to be installed. OpenType features can be activated and deactivated
119
+ # using Style#font_features.
53
120
  class TextShaper
54
121
 
55
- # Shapes the given text fragment in-place.
122
+ # Shapes the given text fragment. Returns either the in-place modified fragment or, for
123
+ # complex shaping, an array of fragments.
56
124
  #
57
- # The following shaping options, retrieved from the text fragment's Style#font_features, are
58
- # supported:
59
- #
60
- # :kern:: Pair-wise kerning.
61
- # :liga:: Ligature substitution.
125
+ # The style properties Style#shaping_engine, Style#font_features, Style#font_script,
126
+ # Style#language and Style#direction are used for shaping.
62
127
  def shape_text(text_fragment)
63
128
  font = text_fragment.style.font
129
+ if text_fragment.style.shaping_engine == :harfbuzz && font.font_type == :TrueType
130
+ unless HARFBUZZ_AVAILABLE
131
+ raise HexaPDF::Error, "Shaping engine harfbuzz required but the needed Rubygem " \
132
+ "harfbuzz-ruby is not available"
133
+ end
134
+ return harfbuzz_shape_text(text_fragment)
135
+ end
136
+
64
137
  if text_fragment.style.font_features[:liga] && font.wrapped_font.features.include?(:liga)
65
138
  if font.font_type == :Type1
66
139
  process_type1_ligatures(text_fragment)
@@ -76,11 +149,90 @@ module HexaPDF
76
149
  end
77
150
  text_fragment.clear_cache
78
151
  end
152
+
79
153
  text_fragment
80
154
  end
81
155
 
82
156
  private
83
157
 
158
+ # Shapes the text fragment with HarfBuzz.
159
+ def harfbuzz_shape_text(text_fragment, text = nil)
160
+ text ||= text_fragment.items.map(&:str).join
161
+ style = text_fragment.style
162
+
163
+ # Cache the used main Harfbuzz font objects
164
+ hb_font = style.font.pdf_object.document.cache('harfbuzz', style.font.filename) do
165
+ blob = HarfBuzz::Blob.from_file!(style.font.filename)
166
+ face = HarfBuzz::Face.new(blob, 0)
167
+ HarfBuzz::Font.new(face)
168
+ end
169
+
170
+ # Prepare the buffer and then shape the text. We are using cluster level 1 as this is the
171
+ # recommended level.
172
+ buffer = HarfBuzz::Buffer.new
173
+ buffer.add_utf8(text)
174
+ buffer.cluster_level = 1
175
+ buffer.direction = style.direction
176
+ buffer.script = style.font_script if style.font_script?
177
+ buffer.language = style.language if style.language?
178
+ buffer.guess_segment_properties
179
+ HarfBuzz.shape(hb_font, buffer, HarfBuzz::Feature.from_hash(style.font_features))
180
+
181
+ # Prepare the iteration over the shaping result. The final output will either be
182
+ # +text_fragment+ (no non-zero y_offsets) or +result+ containing at least two TextFragment
183
+ # instances.
184
+ result = nil
185
+ font = style.font
186
+ fragment = text_fragment
187
+ fragment.clear_cache
188
+ items = text_fragment.items.clear
189
+ last_cluster = nil
190
+ last_y_offset = 0
191
+ buffer.each_result do |glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset|
192
+ advance = (x_advance - x_offset) * font.scaling_factor
193
+
194
+ # 1. Determine the source characters for each glyph via their cluster numbers. If two or
195
+ # more glyphs have the same cluster number, the first gets the resulting string while the
196
+ # rest map to an empty string. Otherwise copying from the PDF would result in multiple
197
+ # copies of the resulting string.
198
+ str = (cluster == last_cluster ? '' : text.byteslice(cluster...(next_cluster || text.bytesize)))
199
+
200
+ # 2. Handle invalid glyphs with id=0 by mapping them to an InvalidGlyph instance
201
+ if glyph_id.zero?
202
+ glyph = font.decode_codepoint(str.ord)
203
+ advance = glyph.width
204
+ else
205
+ glyph = font.glyph(glyph_id, str)
206
+ end
207
+
208
+ # 3. Handle differing y_offsets by creating TextFragment instances with appropriate text
209
+ # rise properties.
210
+ if y_offset != last_y_offset
211
+ (result ||= []) << fragment
212
+ items = []
213
+ if y_offset.zero?
214
+ fragment = text_fragment.dup_attributes(items)
215
+ else
216
+ fragment = TextFragment.new(items, style.dup, properties: text_fragment.properties)
217
+ fragment.style.text_rise += y_offset * font.scaling_factor * fragment.style.font_size *
218
+ fragment.style.font.pdf_object.glyph_scaling_factor
219
+ end
220
+ end
221
+
222
+ # 4. Handle the correct x-positioning using x_offset. Also addjust the horizontal advance
223
+ # based on the glyph's fixed advance width as well as x_advance and x_offset (via
224
+ # +advance+).
225
+ items << -x_offset * font.scaling_factor unless x_offset.zero?
226
+ items << glyph
227
+ items << glyph.width - advance if glyph.width - advance != 0
228
+
229
+ last_cluster = cluster
230
+ last_y_offset = y_offset
231
+ end
232
+
233
+ result ? result.append(fragment) : text_fragment
234
+ end
235
+
84
236
  # Processes the text fragment and substitutes ligatures.
85
237
  def process_type1_ligatures(text_fragment)
86
238
  items = text_fragment.items
@@ -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
@@ -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
@@ -270,7 +270,7 @@ module HexaPDF
270
270
  str << ">>"
271
271
  end
272
272
 
273
- STRING_ESCAPE_MAP = {"(" => "\\(", ")" => "\\)", "\\" => "\\\\", "\r" => "\\r"}.freeze # :nodoc:
273
+ STRING_ESCAPE_MAP = {"(" => "\\(", ")" => "\\)", "\\" => "\\\\", "\r" => "\\r", "\n" => "\\n"}.freeze # :nodoc:
274
274
 
275
275
  # Serializes a String object.
276
276
  #
@@ -287,7 +287,7 @@ module HexaPDF
287
287
  else
288
288
  obj.b
289
289
  end
290
- obj.gsub!(/[()\\\r]/n, STRING_ESCAPE_MAP)
290
+ obj.gsub!(/[()\\\r\n]/n, STRING_ESCAPE_MAP)
291
291
  "(#{obj})"
292
292
  end
293
293
 
@@ -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
@@ -0,0 +1,185 @@
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
+ require 'hexapdf/serializer'
38
+ require 'set'
39
+
40
+ module HexaPDF
41
+ module Task
42
+
43
+ # Task for importing pages from another document that preserves the visual appearance.
44
+ #
45
+ # It takes care of
46
+ #
47
+ # * importing the specified pages with all associated objects,
48
+ # * handling optional content groups and their default state,
49
+ # * and merging form fields.
50
+ #
51
+ # Note that the /Order, /AS and /Locked fields of the default optional content configuration
52
+ # dictionary are not preserved.
53
+ #
54
+ # Example:
55
+ #
56
+ # doc.task(:import_pages, source: source_doc, pages: [1..-2])
57
+ module ImportPages
58
+
59
+ # Performs the necessary steps to import the pages from the +source+ docment into the target
60
+ # document +doc+. Returns the imported pages.
61
+ #
62
+ # +source+::
63
+ # Specifies the source PDF document from which the pages should be imported.
64
+ #
65
+ # +pages+::
66
+ # Specifies the pages that should be imported. The argument has to be one of the
67
+ # following:
68
+ #
69
+ # +:all+:: Imports all pages from the +source+ document.
70
+ # Integer value:: Imports the page with the given zero-based index.
71
+ # Range value:: Imports the pages from the zero-based range.
72
+ # Array of Integer or Range values:: Imports all specified pages or page ranges.
73
+ # Array of source page objects:: Imports the given pages.
74
+ #
75
+ # +:append+::
76
+ # Specifies whether the imported pages should be appended to the target document's page
77
+ # tree.
78
+ #
79
+ # +ocgs+::
80
+ # Specifies the handling of optional content groups:
81
+ #
82
+ # +:preserve+:: Preserve the on/off state for all used OCGs.
83
+ # +:ignore+:: Ignore the on/off state.
84
+ #
85
+ # +:acro_form+::
86
+ # Specifies whether AcroForm fields should be merged into the target document.
87
+ #
88
+ # +:merge+:: Merge AcroForm fields using the MergeAcroForm task.
89
+ # +:ignore+:: Ignore AcroForm fields.
90
+ def self.call(doc, source:, pages: :all, append: true, ocgs: :preserve, acro_form: :merge)
91
+ # Retrieve all specified source pages
92
+ pages = if pages == :all
93
+ source.pages.each.to_a
94
+ elsif pages.kind_of?(Integer)
95
+ [source.pages[pages]]
96
+ elsif pages.kind_of?(Array) && pages[0].kind_of?(HexaPDF::Type::Page)
97
+ pages
98
+ else
99
+ result = Set.new
100
+ all_pages = source.pages.each.to_a
101
+ pages = [pages] unless pages.kind_of?(Array)
102
+ pages.each {|selector| result.merge(Array(all_pages[selector])) }
103
+ result
104
+ end
105
+
106
+ # Import the source pages and optionally append them to the target page tree
107
+ pages = pages.map do |page|
108
+ imported_page = doc.import(page)
109
+ doc.pages << imported_page if append
110
+ imported_page
111
+ end
112
+
113
+ doc.task(:merge_acro_form, source: source, pages: pages) if acro_form == :merge
114
+ preserve_ocgs(doc, source, pages) if ocgs == :preserve
115
+
116
+ pages
117
+ end
118
+
119
+ # Preserves the state of the OCGs found on +pages+ so that the visual appearance in the target
120
+ # document +doc+ is the same as in the +source+ document.
121
+ def self.preserve_ocgs(doc, source, pages)
122
+ # Find all OCGs used on all pages
123
+ ocgs = Set.new
124
+ process_ocg_or_ocmd = lambda do |obj|
125
+ if obj.type == :OCG
126
+ ocgs << obj
127
+ elsif obj.type == :OCMD
128
+ ocgs.merge(obj[:OCGs].to_ary)
129
+ end
130
+ end
131
+ seen_resources = {}
132
+ pages.each do |page|
133
+ unless seen_resources[page.resources] # handle case when pages share the resources dict
134
+ page.resources[:Properties]&.each do |name, obj|
135
+ next unless obj
136
+ process_ocg_or_ocmd.call(obj)
137
+ end
138
+
139
+ page.resources[:XObject]&.each do |name, obj|
140
+ process_ocg_or_ocmd.call(obj[:OC]) if obj.key?(:OC)
141
+ end
142
+ end
143
+
144
+ page.each_annotation do |annot|
145
+ process_ocg_or_ocmd.call(annot[:OC]) if annot.key?(:OC)
146
+ end
147
+
148
+ seen_resources[page.resources] = true
149
+ end
150
+
151
+ return if ocgs.empty?
152
+
153
+ # Add all found OCGs to the optional content properties dictionary
154
+ ocp = doc.optional_content
155
+ ocgs.each {|ocg| ocp.add_ocg(ocg) }
156
+
157
+ # Create a mapping from source OCGs to target OCGs and vice-versa
158
+ source_ocg = {}
159
+ target_ocg = {}
160
+ source.optional_content.ocgs.each do |ocg|
161
+ imported_ocg = doc.import(ocg)
162
+ next unless ocgs.include?(imported_ocg)
163
+ source_ocg[imported_ocg] = ocg
164
+ target_ocg[ocg] = imported_ocg
165
+ end
166
+
167
+ # Ensure the initial state of the OCGs is correct
168
+ source_config = source.optional_content.default_configuration
169
+ target_config = ocp.default_configuration
170
+ ocgs.each do |ocg|
171
+ target_config.ocg_state(ocg, source_config.ocg_state(source_ocg[ocg]))
172
+ end
173
+
174
+ # Copy radio button groups from the source document, removing unknown OCGs from them
175
+ source_config[:RBGroups]&.each do |array|
176
+ result = array.map {|ocg| target_ocg[ocg] }.compact
177
+ next if result.empty?
178
+ (target_config[:RBGroups] ||= []) << result
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+ 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
@@ -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
data/lib/hexapdf/task.rb CHANGED
@@ -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
@@ -66,6 +66,7 @@ module HexaPDF
66
66
  autoload(:Dereference, 'hexapdf/task/dereference')
67
67
  autoload(:PDFA, 'hexapdf/task/pdfa')
68
68
  autoload(:MergeAcroForm, 'hexapdf/task/merge_acro_form')
69
+ autoload(:ImportPages, 'hexapdf/task/import_pages')
69
70
 
70
71
  end
71
72