hexapdf 1.2.0 → 1.3.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/inspect.rb +13 -4
  5. data/lib/hexapdf/composer.rb +14 -0
  6. data/lib/hexapdf/configuration.rb +5 -0
  7. data/lib/hexapdf/document/annotations.rb +60 -2
  8. data/lib/hexapdf/document/layout.rb +45 -6
  9. data/lib/hexapdf/error.rb +11 -3
  10. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  11. data/lib/hexapdf/layout/style.rb +101 -7
  12. data/lib/hexapdf/object.rb +2 -2
  13. data/lib/hexapdf/pdf_array.rb +25 -3
  14. data/lib/hexapdf/tokenizer.rb +4 -1
  15. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  16. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  17. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  18. data/lib/hexapdf/type/annotation.rb +12 -0
  19. data/lib/hexapdf/type/annotations/appearance_generator.rb +75 -0
  20. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  21. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  22. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  23. data/lib/hexapdf/type/annotations/line.rb +4 -35
  24. data/lib/hexapdf/type/annotations/square.rb +65 -0
  25. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  26. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  27. data/lib/hexapdf/type/annotations.rb +5 -0
  28. data/lib/hexapdf/version.rb +1 -1
  29. data/test/hexapdf/document/test_annotations.rb +22 -0
  30. data/test/hexapdf/document/test_layout.rb +24 -2
  31. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  32. data/test/hexapdf/layout/test_style.rb +27 -2
  33. data/test/hexapdf/test_composer.rb +7 -0
  34. data/test/hexapdf/test_object.rb +1 -1
  35. data/test/hexapdf/test_pdf_array.rb +36 -3
  36. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  37. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  38. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  39. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  40. data/test/hexapdf/type/annotations/test_appearance_generator.rb +84 -0
  41. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  42. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  43. data/test/hexapdf/type/annotations/test_line.rb +0 -20
  44. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  45. metadata +9 -2
@@ -0,0 +1,77 @@
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-2025 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/annotations'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # This is the base class for the square and circle markup annotations which display a
44
+ # rectangle or ellipse inside the annotation rectangle.
45
+ #
46
+ # The styling is done through methods included by various modules:
47
+ #
48
+ # * Changing the line width, line dash pattern and color is done using the method
49
+ # BorderStyling#border_style. While that method allows special styling of the line (like
50
+ # :beveled), only a simple line dash pattern is supported by the square and circle
51
+ # annotations.
52
+ #
53
+ # * The interior color can be changed through InteriorColor#interior_color.
54
+ #
55
+ # * The border effect can be changed through BorderEffect#border_effect. Note that cloudy
56
+ # borders are not supported.
57
+ #
58
+ # See: PDF2.0 s12.5.6.8, HexaPDF::Type::Annotations::Square,
59
+ # HexaPDF::Type::Annotations::Circle, HexaPDF::Type::MarkupAnnotation
60
+ class SquareCircle < MarkupAnnotation
61
+
62
+ include BorderStyling
63
+ include BorderEffect
64
+ include InteriorColor
65
+
66
+ # Field Subtype is defined in the two subclasses
67
+ define_field :BS, type: :Border
68
+ define_field :IC, type: PDFArray, version: '1.4'
69
+ define_field :BE, type: :XXBorderEffect, version: '1.5'
70
+ # Array instead of Rectangle, see https://github.com/pdf-association/pdf-issues/issues/524
71
+ define_field :RD, type: PDFArray, version: '1.5'
72
+
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -124,15 +124,21 @@ module HexaPDF
124
124
  end
125
125
  end
126
126
 
127
- # Describes the marker style of a check box or radio button widget.
127
+ # Describes the marker style of a check box, radio button or push button widget.
128
128
  class MarkerStyle
129
129
 
130
- # The kind of marker that is shown inside the widget. Can either be one of the symbols
131
- # +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character
132
- # string. The latter is interpreted using the ZapfDingbats font.
130
+ # The kind of marker that is shown inside the widget.
133
131
  #
