hexapdf 0.32.2 → 0.34.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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +9 -0
  4. data/examples/002-graphics.rb +15 -17
  5. data/examples/003-arcs.rb +9 -9
  6. data/examples/009-text_layouter_alignment.rb +1 -1
  7. data/examples/010-text_layouter_inline_boxes.rb +2 -2
  8. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  9. data/examples/012-text_layouter_styling.rb +7 -7
  10. data/examples/013-text_layouter_shapes.rb +1 -1
  11. data/examples/014-text_in_polygon.rb +1 -1
  12. data/examples/015-boxes.rb +8 -7
  13. data/examples/016-frame_automatic_box_placement.rb +2 -2
  14. data/examples/017-frame_text_flow.rb +2 -1
  15. data/examples/018-composer.rb +1 -1
  16. data/examples/020-column_box.rb +2 -1
  17. data/examples/025-table_box.rb +46 -0
  18. data/examples/026-optional_content.rb +55 -0
  19. data/examples/027-composer_optional_content.rb +83 -0
  20. data/lib/hexapdf/cli/command.rb +12 -3
  21. data/lib/hexapdf/cli/fonts.rb +1 -1
  22. data/lib/hexapdf/cli/form.rb +5 -5
  23. data/lib/hexapdf/cli/inspect.rb +5 -7
  24. data/lib/hexapdf/composer.rb +106 -53
  25. data/lib/hexapdf/configuration.rb +65 -40
  26. data/lib/hexapdf/content/canvas.rb +445 -267
  27. data/lib/hexapdf/content/color_space.rb +72 -25
  28. data/lib/hexapdf/content/graphic_object/arc.rb +57 -24
  29. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +66 -23
  30. data/lib/hexapdf/content/graphic_object/geom2d.rb +47 -6
  31. data/lib/hexapdf/content/graphic_object/solid_arc.rb +58 -36
  32. data/lib/hexapdf/content/graphic_object.rb +6 -7
  33. data/lib/hexapdf/content/graphics_state.rb +54 -45
  34. data/lib/hexapdf/content/operator.rb +54 -54
  35. data/lib/hexapdf/content/parser.rb +2 -2
  36. data/lib/hexapdf/content/processor.rb +15 -15
  37. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  38. data/lib/hexapdf/content.rb +5 -0
  39. data/lib/hexapdf/dictionary.rb +7 -5
  40. data/lib/hexapdf/dictionary_fields.rb +43 -16
  41. data/lib/hexapdf/digital_signature/cms_handler.rb +2 -2
  42. data/lib/hexapdf/digital_signature/handler.rb +1 -1
  43. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +2 -3
  44. data/lib/hexapdf/digital_signature/signature.rb +6 -6
  45. data/lib/hexapdf/digital_signature/signatures.rb +13 -12
  46. data/lib/hexapdf/digital_signature/signing/default_handler.rb +14 -5
  47. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +2 -4
  48. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +4 -4
  49. data/lib/hexapdf/digital_signature/signing.rb +4 -0
  50. data/lib/hexapdf/digital_signature/verification_result.rb +3 -4
  51. data/lib/hexapdf/digital_signature.rb +7 -2
  52. data/lib/hexapdf/document/destinations.rb +12 -11
  53. data/lib/hexapdf/document/files.rb +1 -1
  54. data/lib/hexapdf/document/fonts.rb +1 -1
  55. data/lib/hexapdf/document/layout.rb +170 -39
  56. data/lib/hexapdf/document/pages.rb +4 -3
  57. data/lib/hexapdf/document.rb +96 -55
  58. data/lib/hexapdf/encryption/aes.rb +5 -5
  59. data/lib/hexapdf/encryption/arc4.rb +1 -1
  60. data/lib/hexapdf/encryption/fast_aes.rb +2 -2
  61. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  62. data/lib/hexapdf/encryption/identity.rb +1 -1
  63. data/lib/hexapdf/encryption/ruby_aes.rb +11 -21
  64. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  65. data/lib/hexapdf/encryption/security_handler.rb +31 -24
  66. data/lib/hexapdf/encryption/standard_security_handler.rb +45 -36
  67. data/lib/hexapdf/encryption.rb +7 -2
  68. data/lib/hexapdf/error.rb +18 -0
  69. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  70. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  71. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  72. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  73. data/lib/hexapdf/filter/pass_through.rb +1 -1
  74. data/lib/hexapdf/filter/predictor.rb +1 -1
  75. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  76. data/lib/hexapdf/filter.rb +55 -6
  77. data/lib/hexapdf/font/cmap/parser.rb +2 -2
  78. data/lib/hexapdf/font/cmap.rb +1 -1
  79. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  80. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  81. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +2 -2
  82. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  83. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  84. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +3 -3
  85. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  86. data/lib/hexapdf/font/invalid_glyph.rb +3 -0
  87. data/lib/hexapdf/font/true_type_wrapper.rb +17 -4
  88. data/lib/hexapdf/font/type1_wrapper.rb +19 -4
  89. data/lib/hexapdf/font_loader/from_configuration.rb +5 -2
  90. data/lib/hexapdf/font_loader/from_file.rb +5 -5
  91. data/lib/hexapdf/font_loader/standard14.rb +3 -3
  92. data/lib/hexapdf/font_loader.rb +3 -0
  93. data/lib/hexapdf/image_loader/jpeg.rb +2 -2
  94. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  95. data/lib/hexapdf/image_loader/png.rb +2 -2
  96. data/lib/hexapdf/image_loader.rb +1 -1
  97. data/lib/hexapdf/importer.rb +13 -0
  98. data/lib/hexapdf/layout/box.rb +32 -5
  99. data/lib/hexapdf/layout/box_fitter.rb +2 -2
  100. data/lib/hexapdf/layout/column_box.rb +20 -5
  101. data/lib/hexapdf/layout/frame.rb +53 -18
  102. data/lib/hexapdf/layout/image_box.rb +5 -0
  103. data/lib/hexapdf/layout/inline_box.rb +21 -9
  104. data/lib/hexapdf/layout/list_box.rb +50 -20
  105. data/lib/hexapdf/layout/page_style.rb +6 -5
  106. data/lib/hexapdf/layout/style.rb +64 -9
  107. data/lib/hexapdf/layout/table_box.rb +684 -0
  108. data/lib/hexapdf/layout/text_box.rb +12 -3
  109. data/lib/hexapdf/layout/text_fragment.rb +29 -3
  110. data/lib/hexapdf/layout/text_layouter.rb +32 -8
  111. data/lib/hexapdf/layout.rb +1 -0
  112. data/lib/hexapdf/name_tree_node.rb +1 -1
  113. data/lib/hexapdf/number_tree_node.rb +1 -1
  114. data/lib/hexapdf/object.rb +18 -7
  115. data/lib/hexapdf/parser.rb +7 -7
  116. data/lib/hexapdf/pdf_array.rb +1 -1
  117. data/lib/hexapdf/rectangle.rb +1 -1
  118. data/lib/hexapdf/reference.rb +1 -1
  119. data/lib/hexapdf/revision.rb +1 -1
  120. data/lib/hexapdf/revisions.rb +3 -3
  121. data/lib/hexapdf/serializer.rb +15 -15
  122. data/lib/hexapdf/stream.rb +5 -4
  123. data/lib/hexapdf/tokenizer.rb +14 -14
  124. data/lib/hexapdf/type/acro_form/appearance_generator.rb +22 -22
  125. data/lib/hexapdf/type/acro_form/button_field.rb +1 -1
  126. data/lib/hexapdf/type/acro_form/choice_field.rb +1 -1
  127. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  128. data/lib/hexapdf/type/acro_form/form.rb +1 -1
  129. data/lib/hexapdf/type/acro_form/signature_field.rb +4 -4
  130. data/lib/hexapdf/type/acro_form/text_field.rb +1 -1
  131. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  132. data/lib/hexapdf/type/acro_form.rb +1 -1
  133. data/lib/hexapdf/type/action.rb +1 -1
  134. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  135. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  136. data/lib/hexapdf/type/actions/launch.rb +1 -1
  137. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  138. data/lib/hexapdf/type/actions/uri.rb +1 -1
  139. data/lib/hexapdf/type/actions.rb +2 -1
  140. data/lib/hexapdf/type/annotation.rb +3 -3
  141. data/lib/hexapdf/type/annotations/link.rb +1 -1
  142. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  143. data/lib/hexapdf/type/annotations/text.rb +2 -3
  144. data/lib/hexapdf/type/annotations/widget.rb +2 -2
  145. data/lib/hexapdf/type/annotations.rb +1 -1
  146. data/lib/hexapdf/type/catalog.rb +11 -2
  147. data/lib/hexapdf/type/cid_font.rb +18 -4
  148. data/lib/hexapdf/type/embedded_file.rb +1 -1
  149. data/lib/hexapdf/type/file_specification.rb +2 -2
  150. data/lib/hexapdf/type/font_descriptor.rb +1 -1
  151. data/lib/hexapdf/type/font_simple.rb +2 -2
  152. data/lib/hexapdf/type/font_type0.rb +3 -3
  153. data/lib/hexapdf/type/font_type3.rb +1 -1
  154. data/lib/hexapdf/type/form.rb +76 -6
  155. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  156. data/lib/hexapdf/type/icon_fit.rb +1 -1
  157. data/lib/hexapdf/type/image.rb +1 -1
  158. data/lib/hexapdf/type/info.rb +1 -1
  159. data/lib/hexapdf/type/mark_information.rb +1 -1
  160. data/lib/hexapdf/type/names.rb +2 -2
  161. data/lib/hexapdf/type/object_stream.rb +2 -1
  162. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  163. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  164. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  165. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  166. data/lib/hexapdf/type/outline.rb +1 -1
  167. data/lib/hexapdf/type/outline_item.rb +1 -1
  168. data/lib/hexapdf/type/page.rb +46 -21
  169. data/lib/hexapdf/type/page_label.rb +5 -9
  170. data/lib/hexapdf/type/page_tree_node.rb +1 -1
  171. data/lib/hexapdf/type/resources.rb +1 -1
  172. data/lib/hexapdf/type/trailer.rb +2 -2
  173. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  174. data/lib/hexapdf/type/xref_stream.rb +2 -2
  175. data/lib/hexapdf/type.rb +4 -0
  176. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -2
  177. data/lib/hexapdf/version.rb +1 -1
  178. data/lib/hexapdf/writer.rb +4 -4
  179. data/lib/hexapdf/xref_section.rb +2 -2
  180. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +11 -1
  181. data/test/hexapdf/content/graphic_object/test_geom2d.rb +7 -0
  182. data/test/hexapdf/content/test_canvas.rb +49 -1
  183. data/test/hexapdf/digital_signature/test_signatures.rb +22 -0
  184. data/test/hexapdf/document/test_files.rb +2 -2
  185. data/test/hexapdf/document/test_layout.rb +105 -2
  186. data/test/hexapdf/document/test_pages.rb +6 -6
  187. data/test/hexapdf/encryption/test_security_handler.rb +12 -11
  188. data/test/hexapdf/encryption/test_standard_security_handler.rb +35 -23
  189. data/test/hexapdf/font/test_true_type_wrapper.rb +18 -1
  190. data/test/hexapdf/font/test_type1_wrapper.rb +15 -1
  191. data/test/hexapdf/layout/test_box.rb +14 -5
  192. data/test/hexapdf/layout/test_column_box.rb +65 -21
  193. data/test/hexapdf/layout/test_frame.rb +27 -15
  194. data/test/hexapdf/layout/test_image_box.rb +4 -0
  195. data/test/hexapdf/layout/test_inline_box.rb +17 -3
  196. data/test/hexapdf/layout/test_list_box.rb +84 -33
  197. data/test/hexapdf/layout/test_page_style.rb +3 -2
  198. data/test/hexapdf/layout/test_style.rb +60 -0
  199. data/test/hexapdf/layout/test_table_box.rb +728 -0
  200. data/test/hexapdf/layout/test_text_box.rb +26 -0
  201. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  202. data/test/hexapdf/layout/test_text_layouter.rb +36 -5
  203. data/test/hexapdf/test_composer.rb +10 -0
  204. data/test/hexapdf/test_dictionary.rb +10 -0
  205. data/test/hexapdf/test_dictionary_fields.rb +4 -1
  206. data/test/hexapdf/test_document.rb +5 -0
  207. data/test/hexapdf/test_filter.rb +8 -0
  208. data/test/hexapdf/test_importer.rb +9 -0
  209. data/test/hexapdf/test_object.rb +16 -5
  210. data/test/hexapdf/test_stream.rb +7 -0
  211. data/test/hexapdf/test_writer.rb +3 -3
  212. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +13 -5
  213. data/test/hexapdf/type/acro_form/test_form.rb +4 -3
  214. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  215. data/test/hexapdf/type/test_catalog.rb +11 -0
  216. data/test/hexapdf/type/test_form.rb +119 -0
  217. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  218. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  219. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  220. data/test/hexapdf/type/test_page.rb +20 -6
  221. metadata +28 -8
