hexapdf 0.11.9 → 0.12.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 (248) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE +1 -1
  4. data/examples/001-hello_world.rb +1 -1
  5. data/examples/002-graphics.rb +1 -1
  6. data/examples/003-arcs.rb +1 -1
  7. data/examples/004-optimizing.rb +1 -1
  8. data/examples/005-merging.rb +1 -1
  9. data/examples/006-standard_pdf_fonts.rb +1 -1
  10. data/examples/007-truetype.rb +1 -1
  11. data/examples/008-show_char_bboxes.rb +1 -1
  12. data/examples/009-text_layouter_alignment.rb +1 -1
  13. data/examples/010-text_layouter_inline_boxes.rb +1 -1
  14. data/examples/011-text_layouter_line_wrapping.rb +1 -1
  15. data/examples/012-text_layouter_styling.rb +1 -1
  16. data/examples/013-text_layouter_shapes.rb +1 -1
  17. data/examples/014-text_in_polygon.rb +1 -1
  18. data/examples/015-boxes.rb +1 -1
  19. data/examples/016-frame_automatic_box_placement.rb +1 -1
  20. data/examples/017-frame_text_flow.rb +1 -1
  21. data/examples/018-composer.rb +1 -1
  22. data/examples/019-acro_form.rb +51 -0
  23. data/lib/hexapdf.rb +1 -1
  24. data/lib/hexapdf/cli.rb +3 -1
  25. data/lib/hexapdf/cli/batch.rb +1 -1
  26. data/lib/hexapdf/cli/command.rb +18 -9
  27. data/lib/hexapdf/cli/files.rb +1 -1
  28. data/lib/hexapdf/cli/form.rb +240 -0
  29. data/lib/hexapdf/cli/image2pdf.rb +1 -1
  30. data/lib/hexapdf/cli/images.rb +1 -1
  31. data/lib/hexapdf/cli/info.rb +1 -1
  32. data/lib/hexapdf/cli/inspect.rb +1 -1
  33. data/lib/hexapdf/cli/merge.rb +1 -1
  34. data/lib/hexapdf/cli/modify.rb +1 -1
  35. data/lib/hexapdf/cli/optimize.rb +1 -1
  36. data/lib/hexapdf/cli/split.rb +1 -1
  37. data/lib/hexapdf/cli/watermark.rb +1 -1
  38. data/lib/hexapdf/composer.rb +2 -2
  39. data/lib/hexapdf/configuration.rb +66 -11
  40. data/lib/hexapdf/content.rb +3 -1
  41. data/lib/hexapdf/content/canvas.rb +5 -18
  42. data/lib/hexapdf/content/color_space.rb +111 -32
  43. data/lib/hexapdf/content/graphic_object.rb +1 -1
  44. data/lib/hexapdf/content/graphic_object/arc.rb +1 -1
  45. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +1 -1
  46. data/lib/hexapdf/content/graphic_object/geom2d.rb +1 -1
  47. data/lib/hexapdf/content/graphic_object/solid_arc.rb +1 -1
  48. data/lib/hexapdf/content/graphics_state.rb +1 -1
  49. data/lib/hexapdf/content/operator.rb +9 -9
  50. data/lib/hexapdf/content/parser.rb +18 -5
  51. data/lib/hexapdf/content/processor.rb +1 -1
  52. data/lib/hexapdf/content/transformation_matrix.rb +1 -1
  53. data/lib/hexapdf/data_dir.rb +1 -1
  54. data/lib/hexapdf/dictionary.rb +1 -1
  55. data/lib/hexapdf/dictionary_fields.rb +1 -1
  56. data/lib/hexapdf/document.rb +14 -5
  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/pages.rb +3 -14
  61. data/lib/hexapdf/encryption.rb +1 -1
  62. data/lib/hexapdf/encryption/aes.rb +1 -1
  63. data/lib/hexapdf/encryption/arc4.rb +1 -1
  64. data/lib/hexapdf/encryption/fast_aes.rb +1 -1
  65. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  66. data/lib/hexapdf/encryption/identity.rb +1 -1
  67. data/lib/hexapdf/encryption/ruby_aes.rb +1 -1
  68. data/lib/hexapdf/encryption/ruby_arc4.rb +1 -1
  69. data/lib/hexapdf/encryption/security_handler.rb +1 -1
  70. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -1
  71. data/lib/hexapdf/error.rb +1 -1
  72. data/lib/hexapdf/filter.rb +3 -3
  73. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  74. data/lib/hexapdf/filter/ascii_hex_decode.rb +1 -1
  75. data/lib/hexapdf/filter/encryption.rb +1 -1
  76. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  77. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  78. data/lib/hexapdf/filter/{jpx_decode.rb → pass_through.rb} +5 -5
  79. data/lib/hexapdf/filter/predictor.rb +1 -1
  80. data/lib/hexapdf/filter/run_length_decode.rb +1 -1
  81. data/lib/hexapdf/font/cmap.rb +1 -1
  82. data/lib/hexapdf/font/cmap/parser.rb +1 -1
  83. data/lib/hexapdf/font/cmap/writer.rb +1 -1
  84. data/lib/hexapdf/font/encoding.rb +1 -1
  85. data/lib/hexapdf/font/encoding/base.rb +1 -1
  86. data/lib/hexapdf/font/encoding/difference_encoding.rb +1 -1
  87. data/lib/hexapdf/font/encoding/glyph_list.rb +1 -1
  88. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +1 -1
  89. data/lib/hexapdf/font/encoding/mac_roman_encoding.rb +1 -1
  90. data/lib/hexapdf/font/encoding/standard_encoding.rb +1 -1
  91. data/lib/hexapdf/font/encoding/symbol_encoding.rb +1 -1
  92. data/lib/hexapdf/font/encoding/win_ansi_encoding.rb +1 -1
  93. data/lib/hexapdf/font/encoding/zapf_dingbats_encoding.rb +1 -1
  94. data/lib/hexapdf/font/invalid_glyph.rb +1 -1
  95. data/lib/hexapdf/font/true_type.rb +1 -1
  96. data/lib/hexapdf/font/true_type/builder.rb +1 -1
  97. data/lib/hexapdf/font/true_type/font.rb +1 -1
  98. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  99. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  100. data/lib/hexapdf/font/true_type/table.rb +1 -1
  101. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  102. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -1
  103. data/lib/hexapdf/font/true_type/table/directory.rb +1 -1
  104. data/lib/hexapdf/font/true_type/table/glyf.rb +1 -1
  105. data/lib/hexapdf/font/true_type/table/head.rb +1 -1
  106. data/lib/hexapdf/font/true_type/table/hhea.rb +1 -1
  107. data/lib/hexapdf/font/true_type/table/hmtx.rb +1 -1
  108. data/lib/hexapdf/font/true_type/table/kern.rb +1 -1
  109. data/lib/hexapdf/font/true_type/table/loca.rb +1 -1
  110. data/lib/hexapdf/font/true_type/table/maxp.rb +1 -1
  111. data/lib/hexapdf/font/true_type/table/name.rb +1 -1
  112. data/lib/hexapdf/font/true_type/table/os2.rb +1 -1
  113. data/lib/hexapdf/font/true_type/table/post.rb +1 -1
  114. data/lib/hexapdf/font/true_type_wrapper.rb +54 -51
  115. data/lib/hexapdf/font/type1.rb +1 -1
  116. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  117. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  118. data/lib/hexapdf/font/type1/font.rb +1 -1
  119. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  120. data/lib/hexapdf/font/type1/pfb_parser.rb +1 -1
  121. data/lib/hexapdf/font/type1_wrapper.rb +67 -51
  122. data/lib/hexapdf/font_loader.rb +1 -1
  123. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  124. data/lib/hexapdf/font_loader/from_file.rb +1 -1
  125. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  126. data/lib/hexapdf/image_loader.rb +1 -1
  127. data/lib/hexapdf/image_loader/jpeg.rb +1 -1
  128. data/lib/hexapdf/image_loader/pdf.rb +1 -1
  129. data/lib/hexapdf/image_loader/png.rb +1 -1
  130. data/lib/hexapdf/importer.rb +2 -4
  131. data/lib/hexapdf/layout.rb +1 -1
  132. data/lib/hexapdf/layout/box.rb +1 -1
  133. data/lib/hexapdf/layout/frame.rb +1 -1
  134. data/lib/hexapdf/layout/image_box.rb +1 -1
  135. data/lib/hexapdf/layout/inline_box.rb +1 -1
  136. data/lib/hexapdf/layout/line.rb +1 -1
  137. data/lib/hexapdf/layout/numeric_refinements.rb +1 -1
  138. data/lib/hexapdf/layout/style.rb +1 -1
  139. data/lib/hexapdf/layout/text_box.rb +1 -1
  140. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  141. data/lib/hexapdf/layout/text_layouter.rb +1 -1
  142. data/lib/hexapdf/layout/text_shaper.rb +1 -1
  143. data/lib/hexapdf/layout/width_from_polygon.rb +1 -1
  144. data/lib/hexapdf/name_tree_node.rb +1 -1
  145. data/lib/hexapdf/number_tree_node.rb +1 -1
  146. data/lib/hexapdf/object.rb +2 -2
  147. data/lib/hexapdf/parser.rb +4 -3
  148. data/lib/hexapdf/pdf_array.rb +1 -1
  149. data/lib/hexapdf/rectangle.rb +31 -1
  150. data/lib/hexapdf/reference.rb +1 -1
  151. data/lib/hexapdf/revision.rb +2 -1
  152. data/lib/hexapdf/revisions.rb +1 -1
  153. data/lib/hexapdf/serializer.rb +1 -1
  154. data/lib/hexapdf/stream.rb +1 -1
  155. data/lib/hexapdf/task.rb +1 -1
  156. data/lib/hexapdf/task/dereference.rb +1 -1
  157. data/lib/hexapdf/task/optimize.rb +1 -1
  158. data/lib/hexapdf/tokenizer.rb +1 -1
  159. data/lib/hexapdf/type.rb +1 -1
  160. data/lib/hexapdf/type/acro_form.rb +7 -1
  161. data/lib/hexapdf/type/acro_form/appearance_generator.rb +401 -0
  162. data/lib/hexapdf/type/acro_form/button_field.rb +300 -0
  163. data/lib/hexapdf/type/acro_form/choice_field.rb +220 -0
  164. data/lib/hexapdf/type/acro_form/field.rb +220 -17
  165. data/lib/hexapdf/type/acro_form/form.rb +157 -7
  166. data/lib/hexapdf/type/acro_form/text_field.rb +186 -0
  167. data/lib/hexapdf/type/acro_form/variable_text_field.rb +122 -0
  168. data/lib/hexapdf/type/action.rb +1 -1
  169. data/lib/hexapdf/type/actions.rb +1 -1
  170. data/lib/hexapdf/type/actions/go_to.rb +1 -1
  171. data/lib/hexapdf/type/actions/go_to_r.rb +1 -1
  172. data/lib/hexapdf/type/actions/launch.rb +1 -1
  173. data/lib/hexapdf/type/actions/uri.rb +1 -1
  174. data/lib/hexapdf/type/annotation.rb +73 -3
  175. data/lib/hexapdf/type/annotations.rb +1 -1
  176. data/lib/hexapdf/type/annotations/link.rb +2 -2
  177. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  178. data/lib/hexapdf/type/annotations/text.rb +1 -1
  179. data/lib/hexapdf/type/annotations/widget.rb +239 -2
  180. data/lib/hexapdf/type/catalog.rb +21 -1
  181. data/lib/hexapdf/type/cid_font.rb +1 -1
  182. data/lib/hexapdf/type/embedded_file.rb +1 -1
  183. data/lib/hexapdf/type/file_specification.rb +1 -1
  184. data/lib/hexapdf/type/font.rb +18 -1
  185. data/lib/hexapdf/type/font_descriptor.rb +2 -2
  186. data/lib/hexapdf/type/font_simple.rb +1 -1
  187. data/lib/hexapdf/type/font_true_type.rb +1 -1
  188. data/lib/hexapdf/type/font_type0.rb +1 -1
  189. data/lib/hexapdf/type/font_type1.rb +16 -1
  190. data/lib/hexapdf/type/font_type3.rb +1 -1
  191. data/lib/hexapdf/type/form.rb +1 -1
  192. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  193. data/lib/hexapdf/type/icon_fit.rb +1 -1
  194. data/lib/hexapdf/type/image.rb +3 -1
  195. data/lib/hexapdf/type/info.rb +1 -1
  196. data/lib/hexapdf/type/names.rb +1 -1
  197. data/lib/hexapdf/type/object_stream.rb +1 -1
  198. data/lib/hexapdf/type/page.rb +1 -1
  199. data/lib/hexapdf/type/page_tree_node.rb +8 -11
  200. data/lib/hexapdf/type/resources.rb +16 -3
  201. data/lib/hexapdf/type/trailer.rb +2 -3
  202. data/lib/hexapdf/type/viewer_preferences.rb +1 -1
  203. data/lib/hexapdf/type/xref_stream.rb +1 -1
  204. data/lib/hexapdf/utils/bit_field.rb +38 -24
  205. data/lib/hexapdf/utils/bit_stream.rb +1 -1
  206. data/lib/hexapdf/utils/graphics_helpers.rb +1 -1
  207. data/lib/hexapdf/utils/lru_cache.rb +1 -1
  208. data/lib/hexapdf/utils/math_helpers.rb +1 -1
  209. data/lib/hexapdf/utils/object_hash.rb +1 -1
  210. data/lib/hexapdf/utils/pdf_doc_encoding.rb +1 -1
  211. data/lib/hexapdf/utils/sorted_tree_node.rb +1 -1
  212. data/lib/hexapdf/version.rb +2 -2
  213. data/lib/hexapdf/writer.rb +1 -1
  214. data/lib/hexapdf/xref_section.rb +1 -1
  215. data/test/hexapdf/content/common.rb +2 -2
  216. data/test/hexapdf/content/test_color_space.rb +71 -8
  217. data/test/hexapdf/content/test_operator.rb +22 -22
  218. data/test/hexapdf/content/test_parser.rb +14 -0
  219. data/test/hexapdf/document/test_fonts.rb +1 -1
  220. data/test/hexapdf/document/test_pages.rb +6 -6
  221. data/test/hexapdf/font/test_true_type_wrapper.rb +10 -7
  222. data/test/hexapdf/font/test_type1_wrapper.rb +32 -8
  223. data/test/hexapdf/test_document.rb +12 -0
  224. data/test/hexapdf/test_parser.rb +10 -0
  225. data/test/hexapdf/test_rectangle.rb +14 -0
  226. data/test/hexapdf/test_revision.rb +3 -0
  227. data/test/hexapdf/test_writer.rb +2 -2
  228. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +515 -0
  229. data/test/hexapdf/type/acro_form/test_button_field.rb +276 -0
  230. data/test/hexapdf/type/acro_form/test_choice_field.rb +137 -0
  231. data/test/hexapdf/type/acro_form/test_field.rb +124 -6
  232. data/test/hexapdf/type/acro_form/test_form.rb +189 -22
  233. data/test/hexapdf/type/acro_form/test_text_field.rb +119 -0
  234. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +77 -0
  235. data/test/hexapdf/type/annotations/test_text.rb +1 -1
  236. data/test/hexapdf/type/annotations/test_widget.rb +199 -0
  237. data/test/hexapdf/type/test_annotation.rb +45 -0
  238. data/test/hexapdf/type/test_catalog.rb +18 -0
  239. data/test/hexapdf/type/test_font.rb +5 -0
  240. data/test/hexapdf/type/test_font_type1.rb +8 -0
  241. data/test/hexapdf/type/test_image.rb +7 -0
  242. data/test/hexapdf/type/test_page_tree_node.rb +20 -12
  243. data/test/hexapdf/type/test_resources.rb +20 -0
  244. data/test/hexapdf/type/test_trailer.rb +4 -0
  245. data/test/hexapdf/utils/test_bit_field.rb +13 -1
  246. data/test/test_helper.rb +1 -1
  247. metadata +37 -18
  248. data/lib/hexapdf/filter/dct_decode.rb +0 -60