134
- # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default
135
- # marker for the field type.
132
+ # Radion buttons and check boxes::
133
+ # Can either be one of the symbols +:check+, +:circle+, +:cross+, +:diamond+,
134
+ # +:square+ or +:star+, or a one character string. The latter is interpreted using the
135
+ # ZapfDingbats font.
136
+ #
137
+ # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the
138
+ # default marker for the field type.
139
+ #
140
+ # Push buttons:
141
+ # The caption string.
136
142
  attr_reader :style
137
143
 
138
144
  # The size of the marker in PDF points that is shown inside the widget. The special value
@@ -143,27 +149,40 @@ module HexaPDF
143
149
  # HexaPDF::Content::ColorSpace.
144
150
  attr_reader :color
145
151
 
152
+ # The resource name of the font that should be used for the caption.
153
+ #
154
+ # This is only used for push button widgets.
155
+ attr_reader :font_name
156
+
146
157
  # Creates a new instance with the given values.
147
- def initialize(style, size, color)
158
+ def initialize(style, size, color, font_name)
148
159
  @style = style
149
160
  @size = size
150
161
  @color = color
162
+ @font_name = font_name
151
163
  end
152
164
 
153
165
  end
154
166
 
155
167
  # :call-seq:
156
- # widget.marker_style => marker_style
157
- # widget.marker_style(style: nil, size: nil, color: nil) => widget
168
+ # widget.marker_style => marker_style
169
+ # widget.marker_style(style: nil, size: nil, color: nil, font_name: nil) => widget
158
170
  #
159
171
  # Returns a MarkerStyle instance representing the marker style of the widget when no
160
172
  # argument is given. Otherwise sets the button marker style of the widget and returns self.
161
173
  #
162
- # This method returns valid information only for check boxes and radio buttons!
174
+ # This method returns valid information only for check boxes, radio buttons and push buttons!
163
175
  #
164
- # When setting a marker style, arguments that are not provided will use the default: a black
165
- # auto-sized checkmark (i.e. :check for for check boxes) or circle (:circle for radio
166
- # buttons). This also means that multiple invocations will reset *all* prior values.
176
+ # When setting a marker style, arguments that are not provided will use the default:
177
+ #
178
+ # * For check boxes a black auto-sized checkmark (i.e. :check)
179
+ # * For radio buttons a black auto-sized circle (i.e. :circle)
180
+ # * For push buttons a black 9pt empty text using Helvetica
181
+ #
182
+ # This also means that multiple invocations will reset *all* prior values.
183
+ #
184
+ # Note that the +font_name+ argument must be a valid HexaPDF font name (this is in contrast
185
+ # to MarkerStyle#font_name which returns the resource name of the font).
167
186
  #
168
187
  # Note: The marker is called "normal caption" in the PDF 2.0 spec and the /CA entry of the
169
188
  # associated appearance characteristics dictionary. The marker size and color are set using
@@ -171,13 +190,18 @@ module HexaPDF
171
190
  # does it).
172
191
  #
173
192
  # See: PDF2.0 s12.5.6.19 and s12.7.4.3
174
- def marker_style(style: nil, size: nil, color: nil)
193
+ def marker_style(style: nil, size: nil, color: nil, font_name: nil)
175
194
  field = form_field
176
- if style || size || color
177
- style ||= (field.check_box? ? :check : :cicrle)
178
- size ||= 0
195
+ if style || size || color || font_name
196
+ style ||= case field.concrete_field_type
197
+ when :check_box then :check
198
+ when :radio_button then :circle
199
+ when :push_button then ''
200
+ end
201
+ size ||= (field.push_button? ? 9 : 0)
179
202
  color = Content::ColorSpace.device_color_from_specification(color || 0)
180
203
  serialized_color = Content::ColorSpace.serialize_device_color(color)
204
+ font_name ||= 'Helvetica'
181
205
 
182
206
  self[:MK] ||= {}
183
207
  self[:MK][:CA] = case style
@@ -191,7 +215,13 @@ module HexaPDF
191
215
  else
192
216
  raise ArgumentError, "Unknown value #{style} for argument 'style'"
193
217
  end