@@ -25,6 +25,14 @@ describe HexaPDF::Layout::TextBox do
25
25
  end
26
26
  end
27
27
 
28
+ it "returns the text contents as string" do
29
+ doc = HexaPDF::Document.new
30
+ font = doc.fonts.add("Times")
31
+ box = create_box([HexaPDF::Layout::TextFragment.create('Test ', font: font), @inline_box,
32
+ HexaPDF::Layout::TextFragment.create('here', font: font)])
33
+ assert_equal('Test here', box.text)
34
+ end
35
+
28
36
  describe "fit" do
29
37
  it "fits into a rectangular area" do
30
38
  box = create_box([@inline_box] * 5, style: {padding: 10})
@@ -107,6 +115,14 @@ describe HexaPDF::Layout::TextBox do
107
115
  assert_equal([nil, box], box.split(100, 100, @frame))
108
116
  end
109
117
 
118
+ it "works if the box fits exactly (+/- float divergence)" do
119
+ box = create_box([@inline_box] * 5)
120
+ box.fit(50, 10, @frame)
121
+ box.instance_variable_set(:@width, 50.00000000006)
122
+ box.instance_variable_set(:@height, 10.00000000003)
123
+ assert_equal([box], box.split(50, 10, @frame))
124
+ end
125
+
110
126
  it "splits the box if necessary when using non-flowing text" do