@@ -12,6 +12,14 @@ describe HexaPDF::Font::Type1Wrapper do
12
12
  @symbol_wrapper = HexaPDF::Font::Type1Wrapper.new(@doc, FONT_SYMBOL)
13
13
  end
14
14
 
15
+ it "can be used with an existing PDF object" do
16
+ font = @doc.add({Type: :Font, Subtype: :Type1, Encoding: :WinAnsiEncoding,
17
+ BaseFont: :"Times-Roman"})
18
+ wrapper = HexaPDF::Font::Type1Wrapper.new(@doc, FONT_TIMES, pdf_object: font)
19
+ assert_equal([:T, :e, :s, :t], wrapper.decode_utf8("Test").map(&:name))
20
+ assert_equal("a", wrapper.encode(wrapper.glyph(:a)))
21
+ end
22
+
15
23
  it "returns 1 for the scaling factor" do
16
24
  assert_equal(1, @times_wrapper.scaling_factor)
17
25
  end
@@ -21,6 +29,10 @@ describe HexaPDF::Font::Type1Wrapper do
21
29
  assert_equal([:T, :e, :s, :t], @times_wrapper.decode_utf8("Test").map(&:name))
22
30
  end
23
31
 
32
+ it "falls back to the internal font encoding if the Unicode codepoint is not mapped" do
33
+ assert_equal([:Delta, :Delta], @symbol_wrapper.decode_utf8("D∆").map(&:name))
34
+ end
35
+
24
36
  it "UTF-8 characters for which no glyph name exists, are mapped to InvalidGlyph objects" do
