hexapdf 0.27.0 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -1
- data/examples/019-acro_form.rb +14 -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 +2 -2
- data/lib/hexapdf/configuration.rb +1 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +32 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +13 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +4 -0
- data/lib/hexapdf/tokenizer.rb +14 -8
- 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 +33 -7
- 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/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/page.rb +56 -46
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +2 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- 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 +13 -6
- data/test/hexapdf/test_parser.rb +17 -0
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +43 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +3 -4
- data/test/hexapdf/test_writer.rb +3 -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 +18 -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 +3 -1
- 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 +3 -0
- data/test/hexapdf/type/test_page.rb +67 -30
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- metadata +46 -3
@@ -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
|
@@ -84,6 +84,8 @@ module HexaPDF
|
|
84
84
|
# See: PDF1.7 s12.7.4.2
|
85
85
|
class ButtonField < Field
|
86
86
|
|
87
|
+
define_type :XXAcroFormField
|
88
|
+
|
87
89
|
define_field :Opt, type: PDFArray, version: '1.4'
|
88
90
|
|
89
91
|
# All inheritable dictionary fields for button fields.
|
@@ -206,7 +208,7 @@ module HexaPDF
|
|
206
208
|
# Note that this will only return useful values if there is at least one correctly set-up
|
207
209
|
# widget.
|
208
210
|
def allowed_values
|
209
|
-
(each_widget.
|
211
|
+
(each_widget.with_object([]) do |widget, result|
|
210
212
|
keys = widget.appearance_dict&.normal_appearance&.value&.keys
|
211
213
|
result.concat(keys) if keys
|
212
214
|
end - [:Off]).uniq
|
@@ -254,14 +256,14 @@ module HexaPDF
|
|
254
256
|
normal_appearance = widget.appearance_dict&.normal_appearance
|
255
257
|
next if !force && normal_appearance &&
|
256
258
|
((!push_button? && normal_appearance.value.length == 2 &&
|
257
|
-
normal_appearance.
|
259
|
+
normal_appearance.each.all? {|_, v| v.kind_of?(HexaPDF::Stream) }) ||
|
258
260
|
(push_button? && normal_appearance.kind_of?(HexaPDF::Stream)))
|
259
261
|
if check_box?
|
260
262
|
appearance_generator_class.new(widget).create_check_box_appearances
|
261
263
|
elsif radio_button?
|
262
264
|
appearance_generator_class.new(widget).create_radio_button_appearances
|
263
265
|
else
|
264
|
-
|
266
|
+
appearance_generator_class.new(widget).create_push_button_appearances
|
265
267
|
end
|
266
268
|
end
|
267
269
|
end
|
@@ -242,8 +242,8 @@ module HexaPDF
|
|
242
242
|
end
|
243
243
|
|
244
244
|
# :call-seq:
|
245
|
-
# field.each_widget {|widget| block} -> field
|
246
|
-
# field.each_widget -> Enumerator
|
245
|
+
# field.each_widget(direct_only: true) {|widget| block} -> field
|
246
|
+
# field.each_widget(direct_only: true) -> Enumerator
|
247
247
|
#
|
248
248
|
# Yields each widget, i.e. visual representation, of this field.
|
249
249
|
#
|
@@ -253,11 +253,17 @@ module HexaPDF
|
|
253
253
|
# 2. One or more widgets are defined as children of this field.
|
254
254
|
# 3. Widgets of *another field instance with the same full field name*.
|
255
255
|
#
|
256
|
-
#
|
257
|
-
#
|
256
|
+
# With the default of +direct_only+ being +true+, only the usual cases 1 and 2 are handled/
|
257
|
+
# If case 3 also needs to be handled, set +direct_only+ to +false+ or run the validation on
|
258
|
+
# the main AcroForm object (HexaPDF::Document#acro_form) before using this method (this will
|
259
|
+
# reduce case 3 to case 2).
|
260
|
+
#
|
261
|
+
# *Note*: Setting +direct_only+ to +false+ will have a severe performance impact since all
|
262
|
+
# fields of the form have to be searched to check whether there is another field with the
|
263
|
+
# same full field name.
|
258
264
|
#
|
259
265
|
# See: HexaPDF::Type::Annotations::Widget
|
260
|
-
def each_widget(direct_only:
|
266
|
+
def each_widget(direct_only: true, &block) # :yields: widget
|
261
267
|
return to_enum(__method__, direct_only: direct_only) unless block_given?
|
262
268
|
|
263
269
|
if embedded_widget?
|
@@ -125,14 +125,22 @@ module HexaPDF
|
|
125
125
|
def each_field(terminal_only: true)
|
126
126
|
return to_enum(__method__, terminal_only: terminal_only) unless block_given?
|
127
127
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
128
|
+
process_field_array = lambda do |array|
|
129
|
+
array.each_with_index do |field, index|
|
130
|
+
unless field.respond_to?(:type) && field.type == :XXAcroFormField
|
131
|
+
array[index] = field = document.wrap(field, type: :XXAcroFormField,
|
132
|
+
subtype: Field.inherited_value(field, :FT))
|
133
|
+
end
|
134
|
+
if field.terminal_field?
|
135
|
+
yield(field)
|
136
|
+
else
|
137
|
+
yield(field) unless terminal_only
|
138
|
+
process_field_array.call(field[:Kids])
|
139
|
+
end
|
140
|
+
end
|
133
141
|
end
|
134
142
|
|
135
|
-
|
143
|
+
process_field_array.call(root_fields)
|
136
144
|
self
|
137
145
|
end
|
138
146
|
|
@@ -393,7 +401,7 @@ module HexaPDF
|
|
393
401
|
fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) }
|
394
402
|
end
|
395
403
|
|
396
|
-
not_flattened = fields.map {|field| field.each_widget.to_a }.flatten
|
404
|
+
not_flattened = fields.map {|field| field.each_widget(direct_only: true).to_a }.flatten
|
397
405
|
document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) }
|
398
406
|
not_flattened.map!(&:form_field)
|
399
407
|
fields -= not_flattened
|
@@ -449,6 +457,8 @@ module HexaPDF
|
|
449
457
|
def perform_validation # :nodoc:
|
450
458
|
super
|
451
459
|
|
460
|
+
seen = {} # used for combining field
|
461
|
+
|
452
462
|
validate_array = lambda do |parent, container|
|
453
463
|
container.reject! do |field|
|
454
464
|
if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) || field.null?
|
@@ -469,6 +479,22 @@ module HexaPDF
|
|
469
479
|
field[:Parent] = parent
|
470
480
|
end
|
471
481
|
end
|
482
|
+
|
483
|
+
# Combine fields with same name
|
484
|
+
name = field.full_field_name
|
485
|
+
if (other_field = seen[name])
|
486
|
+
kids = other_field[:Kids] ||= []
|
487
|
+
kids << other_field.send(:extract_widget) if other_field.embedded_widget?
|
488
|
+
widgets = field.embedded_widget? ? [field.send(:extract_widget)] : field.each_widget.to_a
|
489
|
+
widgets.each do |widget|
|
490
|
+
widget[:Parent] = other_field
|
491
|
+
kids << widget
|
492
|
+
end
|
493
|
+
reject = true
|
494
|
+
elsif !reject
|
495
|
+
seen[name] = field
|
496
|
+
end
|
497
|
+
|
472
498
|
validate_array.call(field, field[:Kids]) if field.key?(:Kids)
|
473
499
|
reject
|
474
500
|
end
|