111
127
  box = create_box([@inline_box] * 10)
112
128
  boxes = box.split(50, 10, @frame)
@@ -125,6 +141,16 @@ describe HexaPDF::Layout::TextBox do
125
141
  assert_equal(box, boxes[0])
126
142
  assert_equal(5, boxes[1].instance_variable_get(:@items).length)
127
143
  end
144
+
145
+ it "correctly handles text indentation for split boxes" do
146
+ [{}, {position: :flow}].each do |styles|
147
+ box = create_box([@inline_box] * 202, style: {text_indent: 50, **styles})
148
+ boxes = box.split(100, 100, @frame)
149
+ assert_equal(107, boxes[1].instance_variable_get(:@items).length)
150
+ boxes = boxes[1].split(100, 100, @frame)
151
+ assert_equal(7, boxes[1].instance_variable_get(:@items).length)
152
+ end
153
+ end
128
154
  end
129
155
 
130
156
  describe "draw" do
@@ -51,6 +51,10 @@ describe HexaPDF::Layout::TextFragment do
51
51
  end
52
52
  end
53
53
 
54
+ it "returns the text value of the items as string" do
55
+ assert_equal("Hal lo\u{00a0}d\n", setup_fragment(@font.decode_utf8("Hal lo\u{00a0}d\n")).text)
56
+ end
57
+
54
58
  it "allows duplicating with only its attributes while also setting new items" do
55
59
  setup_fragment([20])
56
60
  @fragment.properties['key'] = :value