25
37
  glyphs = @times_wrapper.decode_utf8("😁")
26
38
  assert_equal(1, glyphs.length)
@@ -58,7 +70,7 @@ describe HexaPDF::Font::Type1Wrapper do
58
70
  code = @times_wrapper.encode(@times_wrapper.glyph(:a))
59
71
  @doc.dispatch_message(:complete_objects)
60
72
  assert_equal("a", code)
61
- assert_equal(:WinAnsiEncoding, @times_wrapper.dict[:Encoding])
73
+ assert_equal(:WinAnsiEncoding, @times_wrapper.pdf_object[:Encoding])
62
74
  end
63
75
 
64
76
  it "fails if an InvalidGlyph is encoded" do
@@ -70,13 +82,25 @@ describe HexaPDF::Font::Type1Wrapper do
70
82
  end
71
83
  end
72
84
 
73
- describe "uses an empty encoding as initial encoding for symbolic fonts" do
74
- it "returns the PDF font dictionary and encoded glyph" do
75
- code = @symbol_wrapper.encode(@symbol_wrapper.glyph(:plus))
76
- @doc.dispatch_message(:complete_objects)
77
- assert_equal("\x21", code)
78
- assert_equal({Differences: [32, :space, :plus]}, @symbol_wrapper.dict[:Encoding].value)
79
- end
85
+ it "uses the font's internal encoding for fonts with the Special character set" do
86
+ code = @symbol_wrapper.encode(@symbol_wrapper.glyph(:plus))
87
+ @doc.dispatch_message(:complete_objects)
88
+ assert_equal("+", code)
89
+ assert_nil(@symbol_wrapper.pdf_object[:Encoding])
90
+ end
91
+
92
+ it "uses an empty encoding as initial encoding if a custom encoding is needed" do
93
+ wrapper = HexaPDF::Font::Type1Wrapper.new(@doc, FONT_TIMES, custom_encoding: true)
94
+ code = wrapper.encode(wrapper.glyph(:plus))
95
+ @doc.dispatch_message(:complete_objects)
96
+ assert_equal("\x21", code)
97
+ assert_equal({Differences: [32, :space, :plus]}, wrapper.pdf_object[:Encoding].value)
98
+ end
99
+ end
100
+
101
+ describe "creates the necessary PDF dictionaries" do
102
+ it "sets the circular reference" do
103
+ assert_same(@times_wrapper, @times_wrapper.pdf_object.font_wrapper)
80
104
  end
