hexapdf 0.27.0 → 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 +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
|