@@ -392,6 +396,35 @@ describe HexaPDF::Layout::TextFragment do
392
396
  end
393
397
  end
394
398
 
399
+ describe "fill_horizontal!" do
400
+ before do
401
+ @fragment = HexaPDF::Layout::TextFragment.create('ab', fill_horizontal: 1, font: @font)
402
+ end
403
+
404
+ it "returns the fragment if the given width is too small" do
405
+ assert_same(@fragment, @fragment.fill_horizontal!(0.1))
406
+ end
407
+
408
+ it "repeats all items of the fragment" do
409
+ fragment = @fragment.fill_horizontal!(@fragment.width * 2)
410
+ assert_equal([*(@fragment.items * 2), 0], fragment.items)
411
+ assert_in_delta(@fragment.width * 2, fragment.width)
412
+ end
413
+
414
+ it "adds, after repeating, items from the start of the fragment to fill the available space" do
415
+ fragment = @fragment.fill_horizontal!(90)
416
+ assert_equal([*(@fragment.items * 9), @fragment.items[0], 3.3333333333332673], fragment.items)
417
+ assert_in_delta(90, fragment.width)
418
+ end
419
+
420
+ it "sets the character spacing correctly to account for the remaining space after filling with items" do
421
+ fragment = @fragment.fill_horizontal!(90)
422
+ refute_same(@fragment.style, fragment.style)
423
+ assert_equal(0.033333333333332674, fragment.style.character_spacing)
424
+ assert_in_delta(90, fragment.width)
425
+ end
426
+ end
427
+
395
428
  it "can be inspected" do
396
429
  frag = setup_fragment(@font.decode_utf8("H") << 5)
397
430
  assert_match(/:H/, frag.inspect)
@@ -262,7 +262,7 @@ module CommonLineWrappingTests
262
262
  end
263
263
 
264
264
  it "handles prohibited breakpoint penalties with non-zero width" do
265
- item = boxes(20).first
265
+ item = boxes(20).first.item
266
266
  result = call(boxes(70) + [glue(10)] + boxes(10) + [penalty(5000, item)] + boxes(30))
267
267
  assert_line_wrapping(result, [70, 60])
268
268
  end
@@ -295,11 +295,34 @@ module CommonLineWrappingTests
295
295
  assert_equal(2, lines.count)
296
296
  end
297
297
 
298
+ it "handles items with fill_horizontal correctly" do
299
+ doc = HexaPDF::Document.new
300
+ font = doc.fonts.add("Times")
301
+ box1 = HexaPDF::Layout::TextLayouter::Box.new(
302
+ HexaPDF::Layout::TextFragment.create('.', font: font, fill_horizontal: 1)
303
+ )
304
+ box2 = HexaPDF::Layout::TextLayouter::Box.new(
305
+ HexaPDF::Layout::TextFragment.create('.', font: font, fill_horizontal: 2)
306
+ )
307
+ items = [box1, *boxes(10), box2]
308
+ rest, lines = call(items, 40)
309
+ assert_equal(0, rest.size)
310
+ assert_equal(1, lines.size)
311
+
312
+ line = lines.first
313
+ refute_same(items[0].item, line.items[0])
314
+ assert_same(items[1].item, line.items[1])
315
+ refute_same(items[2].item, line.items[2])
316
+ assert_equal(10, line.items[0].width)
317
+ assert_equal(10, line.items[1].width)
318
+ assert_equal(20, line.items[2].width)
319
+ end
298
320
  end
299
321
 
300
322
  describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
301
323
  before do
302
324
  @obj = HexaPDF::Layout::TextLayouter::SimpleLineWrapping
325
+ @mock_frame = nil
303
326
  end
304
327
 
305
328
  describe "fixed width wrapping" do
@@ -308,7 +331,9 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
308
331
  def call(items, width = 100, &block)
309
332
  lines = []
310
333
  block ||= proc { true }
311
- rest = @obj.call(items, proc { width }) {|line, item| lines << line; block.call(line, item) }
334
+ rest = @obj.call(items, proc { width }, @mock_frame) do |line, item|
335
+ lines << line; block.call(line, item)
336
+ end
312
337
  [rest, lines]
313
338
  end
314
339
  end
@@ -319,7 +344,9 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
319
344
  def call(items, width = 100, &block)
320
345
  lines = []
321
346
  block ||= proc { true }
322
- rest = @obj.call(items, proc {|_| width }) {|line, i| lines << line; block.call(line, i) }
347
+ rest = @obj.call(items, proc {|_| width }, @mock_frame) do |line, i|
348
+ lines << line; block.call(line, i)
349
+ end
323
350
  [rest, lines]
324
351
  end
325
352
 
@@ -334,7 +361,7 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
334
361
  end
335
362
  end
336
363
  lines = []
337
- rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block) do |line|
364
+ rest = @obj.call(boxes([20, 10], [10, 10], [20, 15], [40, 10]), width_block, @mock_frame) do |line|
338
365
  height += line.height
339
366
  lines << line
340
367
  true
@@ -357,7 +384,7 @@ describe HexaPDF::Layout::TextLayouter::SimpleLineWrapping do
357
384
  lines = []
358
385
  item = HexaPDF::Layout::InlineBox.create(width: 20, height: 10) {}
359
386
  items = boxes([20, 10]) + [penalty(0, item)] + boxes([40, 15])
360
- rest = @obj.call(items, width_block) do |line|
387
+ rest = @obj.call(items, width_block, @mock_frame) do |line|
361
388
  height += line.height