194
- self[:DA] = "/ZaDb #{size} Tf #{serialized_color}".strip
218
+ self[:DA] = if field.push_button?
219
+ name = document.acro_form(create: true).default_resources.
220
+ add_font(document.fonts.add(font_name).pdf_object)
221
+ "/#{name} #{size} Tf #{serialized_color}".strip
222
+ else
223
+ "/ZaDb #{size} Tf #{serialized_color}".strip
224
+ end
195
225
  else
196
226
  style = case self[:MK]&.[](:CA)
197
227
  when '4' then :check
@@ -211,12 +241,12 @@ module HexaPDF
211
241
  size = 0
212
242
  color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0])
213
243
  if (da = self[:DA] || field[:DA])
214
- _, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
244
+ font_name, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
215
245
  size = da_size || size
216
246
  color = da_color || color
217
247
  end
218
248
 
219
- MarkerStyle.new(style, size, color)
249
+ MarkerStyle.new(style, size, color, font_name)
220
250
  end
221
251
  end
222
252
 
@@ -51,6 +51,11 @@ module HexaPDF
51
51
  autoload(:BorderStyling, 'hexapdf/type/annotations/border_styling')
52
52
  autoload(:Line, 'hexapdf/type/annotations/line')
53
53
  autoload(:AppearanceGenerator, 'hexapdf/type/annotations/appearance_generator')
54
+ autoload(:BorderEffect, 'hexapdf/type/annotations/border_effect')
55
+ autoload(:InteriorColor, 'hexapdf/type/annotations/interior_color')
56
+ autoload(:SquareCircle, 'hexapdf/type/annotations/square_circle')
57
+ autoload(:Square, 'hexapdf/type/annotations/square')
58
+ autoload(:Circle, 'hexapdf/type/annotations/circle')
54
59
 
55
60
  end
56
61
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.2.0'
40
+ VERSION = '1.3.0'
41
41
 
42
42
  end
@@ -18,6 +18,8 @@ describe HexaPDF::Document::Annotations do
18
18
  it "delegates to the actual create_TYPE implementation" do
19
19
  annot = @annots.create(:line, @page, start_point: [0, 0], end_point: [10, 10])
20
20
  assert_equal(:Line, annot[:Subtype])
21
+ annot = @annots.create(:rectangle, @page, 10, 20, 30, 40)
22
+ assert_equal(:Square, annot[:Subtype])
21
23
  end
22
24
  end
23
25
 
@@ -30,4 +32,24 @@ describe HexaPDF::Document::Annotations do
30
32
  assert_equal(annot, @page[:Annots].first)
31
33
  end
32
34
  end
35
+
36
+ describe "create_rectangle" do
37
+ it "creates an appropriate square annotation object" do
38
+ annot = @annots.create(:rectangle, @page, 10, 20, 30, 40)
39
+ assert_equal(:Annot, annot[:Type])
40
+ assert_equal(:Square, annot[:Subtype])
41
+ assert_equal([10, 20, 40, 60], annot[:Rect])
42
+ assert_equal(annot, @page[:Annots].first)
43
+ end
44
+ end
45
+
46
+ describe "create_ellipse" do
47
+ it "creates an appropriate circle annotation object" do
48
+ annot = @annots.create(:ellipse, @page, 100, 100, a: 30, b: 40)
49
+ assert_equal(:Annot, annot[:Type])
50
+ assert_equal(:Circle, annot[:Subtype])
51
+ assert_equal([70, 60, 130, 140], annot[:Rect])
52
+ assert_equal(annot, @page[:Annots].first)
53
+ end
54
+ end
33
55
  end
@@ -146,6 +146,16 @@ describe HexaPDF::Document::Layout do
146
146
  end
147
147
  end
148
148
 
149
+ describe "style?" do
150
+ it "returns true if a given style is defined" do
151
+ assert(@layout.style?(:base))
152
+ end
153
+
154
+ it "returns false if a given style is not defined" do
155
+ refute(@layout.style?(:unknown))
156
+ end
157
+ end
158
+
149
159
  describe "styles" do
150
160
  it "returns the existing styles" do
151
161
  @layout.style(:test, font_size: 20)
@@ -168,6 +178,16 @@ describe HexaPDF::Document::Layout do
168
178
  assert_kind_of(HexaPDF::Font::Type1Wrapper, style.font)
