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
@@ -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-2019 Thomas Leitner
7
+ # Copyright (C) 2014-2020 Thomas Leitner
8
8
  #
9
9
  # HexaPDF is free software: you can redistribute it and/or modify it
10
10
  # under the terms of the GNU Affero General Public License version 3 as
@@ -44,6 +44,12 @@ module HexaPDF
44
44
 
45
45
  autoload(:Form, 'hexapdf/type/acro_form/form')
46
46
  autoload(:Field, 'hexapdf/type/acro_form/field')
47
+ autoload(:VariableTextField, 'hexapdf/type/acro_form/variable_text_field')
48
+ autoload(:TextField, 'hexapdf/type/acro_form/text_field')
49
+ autoload(:ButtonField, 'hexapdf/type/acro_form/button_field')
50
+ autoload(:ChoiceField, 'hexapdf/type/acro_form/choice_field')
51
+
52
+ autoload(:AppearanceGenerator, 'hexapdf/type/acro_form/appearance_generator')
47
53
 
48
54
  end
49
55
 
@@ -0,0 +1,401 @@
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-2020 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/error'
38
+ require 'hexapdf/layout/style'
39
+ require 'hexapdf/layout/text_fragment'
40
+
41
+ module HexaPDF
42
+ module Type
43
+ module AcroForm
44
+
45
+ # The AppearanceGenerator class provides methods for generating and updating the appearance
46
+ # streams of form fields.
47
+ #
48
+ # The only method needed is #create_appearances since this method determines to what field the
49
+ # widget belongs and therefore which appearance should be generated.
50
+ #
51
+ # The visual appearance of a field is constructed using information from the field itself as
52
+ # well as information from the widget. See the documentation for the individual methods which
53
+ # information is used in which way.
54
+ #
55
+ # By default, any existing appearances are overwritten and the +:print+ flag is set on the
56
+ # widget so that the field appearance will appear on print-outs.
57
+ #
58
+ # The visual appearances are chosen to be similar to those used by Adobe Acrobat and others.
59
+ # By subclassing and overriding the necessary methods it is possible to define custom
60
+ # appearances.
61
+ #
62
+ # See: PDF1.7 s12.5.5, s12.7
63
+ class AppearanceGenerator
64
+
65
+ # Creates a new instance for the given +widget+.
66
+ def initialize(widget)
67
+ @widget = widget
68
+ @field = widget.form_field
69
+ @document = widget.document
70
+ end
71
+
72
+ # Creates the appropriate appearances for the widget.
73
+ def create_appearances
74
+ case @field.field_type
75
+ when :Btn
76
+ if @field.check_box?
77
+ create_check_box_appearances
78
+ elsif @field.radio_button?
79
+ create_radio_button_appearances
80
+ else
81
+ raise HexaPDF::Error, "Unsupported button field type"
82
+ end
83
+ when :Tx
84
+ create_text_appearances
85
+ when :Ch
86
+ if @field.combo_box?
87
+ create_text_appearances
88
+ else
89
+ raise HexaPDF::Error, "List box not supported yet"
90
+ end
91
+ else
92
+ raise HexaPDF::Error, "Unsupported field type #{@field.field_type}"
93
+ end
94
+ end
95
+
96
+ # Creates the appropriate appearances for check boxes.
97
+ #
98
+ # For unchecked boxes an empty rectangle is drawn. When checked, a symbol from the
99
+ # ZapfDingbats font is placed inside the rectangle. How this is exactly done depends on the
100
+ # following values:
101
+ #
102
+ # * The widget's rectangle /Rect must be defined. If the height and/or width of the
103
+ # rectangle are zero, they are based on the configuration option
104
+ # +acro_form.default_font_size+ and widget's border width. In such a case the rectangle is
105
+ # appropriately updated.
106
+ #
107
+ # * The line width, style and color of the rectangle are taken from the widget's border
108
+ # style. See HexaPDF::Type::Annotations::Widget#border_style.
109
+ #
110
+ # * The background color is determined by the widget's background color. See
111
+ # HexaPDF::Type::Annotations::Widget#background_color.
112
+ #
113
+ # * The symbol (marker) as well as its size and color are determined by the marker style of
114
+ # the widget. See HexaPDF::Type::Annotations::Widget#marker_style for details.
115
+ #
116
+ # Examples:
117
+ #
118
+ # widget.border_style(color: 0)
119
+ # widget.background_color(1)
120
+ # widget.marker_style(style: :check, size: 0, color: 0)
121
+ # # => default appearance
122
+ #
123
+ # widget.border_style(color: :transparent, width: 2)
124
+ # widget.background_color(0.7)
125
+ # widget.marker_style(style: :cross)
126
+ # # => no visible rectangle, gray background, cross mark when checked
127
+ def create_check_box_appearances
128
+ unless @widget.appearance&.normal_appearance&.value&.size == 2
129
+ raise HexaPDF::Error, "Widget of check box doesn't define name for on state"
130
+ end
131
+ border_style = @widget.border_style
132
+ border_width = border_style.width
133
+
134
+ rect = update_widget(@field[:V], border_width)
135
+
136
+ off_form = @widget.appearance.normal_appearance[:Off] =
137
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
138
+ apply_background_and_border(border_style, off_form.canvas)
139
+
140
+ on_form = @widget.appearance.normal_appearance[@field.check_box_on_name] =
141
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
142
+ canvas = on_form.canvas
143
+ apply_background_and_border(border_style, canvas)
144
+ canvas.save_graphics_state do
145
+ draw_marker(canvas, rect, border_width, @widget.marker_style)
146
+ end
147
+ end
148
+
149
+ # Creates the appropriate appearances for radio buttons.
150
+ #
151
+ # For unselected radio buttons an empty circle (if the marker is :circle) or rectangle is
152
+ # drawn inside the widget annotation's rectangle. When selected, a symbol from the
153
+ # ZapfDingbats font is placed inside. How this is exactly done depends on the following
154
+ # values:
155
+ #
156
+ # * The widget's rectangle /Rect must be defined. If the height and/or width of the
157
+ # rectangle are zero, they are based on the configuration option
158
+ # +acro_form.default_font_size+ and the widget's border width. In such a case the
159
+ # rectangle is appropriately updated.
160
+ #
161
+ # * The line width, style and color of the circle/rectangle are taken from the widget's
162
+ # border style. See HexaPDF::Type::Annotations::Widget#border_style.
163
+ #
164
+ # * The background color is determined by the widget's background color. See
165
+ # HexaPDF::Type::Annotations::Widget#background_color.
166
+ #
167
+ # * The symbol (marker) as well as its size and color are determined by the marker style of
168
+ # the widget. See HexaPDF::Type::Annotations::Widget#marker_style for details.
169
+ #
170
+ # Examples:
171
+ #
172
+ # widget.border_style(color: 0)
173
+ # widget.background_color(1)
174
+ # widget.marker_style(style: :circle, size: 0, color: 0)
175
+ # # => default appearance
176
+ def create_radio_button_appearances
177
+ unless @widget.appearance&.normal_appearance&.value&.size == 2
178
+ raise HexaPDF::Error, "Widget of radio button doesn't define unique name for on state"
179
+ end
180
+
181
+ on_name = (@widget.appearance.normal_appearance.value.keys - [:Off]).first
182
+ border_style = @widget.border_style
183
+ marker_style = @widget.marker_style
184
+
185
+ rect = update_widget(@field[:V] == on_name ? on_name : :Off, border_style.width)
186
+
187
+ off_form = @widget.appearance.normal_appearance[:Off] =
188
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
189
+ apply_background_and_border(border_style, off_form.canvas,
190
+ circular: marker_style.style == :circle)
191
+
192
+ on_form = @widget.appearance.normal_appearance[on_name] =
193
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
194
+ canvas = on_form.canvas
195
+ apply_background_and_border(border_style, canvas,
196
+ circular: marker_style.style == :circle)
197
+ canvas.save_graphics_state do
198
+ draw_marker(canvas, rect, border_style.width, @widget.marker_style)
199
+ end
200
+ end
201
+
202
+ # Creates the appropriate appearances for text fields.
203
+ #
204
+ # The following describes how the appearance is built:
205
+ #
206
+ # * The font, font size and font color are taken from the associated field's default
207
+ # appearance string. See VariableTextField.
208
+ #
209
+ # * The widget's rectangle /Rect must be defined. If the height is zero, it is auto-sized
210
+ # based on the font size. If additionally the font size is zero, a font size of
211
+ # +acro_form.default_font_size+ is used. If the width is zero, the
212
+ # +acro_form.text_field.default_width+ value is used. In such cases the rectangle is
213
+ # appropriately updated.
214
+ #
215
+ # * The line width, style and color of the rectangle are taken from the widget's border
216
+ # style. See HexaPDF::Type::Annotations::Widget#border_style.
217
+ #
218
+ # * The background color is determined by the widget's background color. See
219
+ # HexaPDF::Type::Annotations::Widget#background_color.
220
+ #
221
+ # Note: Multiline, comb and rich text fields are currently not supported!
222
+ def create_text_appearances
223
+ font_name, font_size = @field.parse_default_appearance_string
224
+ default_resources = @document.acro_form.default_resources
225
+ font = default_resources.font(font_name).font_wrapper
226
+ unless font
227
+ fallback_font_name, fallback_font_options = @document.config['acro_form.fallback_font']
228
+ if fallback_font_name
229
+ font = @document.fonts.add(fallback_font_name, **(fallback_font_options || {}))
230
+ else
231
+ raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
232
+ end
233
+ end
234
+ style = HexaPDF::Layout::Style.new(font: font)
235
+ border_style = @widget.border_style
236
+ padding = [1, border_style.width].max
237
+
238
+ @widget[:AS] = :N
239
+ @widget.flag(:print)
240
+ rect = @widget[:Rect]
241
+ rect.width = @document.config['acro_form.text_field.default_width'] if rect.width == 0
242
+ if rect.height == 0
243
+ style.font_size = \
244
+ (font_size == 0 ? @document.config['acro_form.default_font_size'] : font_size)
245
+ rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
246
+ end
247
+
248
+ form = (@widget[:AP] ||= {})[:N] = @document.add({Type: :XObject, Subtype: :Form,
249
+ BBox: [0, 0, rect.width, rect.height]})
250
+ form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
251
+
252
+ canvas = form.canvas
253
+ apply_background_and_border(border_style, canvas)
254
+ style.font_size = calculate_font_size(font, font_size, rect, border_style)
255
+
256
+ canvas.marked_content_sequence(:Tx) do
257
+ if (value = @field.field_value)
258
+ canvas.save_graphics_state do
259
+ canvas.rectangle(padding, padding, rect.width - 2 * padding,
260
+ rect.height - 2 * padding).clip_path.end_path
261
+ fragment = HexaPDF::Layout::TextFragment.create(value, style)
262
+ # Adobe seems to be left/right-aligning based on twice the border width and
263
+ # vertically centering based on the cap height, if enough space is available
264
+ x = case @field.text_alignment
265
+ when :left then 2 * padding
266
+ when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
267
+ when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
268
+ end
269
+ cap_height = font.wrapped_font.cap_height * font.scaling_factor / 1000.0 *
270
+ style.font_size
271
+ y = padding + (rect.height - 2 * padding - cap_height) / 2.0
272
+ y = padding - style.scaled_font_descender if y < 0
273
+ fragment.draw(canvas, x, y)
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ alias create_combo_box_appearances create_text_appearances
280
+
281
+ private
282
+
283
+ # Updates the widget and returns its (possibly modified) rectangle.
284
+ #
285
+ # The following changes are made:
286
+ #
287
+ # * Sets the appearance state to +appearance_state+.
288
+ # * Sets the :print flag.
289
+ # * Adjusts the rectangle based on the default font size and the given border width if its
290
+ # width and/or height are zero.
291
+ def update_widget(appearance_state, border_width)
292
+ @widget[:AS] = appearance_state
293
+ @widget.flag(:print)
294
+
295
+ default_font_size = @document.config['acro_form.default_font_size']
296
+ rect = @widget[:Rect]
297
+ rect.width = default_font_size + 2 * border_width if rect.width == 0
298
+ rect.height = default_font_size + 2 * border_width if rect.height == 0
299
+ rect
300
+ end
301
+
302
+ # Applies the background and border style of the widget annotation to the appearances.
303
+ #
304
+ # If +circular+ is +true+, then the border is drawn as inscribed circle instead of as
305
+ # rectangle.
306
+ def apply_background_and_border(border_style, canvas, circular: false)
307
+ rect = @widget[:Rect]
308
+ background_color = @widget.background_color
309
+
310
+ if (border_style.width > 0 && border_style.color) || background_color
311
+ canvas.save_graphics_state
312
+ if background_color
313
+ canvas.fill_color(background_color)
314
+ if circular
315
+ canvas.circle(rect.width / 2.0, rect.height / 2.0,
316
+ [rect.width / 2.0, rect.height / 2.0].min)
317
+ else
318
+ canvas.rectangle(0, 0, rect.width, rect.height)
319
+ end
320
+ canvas.fill
321
+ end
322
+ if border_style.color
323
+ offset = [0.5, border_style.width / 2.0].max
324
+ width, height = rect.width - 2 * offset, rect.height - 2 * offset
325
+ canvas.stroke_color(border_style.color).line_width(border_style.width)
326
+ if border_style.style == :underlined # TODO: :beveleded, :inset
327
+ if circular
328
+ canvas.arc(rect.width / 2.0, rect.height / 2.0,
329
+ a: [width / 2.0, height / 2.0].min,
330
+ start_angle: 180, end_angle: 0)
331
+ else
332
+ canvas.line(offset, offset, offset + width, offset)
333
+ end
334
+ else
335
+ canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
336
+ if circular
337
+ canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
338
+ else
339
+ canvas.rectangle(offset, offset, width, height)
340
+ end
341
+ end
342
+ canvas.stroke
343
+ end
344
+ canvas.restore_graphics_state
345
+ end
346
+ end
347
+
348
+ # Draws the marker defined by the marker style inside the widget's rectangle.
349
+ #
350
+ # This method can only used for check boxes and radio buttons!
351
+ def draw_marker(canvas, rect, border_width, marker_style)
352
+ if @field.radio_button? && marker_style.style == :circle
353
+ # Acrobat handles this specially
354
+ canvas.
355
+ fill_color(marker_style.color).
356
+ circle(rect.width / 2.0, rect.height / 2.0,
357
+ ([rect.width / 2.0, rect.height / 2.0].min - border_width) / 2).
358
+ fill
359
+ elsif marker_style.style == :cross # Acrobat just places a cross inside
360
+ canvas.
361
+ stroke_color(marker_style.color).
362
+ line(border_width, border_width, rect.width - border_width,
363
+ rect.height - border_width).
364
+ line(border_width, rect.height - border_width, rect.width - border_width,
365
+ border_width).
366
+ stroke
367
+ else
368
+ font = @document.fonts.add('ZapfDingbats')
369
+ mark = font.decode_utf8(@widget[:MK]&.[](:CA) || '4').first
370
+ square_width = [rect.width, rect.height].min - 2 * border_width
371
+ font_size = (marker_style.size == 0 ? square_width : marker_style.size)
372
+ mark_width = mark.width * font.scaling_factor * font_size / 1000.0
373
+ mark_height = (mark.y_max - mark.y_min) * font.scaling_factor * font_size / 1000.0
374
+ x_offset = (rect.width - square_width) / 2.0 + (square_width - mark_width) / 2.0
375
+ y_offset = (rect.height - square_width) / 2.0 + (square_width - mark_height) / 2.0 -
376
+ (mark.y_min * font.scaling_factor * font_size / 1000.0)
377
+
378
+ canvas.font(font, size: font_size)
379
+ canvas.fill_color(marker_style.color)
380
+ canvas.move_text_cursor(offset: [x_offset, y_offset]).show_glyphs_only([mark])
381
+ end
382
+ end
383
+
384
+ # Calculates the font size for text fields based on the font and font size of the default
385
+ # appearance string, the annotation rectangle and the border style.
386
+ def calculate_font_size(font, font_size, rect, border_style)
387
+ if font_size == 0
388
+ unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
389
+ font.scaling_factor / 1000.0
390
+ # The constant factor was found empirically by checking what Adobe Reader etc. do
391
+ (rect.height - 2 * border_style.width) / unit_font_size * 0.83
392
+ else
393
+ font_size
394
+ end
395
+ end
396
+
397
+ end
398
+
399
+ end
400
+ end
401
+ end
@@ -0,0 +1,300 @@
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-2020 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/type/acro_form/field'
38
+ require 'hexapdf/type/acro_form/appearance_generator'
39
+
40
+ module HexaPDF
41
+ module Type
42
+ module AcroForm
43
+
44
+ # AcroForm button fields represent interactive controls to be used with the mouse.
45
+ #
46
+ # They are divided into push buttons (things to click on), check boxes and radio buttons. All
47
+ # of these are represented with this class.
48
+ #
49
+ # To create a push button, check box or radio button field, use the appropriate convenience
50
+ # methods on the main Form instance (HexaPDF::Document#acro_form). By using those methods,
51
+ # everything needed is automatically set up.
52
+ #
53
+ # == Type Specific Field Flags
54
+ #
55
+ # :no_toggle_to_off:: Only used with radio buttons fields. If this flag is set, one button
56
+ # needs to be selected at all times. Otherwise, clicking on the selected
57
+ # button deselects it.
58
+ #
59
+ # :radio:: If this flag is set, the field is a set of radio buttons. Otherwise it is a check
60
+ # box. Additionally, the :pushbutton flag needs to be clear.
61
+ #
62
+ # :push_button:: The field represents a pushbutton without a permanent value.
63
+ #
64
+ # :radios_in_unison:: A group of radio buttons with the same value for the on state will turn
65
+ # on or off in unison.
66
+ #
67
+ # See: PDF1.7 s12.7.4.2
68
+ class ButtonField < Field
69
+
70
+ define_field :Opt, type: PDFArray, version: '1.4'
71
+
72
+ # All inheritable dictionary fields for button fields.
73
+ INHERITABLE_FIELDS = (superclass::INHERITABLE_FIELDS + [:Opt]).freeze
74
+
75
+ # Updated list of field flags.
76
+ FLAGS_BIT_MAPPING = superclass::FLAGS_BIT_MAPPING.merge(
77
+ {
78
+ no_toggle_to_off: 14,
79
+ radio: 15,
80
+ push_button: 16,
81
+ radios_in_unison: 25,
82
+ }
83
+ ).freeze
84
+
85
+ # Initializes the button field to be a push button.
86
+ #
87
+ # This method should only be called directly after creating a new button field because it
88
+ # doesn't completely reset the object.
89
+ def initialize_as_push_button
90
+ self[:V] = nil
91
+ flag(:push_button)
92
+ unflag(:radio)
93
+ end
94
+
95
+ # Initializes the button field to be a check box.
96
+ #
97
+ # This method should only be called directly after creating a new button field because it
98
+ # doesn't completely reset the object.
99
+ def initialize_as_check_box
100
+ self[:V] = :Off
101
+ unflag(:push_button)
102
+ unflag(:radio)
103
+ end
104
+
105
+ # Initializes the button field to be a radio button.
106
+ #
107
+ # This method should only be called directly after creating a new button field because it
108
+ # doesn't completely reset the object.
109
+ def initialize_as_radio_button
110
+ self[:V] = :Off
111
+ unflag(:push_button)
112
+ flag(:radio)
113
+ end
114
+
115
+ # Returns +true+ if this button field represents a push button.
116
+ def push_button?
117
+ flagged?(:push_button)
118
+ end
119
+
120
+ # Returns +true+ if this button field represents a check box.
121
+ def check_box?
122
+ !push_button? && !flagged?(:radio)
123
+ end
124
+
125
+ # Returns +true+ if this button field represents a radio button set.
126
+ def radio_button?
127
+ !push_button? && flagged?(:radio)
128
+ end
129
+
130
+ # Returns the field value which depends on the concrete type.
131
+ #
132
+ # Push buttons:: They don't have a value, so +nil+ is always returned.
133
+ #
134
+ # Check boxes:: For check boxes that are in the on state the value +true+ is returned.
135
+ # Otherwise +false+ is returned.
136
+ #
137
+ # Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the name of
138
+ # the specific radio button that is selected is returned.
139
+ def field_value
140
+ normalized_field_value(:V)
141
+ end
142
+
143
+ # Sets the field value which depends on the concrete type.
144
+ #
145
+ # Push buttons:: Since push buttons don't store any value, the given value is ignored and
146
+ # nothing is stored for them (e.g a no-op).
147
+ #
148
+ # Check boxes:: Use +true+ for checking the box, i.e. toggling it to the on state, and
149
+ # +false+ for unchecking it.
150
+ #
151
+ # Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
152
+ # the name of a radio button that should be turned on.
153
+ def field_value=(value)
154
+ normalized_field_value_set(:V, value)
155
+ end
156
+
157
+ # Returns the default field value.
158
+ #
159
+ # See: #field_value
160
+ def default_field_value
161
+ normalized_field_value(:DV)
162
+ end
163
+
164
+ # Sets the default field value.
165
+ #
166
+ # See: #field_value=
167
+ def default_field_value=(value)
168
+ normalized_field_value_set(:DV, value)
169
+ end
170
+
171
+ # Returns the concrete button field type, either :push_button, :check_box or :radio_button.
172
+ def concrete_field_type
173
+ if push_button?
174
+ :push_button
175
+ elsif radio_button?
176
+ :radio_button
177
+ else
178
+ :check_box
179
+ end
180
+ end
181
+
182
+ # Returns the name used for setting the check box to the on state.
183
+ #
184
+ # Defaults to :Yes if no other name could be determined.
185
+ def check_box_on_name
186
+ each_widget.to_a.first&.appearance&.normal_appearance&.value&.each_key&.
187
+ find {|key| key != :Off } || :Yes
188
+ end
189
+
190
+ # Returns the array of values that can be used for the field value of the radio button.
191
+ def radio_button_values
192
+ each_widget.map do |widget|
193
+ widget.appearance&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
194
+ end.compact
195
+ end
196
+
197
+ # Creates a widget for the button field.
198
+ #
199
+ # If +defaults+ is +true+, then default values will be set on the widget so that it uses a
200
+ # default appearance.
201
+ #
202
+ # If the widget is created for a radio button field, the +value+ argument needs to set to
203
+ # the value (a symbol) this widget represents. It can be used with #field_value= to set this
204
+ # specific widget of the radio button set to on.
205
+ #
206
+ # See: Field#create_widget, AppearanceGenerator button field methods
207
+ def create_widget(page, defaults: true, value: nil, **values)
208
+ super(page, allow_embedded: !radio_button?, **values).tap do |widget|
209
+ if check_box?
210
+ widget[:AP] = {N: {Yes: nil, Off: nil}}
211
+ elsif radio_button?
212
+ raise ArgumentError, "Argument value has to be provided for radio buttons" unless value
213
+ widget[:AP] = {N: {value => nil, Off: nil}}
214
+ end
215
+ next unless defaults
216
+ widget.border_style(color: 0, width: 1, style: (push_button? ? :beveled : :solid))
217
+ widget.background_color(push_button? ? 0.5 : 255)
218
+ widget.marker_style(style: check_box? ? :check : :circle) unless push_button?
219
+ end
220
+ end
221
+
222
+ # Creates appropriate appearances for all widgets if they don't already exist.
223
+ #
224
+ # The created appearance streams depend on the actual type of the button field. See
225
+ # AppearanceGenerator for the details.
226
+ def create_appearances
227
+ appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
228
+ each_widget do |widget|
229
+ next if widget.appearance?
230
+ if check_box?
231
+ appearance_generator_class.new(widget).create_check_box_appearances
232
+ elsif radio_button?
233
+ appearance_generator_class.new(widget).create_radio_button_appearances
234
+ else
235
+ raise HexaPDF::Error, "Push buttons not yet supported"
236
+ end
237
+ end
238
+ end
239
+
240
+ # Updates the widgets so that they reflect the current field value.
241
+ def update_widgets
242
+ return if push_button?
243
+ value = self[:V]
244
+ each_widget do |widget|
245
+ widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ # Returns the normalized field value for the given key which can be :V or :DV.
252
+ #
253
+ # See #field_value for details.
254
+ def normalized_field_value(key)
255
+ if push_button?
256
+ nil
257
+ elsif check_box?
258
+ self[key] == check_box_on_name
259
+ elsif radio_button?
260
+ self[key] == :Off ? nil : self[key]
261
+ end
262
+ end
263
+
264
+ # Sets the key, either :V or :DV, to the value. The given normalized value is first
265
+ # transformed into the expected value depending on the specific field type.
266
+ #
267
+ # See #field_value= for details.
268
+ def normalized_field_value_set(key, value)
269
+ return if push_button?
270
+ self[key] = if check_box?
271
+ value == true ? check_box_on_name : :Off
272
+ elsif value.nil?
273
+ :Off
274
+ elsif radio_button_values.include?(value)
275
+ value
276
+ else
277
+ @document.config['acro_form.on_invalid_value'].call(self, value)
278
+ end
279
+ update_widgets
280
+ end
281
+
282
+ def perform_validation #:nodoc:
283
+ if field_type != :Btn
284
+ yield("Field /FT of AcroForm button field has to be :Btn", true)
285
+ self[:FT] = :Btn
286
+ end
287
+
288
+ super
289
+
290
+ unless key?(:V)
291
+ yield("Button field has no value set, defaulting to :Off", true)
292
+ self[:V] = :Off
293
+ end
294
+ end
295
+
296
+ end
297
+
298
+ end
299
+ end
300
+ end