362
389
  lines << line
363
390
  true
@@ -419,6 +446,10 @@ describe HexaPDF::Layout::TextLayouter do
419
446
  assert(result.remaining_items.empty?)
420
447
  assert_equal(:success, result.status)
421
448
  end
449
+
450
+ result = @layouter.fit(boxes([20, 20], [20, 20], [20, 20], [20, 20]), 60, 200,
451
+ apply_first_text_indent: false)
452
+ assert_equal([60, 20], result.lines.map(&:width))
422
453
  end
423
454
 
424
455
  it "fits using a limited height" do
@@ -243,6 +243,16 @@ describe HexaPDF::Composer do
243
243
  [:restore_graphics_state]])
244
244
  end
245
245
 
246
+ it "returns the last drawn box" do
247
+ box = create_box(height: 400)
248
+ assert_same(box, @composer.draw_box(box))
249
+
250
+ box = create_box(height: 400)
251
+ split_box = create_box(height: 100)
252
+ box.define_singleton_method(:split) {|*| [box, split_box] }
253
+ assert_same(split_box, @composer.draw_box(box))
254
+ end
255
+
246
256
  it "raises an error if a box doesn't fit onto an empty page" do
247
257
  assert_raises(HexaPDF::Error) do
248
258
  @composer.draw_box(create_box(height: 800))
@@ -94,6 +94,16 @@ describe HexaPDF::Dictionary do
94
94
  obj = @test_class.new(nil)
95
95
  assert_equal(:MyType, obj.value[:Type])
96
96
  end
97
+
98
+ it "doesn't set the default values for required fields if the type class might be wrong" do
99
+ @test_class.define_type(:MyType)
100
+ obj = @test_class.new({})
101
+ assert_equal([], obj.value[:Array])
102
+ obj = @test_class.new({Type: :MyType})
103
+ assert_equal([], obj.value[:Array])
104
+ obj = @test_class.new({Type: :OtherType})
105
+ refute(obj.key?(:Array))
106
+ end
97
107
  end
98
108
 
99
109
  describe "[]" do
@@ -188,8 +188,11 @@ describe HexaPDF::DictionaryFields do
188
188
  ["D:19981223-08'00'", [1998, 12, 23, 00, 00, 00, "-08:00"]],
189
189
  ["D:199812-08'00'", [1998, 12, 01, 00, 00, 00, "-08:00"]],
190
190
  ["D:1998-08'00'", [1998, 01, 01, 00, 00, 00, "-08:00"]],
191
+ ["D:19981223195210Z", [1998, 12, 23, 19, 52, 10, "+00:00"]],
192
+ ["D:19981223195210Z00", [1998, 12, 23, 19, 52, 10, "+00:00"]],
193
+ ["D:19981223195210Z00'00", [1998, 12, 23, 19, 52, 10, "+00:00"]],
191
194
  ["D:19981223195210-08", [1998, 12, 23, 19, 52, 10, "-08:00"]], # missing '
192
- ["D:19981223195210-08'00", [1998, 12, 23, 19, 52, 10, "-08:00"]], # missing '
195
+ ["D:19981223195210-08'00", [1998, 12, 23, 19, 52, 10, "-08:00"]], # no trailing ', as per PDF 2.0
193
196
  ["D:19981223195210-54'00", [1998, 12, 23, 19, 52, 10, "-23:59:59"]], # TZ hour too large
194
197
  ["D:19981223195210+10'65", [1998, 12, 23, 19, 52, 10, "+11:05"]], # TZ min too large
195
198
  ["D:19982423195210-08'00'", [1998, 12, 23, 19, 52, 10, "-08:00"]], # months too large
@@ -240,6 +240,7 @@ describe HexaPDF::Document do
240
240
  assert_kind_of(@myclass2, @doc.wrap({Subtype: :Global, Test: "true"}))
241
241
  assert_kind_of(@myclass2, @doc.wrap({Type: :MyClass, S: :TheSecond}))
242
242
  assert_kind_of(@myclass, @doc.wrap({Type: :MyClass, Subtype: :TheThird}))
243
+ assert_kind_of(HexaPDF::Type::FontTrueType, @doc.wrap({Subtype: :TrueType, BaseFont: :Help}))
243
244
  end
244
245
 
245
246
  it "respects the given type/subtype arguments" do
@@ -560,6 +561,10 @@ describe HexaPDF::Document do
560
561
  assert_kind_of(HexaPDF::Type::Outline, @doc.outline)
561
562
  end
562
563
 
564
+ it "returns the optional content properties" do
565
+ assert_kind_of(HexaPDF::Type::OptionalContentProperties, @doc.optional_content)
566
+ end
567
+
563
568
  it "can be inspected and the output is not too large" do
564
569
  assert_match(/HexaPDF::Document:\d+/, @doc.inspect)
565
570
  end
@@ -12,6 +12,14 @@ describe HexaPDF::Filter do
12
12
  40.times { @str << [rand(2**32)].pack('N') }
13
13
  end
14
14
 
15
+ describe "source_from_proc" do
16
+ it "returns the whole string, once" do
17
+ fib = @obj.source_from_proc { @str }
18
+ assert_equal(@str, collector(fib))
19
+ assert_equal('', collector(fib))
20
+ end
21
+ end
22
+
15
23
  describe "source_from_string" do