81
105
  end
82
106
  end
@@ -584,6 +584,18 @@ describe HexaPDF::Document do
584
584
  end
585
585
  end
586
586
 
587
+ describe "acro_form" do
588
+ it "returns the main AcroForm object" do
589
+ assert_nil(@doc.acro_form)
590
+ @doc.catalog[:AcroForm] = 5
591
+ assert_equal(5, @doc.acro_form)
592
+ end
593
+
594
+ it "create the AcroForm object if instructed" do
595
+ assert_equal(:XXAcroForm, @doc.acro_form(create: true).type)
596
+ end
597
+ end
598
+
587
599
  describe "listener interface" do
588
600
  it "allows registering and dispatching messages" do
589
601
  args = []
@@ -231,6 +231,10 @@ describe HexaPDF::Parser do
231
231
  create_parser("startxref\n5")
232
232
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
233
233
  assert_match(/end-of-file marker not found/, exp.message)
234
+
235
+ create_parser("")
236
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
237
+ assert_match(/end-of-file marker not found/, exp.message)
234
238
  end
235
239
 
236
240
  it "fails if the startxref keyword is missing" do
@@ -251,6 +255,12 @@ describe HexaPDF::Parser do
251
255
  assert_match(/file header/, exp.message)
252
256
  end
253
257
 
258
+ it "fails if the header is missing" do
259
+ create_parser("no header")
260
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.file_header_version }
261
+ assert_match(/file header/, exp.message)
262
+ end
263
+
254
264
  it "ignores junk at the beginning of the file and correctly calculates offset" do
255
265
  create_parser("junk" * 200 + "\n%PDF-1.4\n")
256
266
  assert_equal('1.4', @parser.file_header_version)
@@ -36,6 +36,20 @@ describe HexaPDF::Rectangle do
36
36
  assert_equal(4, rect.height)
37
37
  end
38
38
 
39
+ it "allows setting all fields of the rectangle" do
40
+ rect = HexaPDF::Rectangle.new([2, 1, 0, 5])
41
+ rect.left = 5
42
+ rect.right = 1
43
+ rect.bottom = 2
44
+ rect.top = 3
45
+ assert_equal([5, 2, 1, 3], rect.value)
46
+
47
+ rect.width = 10
48
+ assert_equal(15, rect.right)
49
+ rect.height = 10
50
+ assert_equal(12, rect.top)
51
+ end
52
+
39
53
  it "allows comparison to arrays" do
40
54
  rect = HexaPDF::Rectangle.new([0, 1, 2, 5])
41
55
  assert(rect == [0, 1, 2, 5])
@@ -114,12 +114,14 @@ describe HexaPDF::Revision do
114
114
  @rev.delete(@ref, mark_as_free: false)