169
179
  end
170
180
 
181
+ it "uses the font_bold property when resolving a font name to a font wrapper" do
182
+ style = @layout.send(:retrieve_style, {font: 'Helvetica', font_bold: true})
183
+ assert_equal('Helvetica-Bold', style.font.wrapped_font.font_name)
184
+ end
185
+
186
+ it "uses the font_italic property when resolving a font name to a font wrapper" do
187
+ style = @layout.send(:retrieve_style, {font: 'Helvetica', font_italic: true})
188
+ assert_equal('Helvetica-Oblique', style.font.wrapped_font.font_name)
189
+ end
190
+
171
191
  it "sets the :base style's font if no font is set" do
172
192
  @layout.style(:base, font: 'Helvetica')
173
193
  style = @layout.send(:retrieve_style, {})
@@ -203,7 +223,8 @@ describe HexaPDF::Document::Layout do
203
223
 
204
224
  describe "box" do
205
225
  it "creates the request box" do
206
- box = @layout.box(:column, columns: 3, gaps: 20, width: 15, height: 30, style: {font_size: 10},
226
+ box = @layout.box(:column, columns: 3, width: 15, height: 30,
227
+ style: {font_size: 10, box_options: {gaps: 20}},
207
228
  properties: {key: :value})
208
229
  assert_equal(15, box.width)
209
230
  assert_equal(30, box.height)
@@ -431,9 +452,10 @@ describe HexaPDF::Document::Layout do
431
452
 
432
453
  describe "table_box" do
433
454
  it "creates a table box" do
434
- box = @layout.table_box([['m']], column_widths: [100], header: proc { [['a']] },
455
+ box = @layout.table_box([['m']], header: proc { [['a']] },
435
456
  footer: proc { [['b']] }, cell_style: {background_color: "red"},
436
457
  width: 100, height: 300, style: {background_color: "blue"},
458
+ box_options: {column_widths: [100]},
437
459
  properties: {key: :value}, border: {width: 1})
438
460
  assert_equal(100, box.width)
439
461
  assert_equal(300, box.height)
@@ -27,6 +27,16 @@ describe HexaPDF::Font::TrueType::Subsetter do
27
27
  assert_equal(value, @subsetter.subset_glyph_id(5))
28
28
  end
29
29
 
30
+ it "doesn't use certain subset glyph IDs for performance reasons" do
31
+ 1.upto(93) {|i| @subsetter.use_glyph(i) }
32
+ # glyph 0, 93 used glyph, 4 special glyphs
33
+ assert_equal(1 + 93 + 4, @subsetter.instance_variable_get(:@glyph_map).size)
34
+ 1.upto(12) {|i| assert_equal(i, @subsetter.subset_glyph_id(i), "id=#{i}") }
35
+ 13.upto(38) {|i| assert_equal(i + 1, @subsetter.subset_glyph_id(i), "id=#{i}") }
36
+ 39.upto(88) {|i| assert_equal(i + 3, @subsetter.subset_glyph_id(i), "id=#{i}") }
37
+ 89.upto(93) {|i| assert_equal(i + 4, @subsetter.subset_glyph_id(i), "id=#{i}") }
38
+ end
39
+
30
40
  it "creates the subset font file" do
31
41
  gid = @font[:cmap].preferred_table[0x41]
32
42
  @subsetter.use_glyph(gid)
@@ -738,6 +738,30 @@ describe HexaPDF::Layout::Style do
738
738
  end
739
739
  end
740
740
 
741
+ describe "each_property" do
742
+ it "yields all set properties with their values" do
743
+ @style.font_size = 5
744
+ @style.line_spacing = 1.2
745
+ assert_equal(0.005, @style.scaled_font_size)
746
+ assert_equal([[:font, @style.font], [:font_size, 5], [:horizontal_scaling, 100],
747
+ [:line_spacing, @style.line_spacing], [:subscript, false], [:superscript, false]],
748
+ @style.each_property.to_a.sort)
749
+ end
750
+ end
751
+
752
+ describe "merge" do
753
+ it "merges all set properties" do
754
+ @style.font_size = 5
755
+ @style.line_spacing = 1.2
756
+ new_style = HexaPDF::Layout::Style.new
757
+ new_style.update(font_size: 3, line_spacing: {type: :fixed, value: 2.5})
758
+ new_style.merge(@style)
759
+ assert_equal(5, new_style.font_size)
760
+ assert_equal(:proportional, new_style.line_spacing.type)
761
+ assert_equal(1.2, new_style.line_spacing.value)
762
+ end
763
+ end
764
+
741
765
  it "has several simple and dynamically generated properties with default values" do
742
766
  @style = HexaPDF::Layout::Style.new
743
767
  assert_raises(HexaPDF::Error) { @style.font }
@@ -780,6 +804,7 @@ describe HexaPDF::Layout::Style do
780
804
  assert_equal(:left, @style.align)
781
805
  assert_equal(:top, @style.valign)
782
806
  assert_equal(:default, @style.mask_mode)
807
+ assert_equal({}, @style.box_options)
783
808
  end
784
809
 
785
810
  it "allows using a non-standard setter for generated properties" do
@@ -790,8 +815,8 @@ describe HexaPDF::Layout::Style do
790
815
  @style.stroke_dash_pattern(5, 2)
791
816
  assert_equal([[5], 2], @style.stroke_dash_pattern.to_operands)
792
817
 
793
- @style.line_spacing(1.2)
794
- assert_equal([:proportional, 1.2], [@style.line_spacing.type, @style.line_spacing.value])
818
+ @style.line_spacing(HexaPDF::Layout::Style::LineSpacing.new(type: :double))
819
+ assert_equal([:proportional, 2], [@style.line_spacing.type, @style.line_spacing.value])
795
820
  end
796
821
 
797
822
  it "allows checking for valid values" do
@@ -127,6 +127,13 @@ describe HexaPDF::Composer do
127
127
  end
128
128
  end
129
129
 
130
+ describe "style?" do
131
+ it "delegates to layout.style?" do
132
+ @composer.document.layout.style(:header, font_size: 20)
133
+ assert(@composer.style?(:header))
134
+ end
135
+ end
136
+
130
137
  describe "styles" do
131
138
  it "delegates to layout.styles" do
132
139
  @composer.styles(base: {font_size: 30}, other: {font_size: 40})
@@ -197,7 +197,7 @@ describe HexaPDF::Object do
197
197
  @obj.define_singleton_method(:perform_validation) { raise "Unknown" }
198
198
  invoked = []
199
199
  refute(@obj.validate {|*a| invoked << a })
200
- assert_equal([["Error: Unexpected value encountered", false, @obj]], invoked)
200
+ assert_equal([["Unexpected error encountered: Unknown", false, @obj]], invoked)
201
201
  end
202
202
  end
203
203
 
@@ -131,9 +131,42 @@ describe HexaPDF::PDFArray do
131
131
  end
132
132
  end
133
133
 
134
- it "allows deleting elements that are selected using a block" do
135
- @array.reject! {|item| item == :data }
136
- assert_equal([1, "deref", @array[2]], @array[0, 5])
134
+ describe "reject!" do
135
+ it "allows deleting elements that are selected using a block" do
136
+ assert_same(@array, @array.reject! {|item| item == :data })
137
+ assert_equal([1, "deref", @array[2]], @array.to_a)
138
+ end
139
+
140
+ it "returns nil if no elements were deleted" do
141
+ assert_nil(@array.reject! {|item| false })
142
+ end
143
+
144
+ it "returns an enumerator if no block is given" do
145
+ assert_kind_of(Enumerator, @array.reject!)
146
+ end
147
+ end
148
+
149
+ describe "map!" do
150
+ it "maps elements in-place to the return values of the block" do
151
+ assert_same(@array, @array.map! {|item| 5 })
152
+ assert_equal([5, 5, 5, 5], @array.to_a)
153
+ end
154
+
155
+ it "returns an enumerator if no block is given" do
156
+ assert_kind_of(Enumerator, @array.reject!)
157
+ end
158
+ end
159
+
160
+ describe "compact!" do
161
+ it "removes all nil elements and returns self" do
162
+ @array << nil
163
+ assert_same(@array, @array.compact!)
164
+ assert_equal(4, @array.size)
165
+ end
166
+
167
+ it "returns nil if no elements were removed" do
168
+ assert_nil(@array.compact!)
169
+ end
137
170
  end
138
171
 
139
172
  describe "index" do
@@ -373,12 +373,87 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
373
373
  describe "push buttons" do
374
374
  before do
375
375
  @field.initialize_as_push_button
376
- @widget = @field.create_widget(@page, Rect: [0, 0, 0, 0])
376
+ @widget = @field.create_widget(@page, Rect: [0, 0, 100, 50])
377
+ @widget.marker_style(style: 'Test')
377
378
  @generator = HexaPDF::Type::AcroForm::AppearanceGenerator.new(@widget)
378
379
  end
379
380
 
380
- it "fails because it is not implemented yet" do
381
- assert_raises(HexaPDF::Error) { @generator.create_appearances }
381
+ it "set the print flag on the widgets" do
382
+ @generator.create_appearances
383
+ assert(@widget.flagged?(:print))
384
+ end
385
+
386
+ it "removes the hidden flag on the widgets" do
387
+ @widget.flag(:hidden)
388
+ @generator.create_appearances
389
+ refute(@widget.flagged?(:hidden))
390
+ end
391
+
392
+ it "adds an appropriate form XObject" do
393
+ @generator.create_appearances
394
+ form = @widget[:AP][:N]
395
+ assert_equal(:XObject, form.type)
396
+ assert_equal(:Form, form[:Subtype])
397
+ assert_equal([0, 0, 100, 50], form[:BBox])
398
+ assert_equal(@doc.acro_form.default_resources[:Font][:F1], form[:Resources][:Font][:F1])
399
+ end
400
+
401
+ it "re-uses the existing form XObject" do
402
+ @generator.create_appearances
403
+ form = @widget[:AP][:N]
404
+ form[:key] = :value
405
+ form.delete(:Subtype)
406
+ @widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Dictionary)
407
+
408
+ @generator.create_appearances
409
+ assert_equal(form, @widget[:AP][:N])
410
+ refute(form.key?(:key))
411
+ end
412
+
413
+ describe "takes the rotation into account" do
414
+ def check_rotation(angle, width, height, matrix)
415
+ @widget[:MK][:R] = angle
416
+ @generator.create_appearances
417
+ form = @widget[:AP][:N]
418
+ assert_equal([0, 0, width, height], form[:BBox].value)
419
+ assert_equal(matrix, form[:Matrix].value)
420
+ end
421
+
422
+ it "works for 0 degrees" do
423
+ check_rotation(-360, @widget[:Rect].width, @widget[:Rect].height, [1, 0, 0, 1, 0, 0])
424
+ end
425
+
426
+ it "works for 90 degrees" do
427
+ check_rotation(450, @widget[:Rect].height, @widget[:Rect].width, [0, 1, -1, 0, 0, 0])
428
+ end
429
+
430
+ it "works for 180 degrees" do
431
+ check_rotation(180, @widget[:Rect].width, @widget[:Rect].height, [0, -1, -1, 0, 0, 0])
432
+ end
433
+
434
+ it "works for 270 degrees" do
435
+ check_rotation(-90, @widget[:Rect].height, @widget[:Rect].width, [0, -1, 1, 0, 0, 0])
436
+ end
437
+ end
438
+
439
+ it "adds the button title in the center" do
440
+ @generator.create_appearances
441
+ assert_operators(@widget[:AP][:N].stream,
442
+ [[:save_graphics_state],
443
+ [:set_device_gray_non_stroking_color, [0.5]],
444
+ [:append_rectangle, [0, 0, 100, 50]],
445
+ [:fill_path_non_zero],
446
+ [:append_rectangle, [0.5, 0.5, 99.0, 49.0]],
447
+ [:stroke_path],
448
+ [:restore_graphics_state],
449
+ [:save_graphics_state],
450
+ [:set_font_and_size, [:F1, 9]],
451
+ [:begin_text],
452
+ [:move_text, [41.2475, 22.7005]],
453
+ [:show_text, ["Test"]],
454
+ [:end_text],
455
+ [:restore_graphics_state]],
456
+ )
382
457
  end
383
458
  end
384
459
  end
@@ -236,6 +236,13 @@ describe HexaPDF::Type::AcroForm::ButtonField do
236
236
  assert(widget[:AP][:N][:test])
237
237
  end
238
238
 
239
+ it "works for push buttons" do
240
+ @field.initialize_as_push_button
241
+ @field.create_widget(@doc.pages.add, Rect: [0, 0, 100, 50])
242
+ @field.create_appearances
243
+ assert(@field[:AP][:N])
244
+ end
245
+
239
246
  it "won't generate appearances if they already exist" do
240
247
  widget = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
241
248
  @field.create_appearances
@@ -262,12 +269,6 @@ describe HexaPDF::Type::AcroForm::ButtonField do
262
269
  refute_same(yes, widget.appearance_dict.normal_appearance[:Yes])
263
270
  end
264
271
 
265
- it "fails for push buttons as they are not implemented yet" do
266
- @field.flag(:push_button)
267
- @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
268
- assert_raises(HexaPDF::Error) { @field.create_appearances }
269
- end
270
-
271
272
  it "uses the configuration option acro_form.appearance_generator" do
272
273
  @doc.config['acro_form.appearance_generator'] = 'NonExistent'
273
274
  assert_raises(Exception) { @field.create_appearances }
@@ -193,9 +193,14 @@ describe HexaPDF::Type::AcroForm::Field do
193
193
 
194
194
  it "extracts an embedded widget into a standalone object if necessary" do
195
195
  widget1 = @field.create_widget(@page, Rect: [1, 2, 3, 4])
196
+ # Make sure that the field/widget looks like as if it has been loaded from a file
197
+ @doc.revisions.current.update(widget1)
198
+ assert_equal(@field, widget1)
199
+
196
200
  widget2 = @field.create_widget(@doc.pages.add, Rect: [2, 1, 4, 3])
197
201
  kids = @field[:Kids]
198
202
 
203
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, @doc.object(@field.oid))
199
204
  assert_equal(2, kids.length)