16
24
  it "doesn't modify the given string" do
17
25
  str = @str.dup
@@ -40,6 +40,15 @@ describe HexaPDF::Importer do
40
40
  end
41
41
  end
42
42
 
43
+ describe "::copy" do
44
+ it "copies a complete object including references" do
45
+ obj1 = HexaPDF::Importer.copy(@dest, @obj)
46
+ obj2 = HexaPDF::Importer.copy(@dest, @obj)
47
+ refute_same(obj1, obj2)
48
+ refute_same(obj1[:ref], obj2[:ref])
49
+ end
50
+ end
51
+
43
52
  describe "import" do
44
53
  it "updates the associated document" do
45
54
  obj = @importer.import(@obj)
@@ -50,26 +50,37 @@ describe HexaPDF::Object do
50
50
  @doc = HexaPDF::Document.new
51
51
  end
52
52
 
53
- it "doesn't touch wrapped direct objects" do
53
+ it "makes values of wrapped direct objects also direct" do
54
54
  obj = HexaPDF::Object.new(5)
55
- assert_same(obj, HexaPDF::Object.make_direct(obj))
55
+ assert_same(obj, HexaPDF::Object.make_direct(obj, @doc))
56
+ obj = HexaPDF::Dictionary.new({a: 5, b: HexaPDF::Object.new(:a, oid: 3, document: @doc)})
57
+ assert_same(obj, HexaPDF::Object.make_direct(obj, @doc))
58
+ assert_equal(:a, obj[:b])
56
59
  end
57
60
 
58
61
  it "works for simple values" do
59
62
  obj = HexaPDF::Object.new(5, oid: 1, document: @doc)
60
- assert_same(5, HexaPDF::Object.make_direct(obj))
63
+ assert_same(5, HexaPDF::Object.make_direct(obj, @doc))
61
64
  end
62
65
 
63
66
  it "works for hashes" do
64
67
  obj = HexaPDF::Dictionary.new({a: 5, b: HexaPDF::Object.new(:a, oid: 3, document: @doc)},
65
68
  oid: 1, document: @doc)
66
- assert_equal({a: 5, b: :a}, HexaPDF::Object.make_direct(obj))
69
+ assert_equal({a: 5, b: :a}, HexaPDF::Object.make_direct(obj, @doc))
67
70
  end
68
71
 
69
72
  it "works for arrays" do
70
73
  obj = HexaPDF::PDFArray.new([:b, HexaPDF::Object.new(:a, oid: 3, document: @doc)],
71
74
  oid: 1, document: @doc)
72
- assert_equal([:b, :a], HexaPDF::Object.make_direct(obj))
75
+ assert_equal([:b, :a], HexaPDF::Object.make_direct(obj, @doc))
76
+ end
77
+
78
+ it "resolves references" do
79
+ @doc.add(:Test, oid: 1)
80
+ obj = HexaPDF::PDFArray.new([HexaPDF::Reference.new(1, 0)],
81
+ oid: 2, document: @doc)
82
+ assert_equal([:Test], HexaPDF::Object.make_direct(obj, @doc))
83
+ assert(@doc.object(1).null?)
73
84
  end
74
85
  end
75
86
 
@@ -31,6 +31,13 @@ describe HexaPDF::StreamData do
31
31
  end
32
32
 
33
33
  describe "fiber" do
34
+ it "returns a duplicate for a FiberDoubleForString source" do
35
+ source = HexaPDF::Filter.source_from_string("str")
36
+ fiber = HexaPDF::StreamData.new(source).fiber
37
+ assert_equal("str", fiber.resume)
38
+ refute_same(source, fiber)
39
+ end
40
+
34
41
  it "returns a fiber for a Proc source" do
35
42
  s = HexaPDF::StreamData.new(proc { :source })
36
43
  assert_equal(:source, s.fiber.resume)
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.32.2)>>
43
+ <</Producer(HexaPDF version 0.34.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.32.2)>>
75
+ <</Producer(HexaPDF version 0.34.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -214,7 +214,7 @@ describe HexaPDF::Writer do
214
214
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
215
215
  endobj
216
216
  5 0 obj
217
- <</Producer(HexaPDF version 0.32.2)>>
217
+ <</Producer(HexaPDF version 0.34.0)>>
218
218
  endobj
219
219
  4 0 obj
220
220
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
@@ -467,12 +467,20 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
467
467
  it "calculates the font size based on the rectangle height and border width" do
468
468
  @generator.create_appearances
469
469
  assert_operators(@widget[:AP][:N].stream,
470
- [:set_font_and_size, [:F1, 12.923875]],
470
+ [:set_font_and_size, [:F1, 13.235294]],
471
471
  range: 5)
472
472
  @widget.border_style(width: 2, color: :transparent)
473
473
  @generator.create_appearances
474
474
  assert_operators(@widget[:AP][:N].stream,
475
- [:set_font_and_size, [:F1, 11.487889]],
475
+ [:set_font_and_size, [:F1, 11.764706]],
476
+ range: 5)
477
+ end
478
+
479
+ it "shrinks the font size if necessary to fit the rectangle width" do
480
+ @field.field_value = "This is some arbitrary, long text"
481
+ @generator.create_appearances
482
+ assert_operators(@widget[:AP][:N].stream,
483
+ [:set_font_and_size, [:F1, 6.909955]],
476
484
  range: 5)
477
485
  end
478
486
 
@@ -573,7 +581,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
573
581
  assert_format("2, 3, #{style}, 0, \"E\", true",
574
582
  [[:set_device_rgb_non_stroking_color, [style % 2, 0.0, 0.0]],
575
583
  [:begin_text],
576
- [:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
584
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.183272]],
577
585
  [:show_text, [result]]], 6..9)
