hexapdf 0.26.2 → 0.28.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 +115 -1
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/019-acro_form.rb +14 -3
- data/examples/020-column_box.rb +3 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +8 -8
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +3 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +4 -17
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +35 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +14 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +7 -1
- data/lib/hexapdf/tokenizer.rb +15 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +61 -8
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +19 -1
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +95 -64
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +5 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +27 -6
- data/test/hexapdf/test_parser.rb +19 -2
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +63 -12
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +10 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +65 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +54 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +4 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +103 -45
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- metadata +61 -4
data/lib/hexapdf/revision.rb
CHANGED
|
@@ -229,16 +229,22 @@ module HexaPDF
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
# :call-seq:
|
|
232
|
-
# revision.each_modified_object {|obj| block } -> revision
|
|
233
|
-
# revision.each_modified_object -> Enumerator
|
|
232
|
+
# revision.each_modified_object(delete: false, all: all) {|obj| block } -> revision
|
|
233
|
+
# revision.each_modified_object(delete: false, all: all) -> Enumerator
|
|
234
234
|
#
|
|
235
|
-
# Calls the given block once for each object that has been modified since it was loaded.
|
|
236
|
-
# object and cross-reference streams are ignored.
|
|
235
|
+
# Calls the given block once for each object that has been modified since it was loaded. Added
|
|
236
|
+
# or eleted object and cross-reference streams as well as signature dictionaries are ignored.
|
|
237
|
+
#
|
|
238
|
+
# +delete+:: If the +delete+ argument is set to +true+, each modified object is deleted from the
|
|
239
|
+
# active objects.
|
|
240
|
+
#
|
|
241
|
+
# +all+:: If the +all+ argument is set to +true+, added object and cross-reference streams are
|
|
242
|
+
# also yielded.
|
|
237
243
|
#
|
|
238
244
|
# Note that this also means that for revisions without an associated cross-reference section all
|
|
239
245
|
# loaded objects will be yielded.
|
|
240
|
-
def each_modified_object
|
|
241
|
-
return to_enum(__method__) unless block_given?
|
|
246
|
+
def each_modified_object(delete: false, all: false)
|
|
247
|
+
return to_enum(__method__, delete: delete, all: all) unless block_given?
|
|
242
248
|
|
|
243
249
|
@objects.each do |oid, gen, obj|
|
|
244
250
|
if @xref_section.entry?(oid, gen)
|
|
@@ -259,20 +265,17 @@ module HexaPDF
|
|
|
259
265
|
end
|
|
260
266
|
next if values_unchanged && streams_are_same
|
|
261
267
|
end
|
|
268
|
+
elsif !all && (obj.type == :XRef || obj.type == :ObjStm)
|
|
269
|
+
next
|
|
262
270
|
end
|
|
263
271
|
|
|
264
272
|
yield(obj)
|
|
273
|
+
@objects.delete(oid) if delete
|
|
265
274
|
end
|
|
266
275
|
|
|
267
276
|
self
|
|
268
277
|
end
|
|
269
278
|
|
|
270
|
-
# Resets the revision by deleting all loaded and added objects from it.
|
|
271
|
-
def reset_objects
|
|
272
|
-
@objects = HexaPDF::Utils::ObjectHash.new
|
|
273
|
-
@all_objects_loaded = false
|
|
274
|
-
end
|
|
275
|
-
|
|
276
279
|
private
|
|
277
280
|
|
|
278
281
|
# Loads a single object from the associated cross-reference section.
|
data/lib/hexapdf/revisions.rb
CHANGED
|
@@ -93,15 +93,21 @@ module HexaPDF
|
|
|
93
93
|
seen_xref_offsets[stm] = true
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
if parser.linearized? && !trailer.key?(:Prev)
|
|
97
|
+
merge_revision = offset
|
|
98
|
+
end
|
|
99
|
+
|
|
96
100
|
if merge_revision == offset
|
|
97
101
|
xref_section.merge!(revisions.first.xref_section)
|
|
102
|
+
offset = trailer[:Prev] # Get possible next offset before overwriting trailer
|
|
98
103
|
trailer = revisions.first.trailer
|
|
99
104
|
revisions.shift
|
|
105
|
+
else
|
|
106
|
+
offset = trailer[:Prev]
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
|
|
103
110
|
xref_section: xref_section, loader: object_loader))
|
|
104
|
-
offset = trailer[:Prev]
|
|
105
111
|
end
|
|
106
112
|
rescue HexaPDF::MalformedPDFError
|
|
107
113
|
raise unless (reconstructed_revision = parser.reconstructed_revision)
|
data/lib/hexapdf/tokenizer.rb
CHANGED
|
@@ -274,7 +274,7 @@ module HexaPDF
|
|
|
274
274
|
TOKEN_CACHE[str]
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
-
REFERENCE_RE = /[#{WHITESPACE}]+([
|
|
277
|
+
REFERENCE_RE = /[#{WHITESPACE}]+([+]?\d+)[#{WHITESPACE}]+R#{WHITESPACE_OR_DELIMITER_RE}/ # :nodoc:
|
|
278
278
|
|
|
279
279
|
# Parses the number (integer or real) at the current position.
|
|
280
280
|
#
|
|
@@ -285,7 +285,14 @@ module HexaPDF
|
|
|
285
285
|
tmp = val.to_i
|
|
286
286
|
# Handle object references, see PDF1.7 s7.3.10
|
|
287
287
|
prepare_string_scanner(10)
|
|
288
|
-
|
|
288
|
+
if @ss.scan(REFERENCE_RE)
|
|
289
|
+
tmp = if tmp > 0
|
|
290
|
+
Reference.new(tmp, @ss[1].to_i)
|
|
291
|
+
else
|
|
292
|
+
maybe_raise("Invalid indirect object reference (#{tmp},#{@ss[1].to_i})")
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
end
|
|
289
296
|
tmp
|
|
290
297
|
elsif val.match?(/\A[+-]?(?:\d+\.\d*|\.\d+)\z/)
|
|
291
298
|
val << '0' if val.getbyte(-1) == 46 # dot '.'
|
|
@@ -315,21 +322,20 @@ module HexaPDF
|
|
|
315
322
|
parentheses = 1
|
|
316
323
|
|
|
317
324
|
while parentheses != 0
|
|
318
|
-
data = scan_until(/
|
|
319
|
-
char = @ss[1]
|
|
325
|
+
data = scan_until(/[()\\\r]/)
|
|
320
326
|
unless data
|
|
321
327
|
raise HexaPDF::MalformedPDFError.new("Unclosed literal string found", pos: pos)
|
|
322
328
|
end
|
|
323
329
|
|
|
324
330
|
str << data
|
|
325
331
|
prepare_string_scanner if @ss.eos?
|
|
326
|
-
case
|
|
327
|
-
when
|
|
328
|
-
when
|
|
329
|
-
when
|
|
332
|
+
case @ss.string.getbyte(@ss.pos - 1)
|
|
333
|
+
when 41 then parentheses -= 1 # )
|
|
334
|
+
when 40 then parentheses += 1 # (
|
|
335
|
+
when 13 # \r
|
|
330
336
|
str[-1] = "\n"
|
|
331
337
|
@ss.pos += 1 if @ss.peek(1) == "\n"
|
|
332
|
-
when
|
|
338
|
+
when 92 # \\
|
|
333
339
|
str.chop!
|
|
334
340
|
byte = @ss.get_byte
|
|
335
341
|
if (data = LITERAL_STRING_ESCAPE_MAP[byte])
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
35
|
#++
|
|
36
36
|
|
|
37
|
+
require 'json'
|
|
37
38
|
require 'hexapdf/error'
|
|
38
39
|
require 'hexapdf/layout/style'
|
|
39
40
|
require 'hexapdf/layout/text_fragment'
|
|
@@ -74,12 +75,10 @@ module HexaPDF
|
|
|
74
75
|
def create_appearances
|
|
75
76
|
case @field.field_type
|
|
76
77
|
when :Btn
|
|
77
|
-
if @field.
|
|
78
|
-
|
|
79
|
-
elsif @field.radio_button?
|
|
80
|
-
create_radio_button_appearances
|
|
78
|
+
if @field.push_button?
|
|
79
|
+
create_push_button_appearances
|
|
81
80
|
else
|
|
82
|
-
|
|
81
|
+
create_check_box_appearances
|
|
83
82
|
end
|
|
84
83
|
when :Tx, :Ch
|
|
85
84
|
create_text_appearances
|
|
@@ -88,23 +87,24 @@ module HexaPDF
|
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
89
|
|
|
91
|
-
# Creates the appropriate appearances for check boxes.
|
|
90
|
+
# Creates the appropriate appearances for check boxes and radio buttons.
|
|
92
91
|
#
|
|
93
|
-
# The unchecked box is always represented by the appearance with
|
|
94
|
-
# more than one other key besides the /Off key, the first one is
|
|
95
|
-
# the checked box.
|
|
92
|
+
# The unchecked box or unselected radio button is always represented by the appearance with
|
|
93
|
+
# the key /Off. If there is more than one other key besides the /Off key, the first one is
|
|
94
|
+
# used for the appearance of the checked box or selected radio button.
|
|
96
95
|
#
|
|
97
|
-
# For unchecked boxes an empty rectangle is drawn.
|
|
98
|
-
#
|
|
99
|
-
#
|
|
96
|
+
# For unchecked boxes an empty rectangle is drawn. Similarly, for unselected radio buttons
|
|
97
|
+
# an empty circle (if the marker is :circle) or rectangle is drawn. When checked or
|
|
98
|
+
# selected, a symbol from the ZapfDingbats font is placed inside. How this is exactly done
|
|
99
|
+
# depends on the following values:
|
|
100
100
|
#
|
|
101
101
|
# * The widget's rectangle /Rect must be defined. If the height and/or width of the
|
|
102
102
|
# rectangle are zero, they are based on the configuration option
|
|
103
103
|
# +acro_form.default_font_size+ and widget's border width. In such a case the rectangle is
|
|
104
104
|
# appropriately updated.
|
|
105
105
|
#
|
|
106
|
-
# * The line width, style and color of the rectangle are taken from the widget's
|
|
107
|
-
# style. See HexaPDF::Type::Annotations::Widget#border_style.
|
|
106
|
+
# * The line width, style and color of the cirle/rectangle are taken from the widget's
|
|
107
|
+
# border style. See HexaPDF::Type::Annotations::Widget#border_style.
|
|
108
108
|
#
|
|
109
109
|
# * The background color is determined by the widget's background color. See
|
|
110
110
|
# HexaPDF::Type::Annotations::Widget#background_color.
|
|
@@ -114,94 +114,66 @@ module HexaPDF
|
|
|
114
114
|
#
|
|
115
115
|
# Examples:
|
|
116
116
|
#
|
|
117
|
+
# # check box: default appearance
|
|
117
118
|
# widget.border_style(color: 0)
|
|
118
119
|
# widget.background_color(1)
|
|
119
120
|
# widget.marker_style(style: :check, size: 0, color: 0)
|
|
120
|
-
# # => default appearance
|
|
121
121
|
#
|
|
122
|
+
# # check box: no visible rectangle, gray background, cross mark when checked
|
|
122
123
|
# widget.border_style(color: :transparent, width: 2)
|
|
123
124
|
# widget.background_color(0.7)
|
|
124
125
|
# widget.marker_style(style: :cross)
|
|
125
|
-
# # => no visible rectangle, gray background, cross mark when checked
|
|
126
|
-
def create_check_box_appearances
|
|
127
|
-
appearance_keys = @widget.appearance_dict&.normal_appearance&.value&.keys || []
|
|
128
|
-
on_name = (appearance_keys - [:Off]).first
|
|
129
|
-
unless on_name
|
|
130
|
-
raise HexaPDF::Error, "Widget of check box doesn't define name for on state"
|
|
131
|
-
end
|
|
132
|
-
border_style = @widget.border_style
|
|
133
|
-
border_width = border_style.width
|
|
134
|
-
|
|
135
|
-
rect = update_widget(@field[:V], border_width)
|
|
136
|
-
|
|
137
|
-
off_form = @widget.appearance_dict.normal_appearance[:Off] =
|
|
138
|
-
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
|
139
|
-
apply_background_and_border(border_style, off_form.canvas)
|
|
140
|
-
|
|
141
|
-
on_form = @widget.appearance_dict.normal_appearance[on_name] =
|
|
142
|
-
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
|
143
|
-
canvas = on_form.canvas
|
|
144
|
-
apply_background_and_border(border_style, canvas)
|
|
145
|
-
canvas.save_graphics_state do
|
|
146
|
-
draw_marker(canvas, rect, border_width, @widget.marker_style)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Creates the appropriate appearances for radio buttons.
|
|
151
|
-
#
|
|
152
|
-
# For unselected radio buttons an empty circle (if the marker is :circle) or rectangle is
|
|
153
|
-
# drawn inside the widget annotation's rectangle. When selected, a symbol from the
|
|
154
|
-
# ZapfDingbats font is placed inside. How this is exactly done depends on the following
|
|
155
|
-
# values:
|
|
156
|
-
#
|
|
157
|
-
# * The widget's rectangle /Rect must be defined. If the height and/or width of the
|
|
158
|
-
# rectangle are zero, they are based on the configuration option
|
|
159
|
-
# +acro_form.default_font_size+ and the widget's border width. In such a case the
|
|
160
|
-
# rectangle is appropriately updated.
|
|
161
|
-
#
|
|
162
|
-
# * The line width, style and color of the circle/rectangle are taken from the widget's
|
|
163
|
-
# border style. See HexaPDF::Type::Annotations::Widget#border_style.
|
|
164
|
-
#
|
|
165
|
-
# * The background color is determined by the widget's background color. See
|
|
166
|
-
# HexaPDF::Type::Annotations::Widget#background_color.
|
|
167
|
-
#
|
|
168
|
-
# * The symbol (marker) as well as its size and color are determined by the marker style of
|
|
169
|
-
# the widget. See HexaPDF::Type::Annotations::Widget#marker_style for details.
|
|
170
|
-
#
|
|
171
|
-
# Examples:
|
|
172
126
|
#
|
|
127
|
+
# # radio button: default appearance
|
|
173
128
|
# widget.border_style(color: 0)
|
|
174
129
|
# widget.background_color(1)
|
|
175
130
|
# widget.marker_style(style: :circle, size: 0, color: 0)
|
|
176
|
-
|
|
177
|
-
def create_radio_button_appearances
|
|
131
|
+
def create_check_box_appearances
|
|
178
132
|
appearance_keys = @widget.appearance_dict&.normal_appearance&.value&.keys || []
|
|
179
133
|
on_name = (appearance_keys - [:Off]).first
|
|
180
134
|
unless on_name
|
|
181
|
-
raise HexaPDF::Error, "Widget of
|
|
135
|
+
raise HexaPDF::Error, "Widget of button field doesn't define name for on state"
|
|
182
136
|
end
|
|
183
137
|
|
|
138
|
+
@widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
|
|
139
|
+
@widget.flag(:print)
|
|
140
|
+
|
|
184
141
|
border_style = @widget.border_style
|
|
185
142
|
marker_style = @widget.marker_style
|
|
143
|
+
circular = (@field.radio_button? && marker_style.style == :circle)
|
|
144
|
+
|
|
145
|
+
default_font_size = @document.config['acro_form.default_font_size']
|
|
146
|
+
rect = @widget[:Rect]
|
|
147
|
+
rect.width = default_font_size + 2 * border_style.width if rect.width == 0
|
|
148
|
+
rect.height = default_font_size + 2 * border_style.width if rect.height == 0
|
|
186
149
|
|
|
187
|
-
|
|
150
|
+
width, height, matrix = perform_rotation(rect.width, rect.height)
|
|
188
151
|
|
|
189
152
|
off_form = @widget.appearance_dict.normal_appearance[:Off] =
|
|
190
|
-
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0,
|
|
191
|
-
|
|
192
|
-
|
|
153
|
+
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
|
154
|
+
Matrix: matrix})
|
|
155
|
+
apply_background_and_border(border_style, off_form.canvas, circular: circular)
|
|
193
156
|
|
|
194
157
|
on_form = @widget.appearance_dict.normal_appearance[on_name] =
|
|
195
|
-
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0,
|
|
158
|
+
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
|
159
|
+
Matrix: matrix})
|
|
196
160
|
canvas = on_form.canvas
|
|
197
|
-
apply_background_and_border(border_style, canvas,
|
|
198
|
-
circular: marker_style.style == :circle)
|
|
161
|
+
apply_background_and_border(border_style, canvas, circular: circular)
|
|
199
162
|
canvas.save_graphics_state do
|
|
200
|
-
draw_marker(canvas,
|
|
163
|
+
draw_marker(canvas, width, height, border_style.width, marker_style)
|
|
201
164
|
end
|
|
202
165
|
end
|
|
203
166
|
|
|
204
|
-
|
|
167
|
+
alias create_radio_button_appearances create_check_box_appearances
|
|
168
|
+
|
|
169
|
+
# Creates the appropriate appearances for push buttons.
|
|
170
|
+
#
|
|
171
|
+
# This is currently a dummy implementation raising an error.
|
|
172
|
+
def create_push_button_appearances
|
|
173
|
+
raise HexaPDF::Error, "Push button appearance generation not yet supported"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Creates the appropriate appearances for text fields, combo box fields and list box fields.
|
|
205
177
|
#
|
|
206
178
|
# The following describes how the appearance is built:
|
|
207
179
|
#
|
|
@@ -242,31 +214,33 @@ module HexaPDF
|
|
|
242
214
|
rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
|
|
243
215
|
end
|
|
244
216
|
|
|
217
|
+
width, height, matrix = perform_rotation(rect.width, rect.height)
|
|
218
|
+
|
|
245
219
|
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
|
246
220
|
# Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
|
|
247
221
|
# key; we can do this since we know this has to be a Form object
|
|
248
222
|
form = @document.wrap(form, type: :XObject, subtype: :Form) unless form[:Subtype] == :Form
|
|
249
|
-
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0,
|
|
223
|
+
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
|
224
|
+
Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
|
|
250
225
|
form.contents = ''
|
|
251
|
-
form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
|
|
252
226
|
|
|
253
227
|
canvas = form.canvas
|
|
254
228
|
apply_background_and_border(border_style, canvas)
|
|
255
|
-
style.font_size = calculate_font_size(font, font_size,
|
|
229
|
+
style.font_size = calculate_font_size(font, font_size, height, border_style)
|
|
256
230
|
style.clear_cache
|
|
257
231
|
|
|
258
232
|
canvas.marked_content_sequence(:Tx) do
|
|
259
233
|
if @field.field_value || @field.concrete_field_type == :list_box
|
|
260
234
|
canvas.save_graphics_state do
|
|
261
|
-
canvas.rectangle(padding, padding,
|
|
262
|
-
|
|
235
|
+
canvas.rectangle(padding, padding, width - 2 * padding,
|
|
236
|
+
height - 2 * padding).clip_path.end_path
|
|
263
237
|
case @field.concrete_field_type
|
|
264
238
|
when :multiline_text_field
|
|
265
|
-
draw_multiline_text(canvas,
|
|
239
|
+
draw_multiline_text(canvas, width, height, style, padding)
|
|
266
240
|
when :list_box
|
|
267
|
-
draw_list_box(canvas,
|
|
241
|
+
draw_list_box(canvas, width, height, style, padding)
|
|
268
242
|
else
|
|
269
|
-
draw_single_line_text(canvas,
|
|
243
|
+
draw_single_line_text(canvas, width, height, style, padding)
|
|
270
244
|
end
|
|
271
245
|
end
|
|
272
246
|
end
|
|
@@ -278,23 +252,20 @@ module HexaPDF
|
|
|
278
252
|
|
|
279
253
|
private
|
|
280
254
|
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
rect.width = default_font_size + 2 * border_width if rect.width == 0
|
|
296
|
-
rect.height = default_font_size + 2 * border_width if rect.height == 0
|
|
297
|
-
rect
|
|
255
|
+
# Performs the rotation specified in /R of the appearance characteristics dictionary and
|
|
256
|
+
# returns the correct width, height and Form XObject matrix.
|
|
257
|
+
def perform_rotation(width, height)
|
|
258
|
+
matrix = case (@widget[:MK]&.[](:R) || 0) % 360
|
|
259
|
+
when 90
|
|
260
|
+
width, height = height, width
|
|
261
|
+
[0, 1, -1, 0, 0, 0]
|
|
262
|
+
when 270
|
|
263
|
+
width, height = height, width
|
|
264
|
+
[0, -1, 1, 0, 0, 0]
|
|
265
|
+
when 180
|
|
266
|
+
[0, -1, -1, 0, 0, 0]
|
|
267
|
+
end
|
|
268
|
+
[width, height, matrix]
|
|
298
269
|
end
|
|
299
270
|
|
|
300
271
|
# Applies the background and border style of the widget annotation to the appearances.
|
|
@@ -353,31 +324,32 @@ module HexaPDF
|
|
|
353
324
|
# Draws the marker defined by the marker style inside the widget's rectangle.
|
|
354
325
|
#
|
|
355
326
|
# This method can only used for check boxes and radio buttons!
|
|
356
|
-
def draw_marker(canvas,
|
|
327
|
+
def draw_marker(canvas, width, height, border_width, marker_style)
|
|
357
328
|
if @field.radio_button? && marker_style.style == :circle
|
|
358
329
|
# Acrobat handles this specially
|
|
359
330
|
canvas.
|
|
360
331
|
fill_color(marker_style.color).
|
|
361
|
-
circle(
|
|
362
|
-
([
|
|
332
|
+
circle(width / 2.0, height / 2.0,
|
|
333
|
+
([width / 2.0, height / 2.0].min - border_width) / 2).
|
|
363
334
|
fill
|
|
364
335
|
elsif marker_style.style == :cross # Acrobat just places a cross inside
|
|
365
336
|
canvas.
|
|
366
337
|
stroke_color(marker_style.color).
|
|
367
|
-
line(border_width, border_width,
|
|
368
|
-
|
|
369
|
-
line(border_width,
|
|
338
|
+
line(border_width, border_width, width - border_width,
|
|
339
|
+
height - border_width).
|
|
340
|
+
line(border_width, height - border_width, width - border_width,
|
|
370
341
|
border_width).
|
|
371
342
|
stroke
|
|
372
343
|
else
|
|
373
344
|
font = @document.fonts.add('ZapfDingbats')
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
marker_string = @widget[:MK]&.[](:CA).to_s
|
|
346
|
+
mark = font.decode_utf8(marker_string.empty? ? '4' : marker_string).first
|
|
347
|
+
square_width = [width, height].min - 2 * border_width
|
|
376
348
|
font_size = (marker_style.size == 0 ? square_width : marker_style.size)
|
|
377
349
|
mark_width = mark.width * font.scaling_factor * font_size / 1000.0
|
|
378
350
|
mark_height = (mark.y_max - mark.y_min) * font.scaling_factor * font_size / 1000.0
|
|
379
|
-
x_offset = (
|
|
380
|
-
y_offset = (
|
|
351
|
+
x_offset = (width - square_width) / 2.0 + (square_width - mark_width) / 2.0
|
|
352
|
+
y_offset = (height - square_width) / 2.0 + (square_width - mark_height) / 2.0 -
|
|
381
353
|
(mark.y_min * font.scaling_factor * font_size / 1000.0)
|
|
382
354
|
|
|
383
355
|
canvas.font(font, size: font_size)
|
|
@@ -387,8 +359,9 @@ module HexaPDF
|
|
|
387
359
|
end
|
|
388
360
|
|
|
389
361
|
# Draws a single line of text inside the widget's rectangle.
|
|
390
|
-
def draw_single_line_text(canvas,
|
|
391
|
-
value = @field.field_value
|
|
362
|
+
def draw_single_line_text(canvas, width, height, style, padding)
|
|
363
|
+
value, text_color = apply_javascript_formatting(@field.field_value)
|
|
364
|
+
style.fill_color = text_color if text_color
|
|
392
365
|
fragment = HexaPDF::Layout::TextFragment.create(value, style)
|
|
393
366
|
|
|
394
367
|
if @field.concrete_field_type == :comb_text_field
|
|
@@ -396,7 +369,7 @@ module HexaPDF
|
|
|
396
369
|
raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
|
|
397
370
|
end
|
|
398
371
|
new_items = []
|
|
399
|
-
cell_width =
|
|
372
|
+
cell_width = width.to_f / @field[:MaxLen]
|
|
400
373
|
scaled_cell_width = cell_width / style.scaled_font_size.to_f
|
|
401
374
|
fragment.items.each_cons(2) do |a, b|
|
|
402
375
|
new_items << a << -(scaled_cell_width - a.width / 2.0 - b.width / 2.0)
|
|
@@ -415,8 +388,8 @@ module HexaPDF
|
|
|
415
388
|
# Adobe seems to be left/right-aligning based on twice the border width
|
|
416
389
|
x = case @field.text_alignment
|
|
417
390
|
when :left then 2 * padding
|
|
418
|
-
when :right then [
|
|
419
|
-
when :center then [(
|
|
391
|
+
when :right then [width - 2 * padding - fragment.width, 2 * padding].max
|
|
392
|
+
when :center then [(width - fragment.width) / 2.0, 2 * padding].max
|
|
420
393
|
end
|
|
421
394
|
end
|
|
422
395
|
|
|
@@ -424,13 +397,13 @@ module HexaPDF
|
|
|
424
397
|
# available
|
|
425
398
|
cap_height = style.font.wrapped_font.cap_height * style.font.scaling_factor / 1000.0 *
|
|
426
399
|
style.font_size
|
|
427
|
-
y = padding + (
|
|
400
|
+
y = padding + (height - 2 * padding - cap_height) / 2.0
|
|
428
401
|
y = padding - style.scaled_font_descender if y < 0
|
|
429
402
|
fragment.draw(canvas, x, y)
|
|
430
403
|
end
|
|
431
404
|
|
|
432
405
|
# Draws multiple lines of text inside the widget's rectangle.
|
|
433
|
-
def draw_multiline_text(canvas,
|
|
406
|
+
def draw_multiline_text(canvas, width, height, style, padding)
|
|
434
407
|
items = [Layout::TextFragment.create(@field.field_value, style)]
|
|
435
408
|
layouter = Layout::TextLayouter.new(style)
|
|
436
409
|
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
|
@@ -440,22 +413,22 @@ module HexaPDF
|
|
|
440
413
|
style.font_size = 12 # Adobe seems to use this as starting point
|
|
441
414
|
style.clear_cache
|
|
442
415
|
loop do
|
|
443
|
-
result = layouter.fit(items,
|
|
416
|
+
result = layouter.fit(items, width - 4 * padding, height - 4 * padding)
|
|
444
417
|
break if result.status == :success || style.font_size <= 4 # don't make text too small
|
|
445
418
|
style.font_size -= 1
|
|
446
419
|
style.clear_cache
|
|
447
420
|
end
|
|
448
421
|
else
|
|
449
|
-
result = layouter.fit(items,
|
|
422
|
+
result = layouter.fit(items, width - 4 * padding, 2**20)
|
|
450
423
|
end
|
|
451
424
|
|
|
452
425
|
unless result.lines.empty?
|
|
453
|
-
result.draw(canvas, 2 * padding,
|
|
426
|
+
result.draw(canvas, 2 * padding, height - 2 * padding - result.lines[0].height / 2.0)
|
|
454
427
|
end
|
|
455
428
|
end
|
|
456
429
|
|
|
457
430
|
# Draws the visible option items of the list box in the widget's rectangle.
|
|
458
|
-
def draw_list_box(canvas,
|
|
431
|
+
def draw_list_box(canvas, width, height, style, padding)
|
|
459
432
|
option_items = @field.option_items
|
|
460
433
|
top_index = @field.list_box_top_index
|
|
461
434
|
items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
|
|
@@ -464,18 +437,18 @@ module HexaPDF
|
|
|
464
437
|
|
|
465
438
|
layouter = Layout::TextLayouter.new(style)
|
|
466
439
|
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
|
467
|
-
result = layouter.fit(items,
|
|
440
|
+
result = layouter.fit(items, width - 4 * padding, height)
|
|
468
441
|
|
|
469
442
|
unless result.lines.empty?
|
|
470
443
|
top_gap = style.line_spacing.gap(result.lines[0], result.lines[0])
|
|
471
444
|
line_height = style.line_spacing.baseline_distance(result.lines[0], result.lines[0])
|
|
472
445
|
canvas.fill_color(153, 193, 218) # Adobe's color for selection highlighting
|
|
473
|
-
indices.map! {|i|
|
|
474
|
-
next if y + line_height >
|
|
475
|
-
canvas.rectangle(padding, y,
|
|
446
|
+
indices.map! {|i| height - padding - (i - top_index + 1) * line_height }.each do |y|
|
|
447
|
+
next if y + line_height > height || y + line_height < padding
|
|
448
|
+
canvas.rectangle(padding, y, width - 2 * padding, line_height)
|
|
476
449
|
end
|
|
477
450
|
canvas.fill if canvas.graphics_object == :path
|
|
478
|
-
result.draw(canvas, 2 * padding,
|
|
451
|
+
result.draw(canvas, 2 * padding, height - padding - top_gap)
|
|
479
452
|
end
|
|
480
453
|
end
|
|
481
454
|
|
|
@@ -501,8 +474,8 @@ module HexaPDF
|
|
|
501
474
|
end
|
|
502
475
|
|
|
503
476
|
# Calculates the font size for text fields based on the font and font size of the default
|
|
504
|
-
# appearance string, the annotation rectangle and the border style.
|
|
505
|
-
def calculate_font_size(font, font_size,
|
|
477
|
+
# appearance string, the annotation rectangle's height and the border style.
|
|
478
|
+
def calculate_font_size(font, font_size, height, border_style)
|
|
506
479
|
if font_size == 0
|
|
507
480
|
case @field.concrete_field_type
|
|
508
481
|
when :multiline_text_field
|
|
@@ -513,13 +486,86 @@ module HexaPDF
|
|
|
513
486
|
unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
|
|
514
487
|
font.scaling_factor / 1000.0
|
|
515
488
|
# The constant factor was found empirically by checking what Adobe Reader etc. do
|
|
516
|
-
(
|
|
489
|
+
(height - 2 * border_style.width) / unit_font_size * 0.83
|
|
517
490
|
end
|
|
518
491
|
else
|
|
519
492
|
font_size
|
|
520
493
|
end
|
|
521
494
|
end
|
|
522
495
|
|
|
496
|
+
# Handles Javascript formatting routines for single-line text fields.
|
|
497
|
+
#
|
|
498
|
+
# Returns [value, nil_or_text_color] where value is the new, potentially adjusted field
|
|
499
|
+
# value and the second argument is either +nil+ or the color that should be used for the
|
|
500
|
+
# text value.
|
|
501
|
+
def apply_javascript_formatting(value)
|
|
502
|
+
format_action = @widget[:AA]&.[](:F)
|
|
503
|
+
return [value, nil] unless format_action && format_action[:S] == :JavaScript
|
|
504
|
+
if (match = AF_NUMBER_FORMAT_RE.match(format_action[:JS]))
|
|
505
|
+
apply_af_number_format(value, match)
|
|
506
|
+
else
|
|
507
|
+
[value, nil]
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Regular expression for matching the AFNumber_Format Javascript method.
|
|
512
|
+
AF_NUMBER_FORMAT_RE = /
|
|
513
|
+
\AAFNumber_Format\(
|
|
514
|
+
\s*(?<ndec>\d+)\s*,
|
|
515
|
+
\s*(?<sep_style>[0-3])\s*,
|
|
516
|
+
\s*(?<neg_style>[0-3])\s*,
|
|
517
|
+
\s*0\s*,
|
|
518
|
+
\s*(?<currency_string>".*?")\s*,
|
|
519
|
+
\s*(?<prepend>false|true)\s*
|
|
520
|
+
\);\z
|
|
521
|
+
/x
|
|
522
|
+
|
|
523
|
+
# Implements the Javascript AFNumber_Format method.
|
|
524
|
+
#
|
|
525
|
+
# See:
|
|
526
|
+
# - https://experienceleague.adobe.com/docs/experience-manager-learn/assets/FormsAPIReference.pdf
|
|
527
|
+
# - https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/JS_API_AcroJS.html#printf
|
|
528
|
+
def apply_af_number_format(value, match)
|
|
529
|
+
value = value.to_f
|
|
530
|
+
format = "%.#{match[:ndec]}f"
|
|
531
|
+
text_color = 'black'
|
|
532
|
+
|
|
533
|
+
currency_string = JSON.parse(match[:currency_string])
|
|
534
|
+
format = (match[:prepend] == 'true' ? currency_string + format : format + currency_string)
|
|
535
|
+
|
|
536
|
+
if value < 0
|
|
537
|
+
value = value.abs
|
|
538
|
+
case match[:neg_style]
|
|
539
|
+
when '0' # MinusBlack
|
|
540
|
+
format = "-#{format}"
|
|
541
|
+
when '1' # Red
|
|
542
|
+
text_color = 'red'
|
|
543
|
+
when '2' # ParensBlack
|
|
544
|
+
format = "(#{format})"
|
|
545
|
+
when '3' # ParensRed
|
|
546
|
+
format = "(#{format})"
|
|
547
|
+
text_color = 'red'
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
result = sprintf(format, value)
|
|
552
|
+
|
|
553
|
+
# sep_style: 0=12,345.67, 1=12345.67, 2=12.345,67, 3=12345,67
|
|
554
|
+
before_decimal_point, after_decimal_point = result.split('.')
|
|
555
|
+
if match[:sep_style] == '0' || match[:sep_style] == '2'
|
|
556
|
+
separator = (match[:sep_style] == '0' ? ',' : '.')
|
|
557
|
+
before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
|
|
558
|
+
end
|
|
559
|
+
result = if after_decimal_point
|
|
560
|
+
decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
|
|
561
|
+
"#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
|
|
562
|
+
else
|
|
563
|
+
before_decimal_point
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
[result, text_color]
|
|
567
|
+
end
|
|
568
|
+
|
|
523
569
|
end
|
|
524
570
|
|
|
525
571
|
end
|