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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +1 -1
- data/lib/hexapdf/cli/inspect.rb +13 -4
- data/lib/hexapdf/composer.rb +14 -0
- data/lib/hexapdf/configuration.rb +5 -0
- data/lib/hexapdf/document/annotations.rb +60 -2
- data/lib/hexapdf/document/layout.rb +45 -6
- data/lib/hexapdf/error.rb +11 -3
- data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
- data/lib/hexapdf/layout/style.rb +101 -7
- data/lib/hexapdf/object.rb +2 -2
- data/lib/hexapdf/pdf_array.rb +25 -3
- data/lib/hexapdf/tokenizer.rb +4 -1
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +7 -6
- data/lib/hexapdf/type/annotation.rb +12 -0
- data/lib/hexapdf/type/annotations/appearance_generator.rb +75 -0
- data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
- data/lib/hexapdf/type/annotations/circle.rb +65 -0
- data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
- data/lib/hexapdf/type/annotations/line.rb +4 -35
- data/lib/hexapdf/type/annotations/square.rb +65 -0
- data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
- data/lib/hexapdf/type/annotations/widget.rb +50 -20
- data/lib/hexapdf/type/annotations.rb +5 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_annotations.rb +22 -0
- data/test/hexapdf/document/test_layout.rb +24 -2
- data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
- data/test/hexapdf/layout/test_style.rb +27 -2
- data/test/hexapdf/test_composer.rb +7 -0
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_pdf_array.rb +36 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
- data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +17 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +84 -0
- data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
- data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
- data/test/hexapdf/type/annotations/test_line.rb +0 -20
- data/test/hexapdf/type/annotations/test_widget.rb +35 -0
- 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
|
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.
|
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
|
-
#
|
135
|
-
#
|
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
|
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
|
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:
|
165
|
-
#
|
166
|
-
#
|
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 ||=
|
178
|
-
|
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] =
|
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
|
-
|
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
|
|
data/lib/hexapdf/version.rb
CHANGED
@@ -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,
|
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']],
|
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(
|
794
|
-
assert_equal([:proportional,
|
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})
|
data/test/hexapdf/test_object.rb
CHANGED
@@ -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([["
|
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
|
-
|
135
|
-
|
136
|
-
|
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,
|
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 "
|
381
|
-
|
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
|