578
586
  end
579
587
  end
@@ -598,10 +606,10 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
598
606
  [:append_rectangle, [1, 1, 98, 9.25]],
599
607
  [:clip_path_non_zero],
600
608
  [:end_path],
601
- [:set_font_and_size, [:F1, 6.641436]],
609
+ [:set_font_and_size, [:F1, 6.801471]],
602
610
  [:set_device_rgb_non_stroking_color, [1.0, 0.0, 0.0]],
603
611
  [:begin_text],
604
- [:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
612
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.183272]],
605
613
  [:show_text, ["Text"]],
606
614
  [:end_text],
607
615
  [:restore_graphics_state],
@@ -42,11 +42,12 @@ describe HexaPDF::Type::AcroForm::Form do
42
42
  end
43
43
 
44
44
  it "finds the root fields" do
45
- @doc.pages.add[:Annots] = [{FT: :Tx}, {FT: :Tx2, Parent: {FT: :Tx3}}]
46
- @doc.pages.add[:Annots] = [{Subtype: :Widget}]
45
+ @doc.pages.add[:Annots] = [{Subtype: :Widget, Rect: [0, 0, 0, 0], FT: :Tx},
46
+ {Subtype: :Widget, Rect: [0, 0, 0, 0], FT: :Tx, Parent: {FT: :Tx}}]
47
+ @doc.pages.add[:Annots] = [{Subtype: :Widget, Rect: [0, 0, 0, 0]}]
47
48
  @doc.pages.add
48
49
 
49
- result = [{FT: :Tx}, {FT: :Tx3}]
50
+ result = [{Subtype: :Widget, Rect: [0, 0, 0, 0], FT: :Tx}, {FT: :Tx}]
50
51
  root_fields = @acro_form.find_root_fields
51
52
  assert_equal(result, root_fields.map(&:value))
52
53
  assert_kind_of(HexaPDF::Type::AcroForm::TextField, root_fields[0])
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/actions/set_ocg_state'
6
+
7
+ describe HexaPDF::Type::Actions::SetOCGState do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @action = HexaPDF::Type::Actions::SetOCGState.new({}, document: @doc)
11
+ @ocg = @doc.optional_content.add_ocg('Test')
12
+ end
13
+
14
+ describe "add_state_change" do
15
+ it "allows using Ruby-esque and PDF type names for the state change type" do
16
+ @action.add_state_change(:on, @ocg)
17
+ @action.add_state_change(:ON, @ocg)
18
+ @action.add_state_change(:off, @ocg)
19
+ @action.add_state_change(:OFF, @ocg)
20
+ @action.add_state_change(:toggle, @ocg)
21
+ @action.add_state_change(:Toggle, @ocg)
22
+ assert_equal([:ON, @ocg, :ON, @ocg, :OFF, @ocg, :OFF, @ocg, :Toggle, @ocg, :Toggle, @ocg],
23
+ @action[:State].value)
24
+ end
25
+
26
+ it "allows specifying more than one OCG" do
27
+ @action.add_state_change(:on, [@ocg, @doc.optional_content.add_ocg('Test2')])
28
+ assert_equal([:ON, @ocg, @doc.optional_content.ocg('Test2')], @action[:State].value)
29
+ end
30
+
31
+ it "raises an error if the provide state change type is invalid" do
32
+ assert_raises(ArgumentError) { @action.add_state_change(:unknown, nil) }
33
+ end
34
+
35
+ it "raises an error if an OCG specified via a string does not exist" do
36
+ error = assert_raises(HexaPDF::Error) { @action.add_state_change(:on, "Unknown") }
37
+ assert_match(/Invalid OCG.*Unknown.*specified/, error.message)
38
+ end
39
+ end
40
+ end
@@ -39,6 +39,17 @@ describe HexaPDF::Type::Catalog do
39
39
  assert_same(outline, @catalog.outline)
40
40
  end
41
41
 
42
+ it "uses or creates the optional content properties dictionary on access" do
43
+ @catalog[:OCProperties] = hash = {}
44
+ assert_equal(:XXOCProperties, @catalog.optional_content.type)
45
+ assert_same(hash, @catalog.optional_content.value)
46
+
47
+ @catalog.delete(:OCProperties)
48
+ oc = @catalog.optional_content
49
+ assert_equal([], oc[:OCGs])
50
+ assert_equal(:XXOCConfiguration, oc[:D].type)
51
+ end
52
+
42
53
  describe "acro_form" do
43
54
  it "returns an existing form object" do
44
55
  @catalog[:AcroForm] = :test
@@ -1,6 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
+ require 'stringio'
5
+ require 'tempfile'
4
6
  require 'hexapdf/document'
5
7
  require 'hexapdf/type/form'
6
8
 
@@ -85,6 +87,18 @@ describe HexaPDF::Type::Form do
85
87
  @form.process_contents(processor, original_resources: resources)
86
88
  assert_same(resources, processor.resources)
87
89
  end