200
205
  refute_same(widget1, kids[0])
201
206
  assert_same(widget2, kids[1])
@@ -558,13 +558,23 @@ describe HexaPDF::Type::AcroForm::Form do
558
558
  assert_equal(:Tx4, @acro_form[:Fields][2][:Kids][0][:T])
559
559
  assert_equal(@acro_form[:Fields][2], @acro_form[:Fields][2][:Kids][0][:Parent])
560
560
  end
561
+
562
+ it "ensures that objects loaded as widget are stored as field" do
563
+ @acro_form[:Fields][2] = @doc.add({T: :WidgetField, Type: :Annot, Subtype: :Widget})
564
+ assert_kind_of(HexaPDF::Type::Annotations::Widget, @acro_form[:Fields][2])
565
+
566
+ assert(@acro_form.validate)
567
+ field = @acro_form[:Fields][0]
568
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, field)
569
+ assert_equal(:WidgetField, field.full_field_name)
570
+ end
561
571
  end
562
572
 
563
573
  describe "combining fields with the same name" do
564
574
  before do
565
575
  @acro_form[:Fields] = [
566
576
  @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 1]}),
567
- @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
577
+ @merged_field = @doc.add({T: 'e', Subtype: :Widget, Rect: [0, 0, 0, 2]}),
568
578
  @doc.add({T: 'Tx2'}),
569
579
  @doc.add({T: 'e', Kids: [{Subtype: :Widget, Rect: [0, 0, 0, 3]}]}),
570
580
  ]
@@ -576,6 +586,12 @@ describe HexaPDF::Type::AcroForm::Form do
576
586
  assert_equal([[0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
577
587
  @acro_form.field_by_name('e').each_widget.map {|w| w[:Rect] })
578
588
  end
589
+
590
+ it "deletes the combined and now unneeded field objects" do
591
+ assert(@acro_form.validate)
592
+ assert(@merged_field.null?)
593
+ assert(@doc.object(@merged_field.oid).null?)
594
+ end
579
595
  end
580
596
 
581
597
  describe "automatically creates the terminal fields; appearances" do