115
115
  refute(@rev.object?(@ref))
116
116
  assert(@obj.null?)
117
+ assert_raises(HexaPDF::Error) { @obj.document }
117
118
  end
118
119
 
119
120
  it "deletes objects specified by object number" do
120
121
  @rev.delete(@ref.oid, mark_as_free: false)
121
122
  refute(@rev.object?(@ref.oid))
122
123
  assert(@obj.null?)
124
+ assert_raises(HexaPDF::Error) { @obj.document }
123
125
  end
124
126
 
125
127
  it "marks the object as PDF null object when using mark_as_free=true" do
@@ -127,6 +129,7 @@ describe HexaPDF::Revision do
127
129
  @rev.delete(@ref)
128
130
  assert(@rev.object(@ref).null?)
129
131
  assert(@obj.null?)
132
+ assert_raises(HexaPDF::Error) { @obj.document }
130
133
  end
131
134
  end
132
135
 
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.11.9)>>
43
+ <</Producer(HexaPDF version 0.12.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.11.9)>>
75
+ <</Producer(HexaPDF version 0.12.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -0,0 +1,515 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative '../../content/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/type/acro_form/appearance_generator'
7
+
8
+ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
9
+ before do
10
+ @doc = HexaPDF::Document.new
11
+ @page = @doc.pages.add
12
+ @form = @doc.acro_form(create: true)
13
+ end
14
+
15
+ describe "create_appearances" do
16
+ before do
17
+ @field = @doc.add({FT: :Btn}, type: :XXAcroFormField, subtype: :Btn)
18
+ @widget = @doc.wrap({Parent: @field, Type: :Annot, Subtype: :Widget})
19
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
20
+ end
21
+
22
+ it "fails for unsupported button fields" do
23
+ @field.flag(:push_button)
24
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
25
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
26
+ end
27
+
28
+ it "fails for unsupported choice fields" do
29
+ @field = @doc.wrap(@field, type: :XXAcroFormField, subtype: :Ch)
30
+ @field[:FT] = :Ch
31
+ @field.initialize_as_list_box
32
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
33
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
34
+ end
35
+
36
+ it "fails for unsupported field types" do
37
+ @field[:FT] = :Unknown
38
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
39
+ end
40
+ end
41
+
42
+ describe "background color and border" do
43
+ before do
44
+ @field = @doc.add({FT: :Btn}, type: :XXAcroFormField, subtype: :Btn)
45
+ @widget = @field.create_widget(@page, defaults: false, Rect: [0, 0, 10, 20])
46
+ @xform = @doc.add({Type: :XObject, Subtype: :Form, BBox: @widget[:Rect]})
47
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
48
+ end
49
+
50
+ def execute(circular = false)
51
+ @generator.send(:apply_background_and_border, @widget.border_style, @xform.canvas,
52
+ circular: circular)
53
+ end
54
+
55
+ it "applies no background color or border if none is set" do
56
+ execute
57
+ assert_operators(@xform.stream, [])
58
+ end
59
+
60
+ it "applies a background color if one set" do
61
+ @widget.background_color(0.5)
62
+ execute
63
+ execute(true)
64
+ assert_operators(@xform.stream,
65
+ [[:save_graphics_state],
66
+ [:set_device_gray_non_stroking_color, [0.5]],
67
+ [:append_rectangle, [0, 0, 10, 20]],
68
+ [:fill_path_non_zero], [:restore_graphics_state],
69
+
70
+ [:save_graphics_state],
71
+ [:set_device_gray_non_stroking_color, [0.5]],
72
+ [:move_to, [10.0, 10.0]],
73
+ [:curve_to, [10.0, 11.78411, 9.045085, 13.438072, 7.5, 14.330127]],
74
+ [:curve_to, [5.954915, 15.222182, 4.045085, 15.222182, 2.5, 14.330127]],
75
+ [:curve_to, [0.954915, 13.438072, 0.0, 11.78411, 0.0, 10.0]],
76
+ [:curve_to, [-0.0, 8.21589, 0.954915, 6.561928, 2.5, 5.669873]],
77
+ [:curve_to, [4.045085, 4.777818, 5.954915, 4.777818, 7.5, 5.669873]],
78
+ [:curve_to, [9.045085, 6.561928, 10.0, 8.21589, 10.0, 10.0]],
79
+ [:close_subpath], [:fill_path_non_zero], [:restore_graphics_state]])
80
+ end
81
+
82
+ it "sets the border color and width correctly" do
83
+ @widget.border_style(color: 0.5, width: 4)
84
+ execute
85
+ execute(true)
86
+ assert_operators(@xform.stream,
87
+ [[:save_graphics_state],
88
+ [:set_device_gray_stroking_color, [0.5]],
89
+ [:set_line_width, [4]],
90
+ [:append_rectangle, [2, 2, 6, 16]],
91
+ [:stroke_path], [:restore_graphics_state],
92
+
93
+ [:save_graphics_state],
94
+ [:set_device_gray_stroking_color, [0.5]],
95
+ [:set_line_width, [4]],
96
+ [:move_to, [8.0, 10.0]],
97
+ [:curve_to, [8.0, 11.070466, 7.427051, 12.062843, 6.5, 12.598076]],
98
+ [:curve_to, [5.572949, 13.133309, 4.427051, 13.133309, 3.5, 12.598076]],
99
+ [:curve_to, [2.572949, 12.062843, 2.0, 11.070466, 2.0, 10.0]],
100
+ [:curve_to, [2.0, 8.929534, 2.572949, 7.937157, 3.5, 7.401924]],
101
+ [:curve_to, [4.427051, 6.866691, 5.572949, 6.866691, 6.5, 7.401924]],
102
+ [:curve_to, [7.427051, 7.937157, 8.0, 8.929534, 8.0, 10.0]],
103
+ [:close_subpath], [:stroke_path], [:restore_graphics_state]])
104
+ end
105
+
106
+ it "handles the case of an underlined border" do
107
+ @widget.border_style(style: :underlined, width: 2)
108
+ execute
109
+ execute(true)
110
+ assert_operators(@xform.stream,
111
+ [[:save_graphics_state],
112
+ [:set_line_width, [2]],
113
+ [:move_to, [1, 1]], [:line_to, [9.0, 1]],
114
+ [:stroke_path], [:restore_graphics_state],
115
+
116
+ [:save_graphics_state],
117
+ [:set_line_width, [2]],
118
+ [:move_to, [1.0, 10.0]],
119
+ [:curve_to, [1.0, 8.572712, 1.763932, 7.249543, 3.0, 6.535898]],
120
+ [:curve_to, [4.236068, 5.822254, 5.763932, 5.822254, 7.0, 6.535898]],
121
+ [:curve_to, [8.236068, 7.249543, 9.0, 8.572712, 9.0, 10.0]],
122
+ [:stroke_path], [:restore_graphics_state]])
123
+ end
124
+ end
125
+
126
+ describe "draw_marker" do
127
+ before do
128
+ @field = @doc.add({FT: :Btn}, type: :XXAcroFormField, subtype: :Btn)
129
+ @widget = @field.create_widget(@page, defaults: false, Rect: [0, 0, 10, 20])
130
+ @xform = @doc.add({Type: :XObject, Subtype: :Form, BBox: @widget[:Rect]})
131
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
132
+ end
133
+
134
+ def execute
135
+ @generator.send(:draw_marker, @xform.canvas, @widget[:Rect], @widget.border_style.width,
136
+ @widget.marker_style)
137
+ end
138
+
139
+ it "handles the marker :circle specially for radio button widgets" do
140
+ @field.initialize_as_radio_button
141
+ @widget.marker_style(style: :circle, color: 0.5)
142
+ execute
143
+ assert_operators(@xform.stream,
144
+ [[:set_device_gray_non_stroking_color, [0.5]],
145
+ [:move_to, [7.0, 10.0]],
146
+ [:curve_to, [7.0, 10.713644, 6.618034, 11.375229, 6.0, 11.732051]],
147
+ [:curve_to, [5.381966, 12.088873, 4.618034, 12.088873, 4.0, 11.732051]],
148
+ [:curve_to, [3.381966, 11.375229, 3.0, 10.713644, 3.0, 10.0]],
149
+ [:curve_to, [3.0, 9.286356, 3.381966, 8.624771, 4.0, 8.267949]],
150
+ [:curve_to, [4.618034, 7.911127, 5.381966, 7.911127, 6.0, 8.267949]],
151
+ [:curve_to, [6.618034, 8.624771, 7.0, 9.286356, 7.0, 10.0]],
152
+ [:close_subpath],
153
+ [:fill_path_non_zero]])
154
+ end
155
+
156
+ it "handles the marker :cross specially" do
157
+ @widget.marker_style(style: :cross, color: 0.5)
158
+ execute
159
+ assert_operators(@xform.stream,
160
+ [[:set_device_gray_stroking_color, [0.5]],
161
+ [:move_to, [1, 1]], [:line_to, [9, 19]],
162
+ [:move_to, [1, 19]], [:line_to, [9, 1]],
163
+ [:stroke_path]])
164
+ end
165
+
166
+ describe "handles the normal markers by drawing them using the ZapfDingbats font" do
167
+ it "works with font auto-sizing" do
168
+ @widget.marker_style(style: :check, color: 0.5, size: 0)
169
+ execute
170
+ assert_operators(@xform.stream,
171
+ [[:set_font_and_size, [:F1, 8]],
172
+ [:set_device_gray_non_stroking_color, [0.5]],
173
+ [:begin_text],
174
+ [:set_text_matrix, [1, 0, 0, 1, 1.616, 7.236]],
175
+ [:show_text, ["4"]],
176
+ [:end_text]])
177
+ end
178
+
179
+ it "works with a fixed font size" do
180
+ @widget.marker_style(style: :check, color: 0.5, size: 5)
181
+ execute
182
+ assert_operators(@xform.stream,
183
+ [[:set_font_and_size, [:F1, 5]],
184
+ [:set_device_gray_non_stroking_color, [0.5]],
185
+ [:begin_text],
186
+ [:set_text_matrix, [1, 0, 0, 1, 2.885, 8.2725]],
187
+ [:show_text, ["4"]],
188
+ [:end_text]])
189
+ end
190
+ end
191
+ end
192
+
193
+ describe "button fields" do
194
+ before do
195
+ @field = @doc.add({FT: :Btn}, type: :XXAcroFormField, subtype: :Btn)
196
+ end
197
+
198
+ describe "check box" do
199
+ before do
200
+ @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
201
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
202
+ @field.field_value = :Off
203
+ end
204
+
205
+ it "updates the widgets' /AS entry to point to the selected appearance" do
206
+ @generator.create_appearances
207
+ assert_equal(@field[:V], @widget[:AS])
208
+ end
209
+
210
+ it "set the print flag on the widgets" do
211
+ @generator.create_appearances
212
+ assert(@widget.flagged?(:print))
213
+ end
214
+
215
+ it "adjusts the /Rect if width is zero" do
216
+ @generator.create_appearances
217
+ assert_equal(12, @widget[:Rect].width)
218
+ end
219
+
220
+ it "adjusts the /Rect if height is zero" do
221
+ @generator.create_appearances
222
+ assert_equal(12, @widget[:Rect].height)
223
+ end
224
+
225
+ it "creates the needed appearance streams" do
226
+ @generator.create_appearances
227
+ assert_equal(:XObject, @widget[:AP][:N][:Off].type)
228
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
229
+ end
230
+
231
+ it "creates the /Off appearance stream" do
232
+ @generator.create_appearances
233
+ assert_operators(@widget[:AP][:N][:Off].stream,
234
+ [[:save_graphics_state],
235
+ [:set_device_gray_non_stroking_color, [1.0]],
236
+ [:append_rectangle, [0, 0, 12, 12]],
237
+ [:fill_path_non_zero],
238
+ [:append_rectangle, [0.5, 0.5, 11, 11]],
239
+ [:stroke_path], [:restore_graphics_state]])
240
+ end
241
+
242
+ it "creates the /Yes appearance stream" do
243
+ @generator.create_appearances
244
+ assert_operators(@widget[:AP][:N][:Yes].stream,
245
+ [[:save_graphics_state],
246
+ [:set_device_gray_non_stroking_color, [1.0]],
247
+ [:append_rectangle, [0, 0, 12, 12]],
248
+ [:fill_path_non_zero],
249
+ [:append_rectangle, [0.5, 0.5, 11, 11]],
250
+ [:stroke_path], [:restore_graphics_state],
251
+
252
+ [:save_graphics_state],
253
+ [:set_font_and_size, [:F1, 10]],
254
+ [:begin_text],
255
+ [:set_text_matrix, [1, 0, 0, 1, 1.77, 2.545]],
256
+ [:show_text, ["4"]],
257
+ [:end_text],
258
+ [:restore_graphics_state]])
259
+ end
260
+
261
+ it "fails if the appearance dictionaries are not set up" do
262
+ @widget[:AP][:N].delete(:Off)
263
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
264
+ end
265
+ end
266
+
267
+ describe "radio button" do
268
+ before do
269
+ @field.initialize_as_radio_button
270
+ @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0], value: :radio)
271
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
272
+ end
273
+
274
+ it "updates the widgets' /AS entry to point to the selected appearance" do
275
+ @field.field_value = :radio
276
+ @generator.create_appearances
277
+ assert_equal(@field[:V], @widget[:AS])
278
+
279
+ @field.create_widget(@page, value: :other)
280
+ @field.field_value = :other
281
+ @generator.create_appearances
282
+ assert_equal(:Off, @widget[:AS])
283
+ end
284
+
285
+ it "set the print flag on the widgets" do
286
+ @generator.create_appearances
287
+ assert(@widget.flagged?(:print))
288
+ end
289
+
290
+ it "adjusts the /Rect if width is zero" do
291
+ @generator.create_appearances
292
+ assert_equal(12, @widget[:Rect].width)
293
+ end
294
+
295
+ it "adjusts the /Rect if height is zero" do
296
+ @generator.create_appearances
297
+ assert_equal(12, @widget[:Rect].height)
298
+ end
299
+
300
+ it "creates the needed appearance streams" do
301
+ @generator.create_appearances
302
+ assert_equal(:XObject, @widget[:AP][:N][:Off].type)
303
+ assert_equal(:XObject, @widget[:AP][:N][:radio].type)
304
+ end
305
+
306
+ it "creates the /Off appearance stream" do
307
+ @widget.marker_style(style: :cross)
308
+ @generator.create_appearances
309
+ assert_operators(@widget[:AP][:N][:Off].stream,
310
+ [[:save_graphics_state],
311
+ [:set_device_gray_non_stroking_color, [1.0]],
312
+ [:append_rectangle, [0, 0, 12, 12]],
313
+ [:fill_path_non_zero],
314
+ [:append_rectangle, [0.5, 0.5, 11, 11]],
315
+ [:stroke_path], [:restore_graphics_state]])
316
+ end
317
+
318
+ it "creates the appearance stream according to the set value" do
319
+ @widget.marker_style(style: :check)
320
+ @generator.create_appearances
321
+ assert_operators(@widget[:AP][:N][:radio].stream,
322
+ [[:save_graphics_state],
323
+ [:set_device_gray_non_stroking_color, [1.0]],
324
+ [:append_rectangle, [0, 0, 12, 12]],
325
+ [:fill_path_non_zero],
326
+ [:append_rectangle, [0.5, 0.5, 11, 11]],
327
+ [:stroke_path], [:restore_graphics_state],
328
+
329
+ [:save_graphics_state],
330
+ [:set_font_and_size, [:F1, 10]],
331
+ [:begin_text],
332
+ [:set_text_matrix, [1, 0, 0, 1, 1.77, 2.545]],
333
+ [:show_text, ["4"]],
334
+ [:end_text],
335
+ [:restore_graphics_state]])
336
+ end
337
+
338
+ it "fails if the appearance dictionaries are not set up" do
339
+ @widget[:AP][:N].delete(:radio)
340
+ assert_raises(HexaPDF::Error) { @generator.create_appearances }
341
+ end
342
+ end
343
+ end
344
+
345
+ describe "text field" do
346
+ before do
347
+ @form.set_default_appearance_string
348
+ @field = @doc.add({FT: :Tx}, type: :XXAcroFormField, subtype: :Tx)
349
+ @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
350
+ @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
351
+ end
352
+
353
+ it "updates the widgets to use the :N appearance stream" do
354
+ @generator.create_appearances
355
+ assert_equal(:N, @widget[:AS])
356
+ end
357
+
358
+ it "set the print flag on the widgets" do
359
+ @generator.create_appearances
360
+ assert(@widget.flagged?(:print))
361
+ end
362
+
363
+ describe "it adjusts the :Rect when necessary" do
364
+ before do
365
+ @widget.border_style(width: 3)
366
+ end
367
+
368
+ it "uses a default width if the width is zero" do
369
+ @generator.create_appearances
370
+ assert_equal(@doc.config['acro_form.text_field.default_width'], @widget[:Rect].width)
371
+ end
372
+
373
+ it "uses the font size of the /DA if non-zero as basis for the height if it is zero" do
374
+ @field.set_default_appearance_string(font_size: 10)
375
+ @generator.create_appearances
376
+ assert_equal(15.25, @widget[:Rect].height)
377
+ end
378
+
379
+ it "uses a default font size as basis for the height if it and the set font size are zero" do
380
+ assert_equal(0, @field.parse_default_appearance_string[1])
381
+ @generator.create_appearances
382
+ assert_equal(15.25, @widget[:Rect].height)
383
+ end
384
+ end
385
+
386
+ it "adds an appropriate form XObject" do
387
+ @generator.create_appearances
388
+ form = @widget[:AP][:N]
389
+ assert_equal(:XObject, form.type)
390
+ assert_equal(:Form, form[:Subtype])
391
+ assert_equal([0, 0, @widget[:Rect].width, @widget[:Rect].height], form[:BBox])
392
+ assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
393
+ end
394
+
395
+ describe "single line text fields" do
396
+ describe "font size calculation" do
397
+ before do
398
+ @widget[:Rect].height = 20
399
+ @widget[:Rect].width = 100
400
+ @field.field_value = ''
401
+ end
402
+
403
+ it "uses the non-zero font size" do
404
+ @field.set_default_appearance_string(font_size: 10)
405
+ @generator.create_appearances
406
+ assert_operators(@widget[:AP][:N].stream,
407
+ [:set_font_and_size, [:F1, 10]],
408
+ range: 5)
409
+ end
410
+
411
+ it "calculates the font size based on the rectangle height and border width" do
412
+ @generator.create_appearances
413
+ assert_operators(@widget[:AP][:N].stream,
414
+ [:set_font_and_size, [:F1, 12.923875]],
415
+ range: 5)
416
+ @widget.border_style(width: 2, color: :transparent)
417
+ @generator.create_appearances
418
+ assert_operators(@widget[:AP][:N].stream,
419
+ [:set_font_and_size, [:F1, 11.487889]],
420
+ range: 5)
421
+ end
422
+ end
423
+
424
+ describe "quadding e.g. text alignment" do
425
+ before do
426
+ @field.field_value = 'Test'
427
+ @field.set_default_appearance_string(font_size: 10)
428
+ @widget[:Rect].height = 20
429
+ end
430
+
431
+ it "works for left aligned text" do
432
+ @field.text_alignment(:left)
433
+ @generator.create_appearances
434
+ assert_operators(@widget[:AP][:N].stream,
435
+ [:set_text_matrix, [1, 0, 0, 1, 2, 6.41]],
436
+ range: 7)
437
+ end
438
+
439
+ it "works for right aligned text" do
440
+ @field.text_alignment(:right)
441
+ @generator.create_appearances
442
+ assert_operators(@widget[:AP][:N].stream,
443
+ [:set_text_matrix, [1, 0, 0, 1, 78.55, 6.41]],
444
+ range: 7)
445
+ end
446
+
447
+ it "works for center aligned text" do
448
+ @field.text_alignment(:center)
449
+ @generator.create_appearances
450
+ assert_operators(@widget[:AP][:N].stream,
451
+ [:set_text_matrix, [1, 0, 0, 1, 40.275, 6.41]],
452
+ range: 7)
453
+ end
454
+
455
+ it "vertically aligns to the font descender if the text is too high" do
456
+ @widget[:Rect].height = 5
457
+ @generator.create_appearances
458
+ assert_operators(@widget[:AP][:N].stream,
459
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.07]],
460
+ range: 7)
461
+ end
462
+ end
463
+
464
+ it "creates the /N appearance stream according to the set string" do
465
+ @field.field_value = 'Text'
466
+ @generator.create_appearances
467
+ assert_operators(@widget[:AP][:N].stream,
468
+ [[:begin_marked_content, [:Tx]],
469
+ [:save_graphics_state],
470
+ [:append_rectangle, [1, 1, 98, 9.25]],
471
+ [:clip_path_non_zero],
472
+ [:end_path],
473
+ [:set_font_and_size, [:F1, 6.641436]],
474
+ [:begin_text],
475
+ [:set_text_matrix, [1, 0, 0, 1, 2, 3.240724]],
476
+ [:show_text, ["Text"]],
477
+ [:end_text],
478
+ [:restore_graphics_state],
479
+ [:end_marked_content]])
480
+ end
481
+
482
+ end
483
+
484
+ describe "choice fields" do
485
+ it "works for combo boxes by using the text appearance method" do
486
+ @form.set_default_appearance_string
487
+ field = @doc.add({FT: :Ch}, type: :XXAcroFormField, subtype: :Ch)
488
+ field.initialize_as_combo_box
489
+ widget = field.create_widget(@page, Rect: [0, 0, 0, 0])
490
+ generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(widget)
491
+ generator.create_appearances
492
+ assert_kind_of(HexaPDF::Type::Form, widget[:AP][:N])
493
+ end
494
+ end
495
+
496
+ describe "font resolution in case the referenced font is not usable" do
497
+ before do
498
+ def (@form.default_resources.font(:F1)).font_wrapper; nil; end
499
+ @field.field_value = 'Test'
500
+ end
501
+
502
+ it "uses the fallback font if configured" do
503
+ @doc.config['acro_form.fallback_font'] = ['Times', variant: :none]
504
+ @generator.create_appearances
505
+ assert_equal(:'Times-Roman', @widget[:AP][:N][:Resources][:Font][:F2][:BaseFont])
506
+ end
507
+
508
+ it "fails if fallback fonts are disabled" do
509
+ @doc.config['acro_form.fallback_font'] = nil
510
+ msg = assert_raises(HexaPDF::Error) { @generator.create_appearances }
511
+ assert_match(/Font.*not usable/, msg.message)
512
+ end
513
+ end
514
+ end
515
+ end