90
+
91
+ it "uses the referenced content in case of a Reference XObject" do
92
+ @form[:Ref] = @doc.add({F: {}})
93
+ io = StringIO.new
94
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
95
+ @form[:Ref][:F].embed(io, name: 'test')
96
+ @form[:Ref][:Page] = 0
97
+
98
+ processor = HexaPDF::TestUtils::OperatorRecorder.new
99
+ @form.process_contents(processor)
100
+ assert_equal([[:set_line_width, [5]]], processor.recorded_ops)
101
+ end
88
102
  end
89
103
 
90
104
  describe "canvas" do
@@ -116,4 +130,109 @@ describe HexaPDF::Type::Form do
116
130
  assert_raises(HexaPDF::Error) { @form.canvas }
117
131
  end
118
132
  end
133
+
134
+ describe "reference_xobject?" do
135
+ it "returns true if the form is a reference XObject" do
136
+ refute(@form.reference_xobject?)
137
+ @form[:Ref] = {}
138
+ assert(@form.reference_xobject?)
139
+ end
140
+ end
141
+
142
+ describe "referenced_content" do
143
+ before do
144
+ @form[:BBox] = [10, 10, 110, 60]
145
+ @form[:Matrix] = [1, 0, 0, 1, 10, 20]
146
+ @form[:Ref] = @doc.add({F: {}})
147
+ @ref = @form[:Ref]
148
+ end
149
+
150
+ it "returns a Form XObject with the imported page from an embedded file" do
151
+ io = StringIO.new
152
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(io)
153
+ @ref[:F].embed(io, name: 'test.pdf')
154
+ @ref[:Page] = 0
155
+
156
+ ref_form = @form.referenced_content
157
+ refute_nil(ref_form)
158
+ assert_equal([10, 10, 110, 60], ref_form[:BBox].value)
159
+ assert_equal([1, 0, 0, 1, 10, 20], ref_form[:Matrix].value)
160
+ assert_equal("5 w\n", ref_form.contents)
161
+ end
162
+
163
+ it "returns a Form XObject with the imported page from an external file" do
164
+ file = Tempfile.new('hexapdf')
165
+ HexaPDF::Document.new.tap {|d| d.pages.add.canvas.line_width(5) }.write(file.path)
166
+ @ref[:F].path = file.path
167
+ @ref[:Page] = 0
168
+ assert_equal("5 w\n", @form.referenced_content.contents)
169
+ end
170
+
171
+ it "also works with a page label" do
172
+ file = Tempfile.new('hexapdf')
173
+ HexaPDF::Document.new.tap do |d|
174
+ d.pages.add
175
+ d.pages.add
176
+ d.pages.add.canvas.line_width(5)
177
+ d.pages.add
178
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test', start_number: 4)
179
+ end.write(file.path)
180
+ @ref[:F].path = file.path
181
+ @ref[:Page] = 'Test5'
182
+ assert_equal("5 w\n", @form.referenced_content.contents)
183
+ end
184
+
185
+ it "flattens printable annotations into the page's content stream" do
186
+ io = StringIO.new
187
+ HexaPDF::Document.new.tap do |d|
188
+ d.pages.add.canvas.line_width(5)
189
+ tf = d.acro_form(create: true).create_text_field('text')
190
+ widget = tf.create_widget(d.pages[0], Rect: [10, 10, 30, 30])
191
+ widget.border_style(color: "black")
192
+ widget = tf.create_widget(d.pages[0], Rect: [40, 10, 70, 30])
193
+ widget.border_style(color: "red")
194
+ tf.field_value = 't'
195
+ widget.unflag(:print)
196
+ end.write(io)
197
+ @ref[:F].embed(io, name: 'Test')
198
+ @ref[:Page] = 0
199
+ assert_equal(" q Q q 5 w\n Q q q\n1.0 0 0 1.0 10.0 10.0 cm\n/XO1 Do\nQ\n Q ",
200
+ @form.referenced_content.contents)
201
+ end
202
+
203
+ it "returns nil if the form is not a reference XObject" do
204
+ @form.delete(:Ref)
205
+ assert_nil(@form.referenced_content)
206
+ end
207
+
208
+ it "returns nil if the file is not embedded and not found" do
209
+ @ref[:F].path = '/tmp/non_existing_path'
210
+ assert_nil(@form.referenced_content)
211
+ end
212
+
213
+ it "returns nil if the page referenced by page number is not found" do
214
+ io = StringIO.new
215
+ HexaPDF::Document.new.tap {|d| d.pages.add; d.pages.add }.write(io)
216
+ @ref[:F].embed(io, name: 'test.pdf')
217
+ @ref[:Page] = 5
218
+ assert_nil(@form.referenced_content)
219
+ end
220
+
221
+ it "returns nil if the page referenced by page label is not found" do
222
+ io = StringIO.new
223
+ HexaPDF::Document.new.tap do |d|
224
+ d.pages.add
225
+ d.pages.add
226
+ d.pages.add_labelling_range(1, numbering_style: :decimal, prefix: 'Test')
227
+ end.write(io)
228
+ @ref[:F].embed(io, name: 'test.pdf')
229
+ @ref[:Page] = 'Test5'
230
+ assert_nil(@form.referenced_content)
231
+ end
232
+
233
+ it "returns nil if an error happens during processing" do
234
+ @ref[:F].embed(StringIO.new('temp'), name: 'test.pdf')
235
+ assert_nil(@form.referenced_content)
236
+ end
237
+ end
119
238
  end