hexapdf 0.12.1 → 0.14.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +130 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/command.rb +4 -2
- data/lib/hexapdf/cli/image2pdf.rb +2 -1
- data/lib/hexapdf/cli/info.rb +51 -2
- data/lib/hexapdf/cli/inspect.rb +30 -8
- data/lib/hexapdf/cli/merge.rb +1 -1
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/configuration.rb +15 -0
- data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
- data/lib/hexapdf/content/parser.rb +1 -1
- data/lib/hexapdf/dictionary.rb +9 -6
- data/lib/hexapdf/dictionary_fields.rb +1 -9
- data/lib/hexapdf/document.rb +41 -16
- data/lib/hexapdf/document/files.rb +0 -1
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
- data/lib/hexapdf/font/cmap.rb +1 -4
- data/lib/hexapdf/font/true_type/subsetter.rb +12 -3
- data/lib/hexapdf/font/true_type/table/head.rb +1 -0
- data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
- data/lib/hexapdf/font/true_type/table/post.rb +15 -10
- data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
- data/lib/hexapdf/font_loader/from_file.rb +18 -8
- data/lib/hexapdf/image_loader/png.rb +3 -2
- data/lib/hexapdf/importer.rb +3 -2
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +23 -23
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +3 -2
- data/lib/hexapdf/object.rb +52 -25
- data/lib/hexapdf/parser.rb +96 -4
- data/lib/hexapdf/pdf_array.rb +12 -5
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +34 -8
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +4 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
- data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
- data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
- data/lib/hexapdf/type/acro_form/field.rb +35 -5
- data/lib/hexapdf/type/acro_form/form.rb +139 -14
- data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
- data/lib/hexapdf/type/actions/uri.rb +3 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -4
- data/lib/hexapdf/type/catalog.rb +2 -2
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +4 -2
- data/lib/hexapdf/type/font_true_type.rb +6 -2
- data/lib/hexapdf/type/font_type0.rb +4 -4
- data/lib/hexapdf/type/form.rb +15 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +37 -13
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +1 -0
- data/lib/hexapdf/type/trailer.rb +2 -3
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +6 -1
- data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_color_space.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +4 -4
- data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
- data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
- data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
- data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
- data/test/hexapdf/font/true_type/test_subsetter.rb +5 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
- data/test/hexapdf/font_loader/test_from_file.rb +7 -0
- data/test/hexapdf/layout/test_style.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +12 -5
- data/test/hexapdf/test_configuration.rb +2 -2
- data/test/hexapdf/test_dictionary.rb +8 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +171 -53
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
- data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
- data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
- data/test/hexapdf/type/acro_form/test_field.rb +39 -0
- data/test/hexapdf/type/acro_form/test_form.rb +87 -15
- data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
- data/test/hexapdf/type/test_font_simple.rb +2 -1
- data/test/hexapdf/type/test_font_true_type.rb +6 -0
- data/test/hexapdf/type/test_form.rb +26 -1
- data/test/hexapdf/type/test_page.rb +45 -7
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/utils/test_bit_field.rb +2 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
- data/test/test_helper.rb +2 -0
- metadata +6 -11
data/lib/hexapdf/revisions.rb
CHANGED
@@ -67,30 +67,38 @@ module HexaPDF
|
|
67
67
|
object_loader = lambda {|xref_entry| parser.load_object(xref_entry) }
|
68
68
|
|
69
69
|
revisions = []
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
70
|
+
begin
|
71
|
+
xref_section, trailer = parser.load_revision(parser.startxref_offset)
|
72
|
+
revisions << Revision.new(document.wrap(trailer, type: :XXTrailer),
|
73
|
+
xref_section: xref_section, loader: object_loader)
|
74
|
+
seen_xref_offsets = {parser.startxref_offset => true}
|
75
|
+
|
76
|
+
while (prev = revisions[0].trailer.value[:Prev]) &&
|
77
|
+
!seen_xref_offsets.key?(prev)
|
78
|
+
# PDF1.7 s7.5.5 states that :Prev needs to be indirect, Adobe's reference 3.4.4 says it
|
79
|
+
# should be direct. Adobe's POV is followed here. Same with :XRefStm.
|
80
|
+
xref_section, trailer = parser.load_revision(prev)
|
81
|
+
seen_xref_offsets[prev] = true
|
82
|
+
|
83
|
+
stm = revisions[0].trailer.value[:XRefStm]
|
84
|
+
if stm && !seen_xref_offsets.key?(stm)
|
85
|
+
stm_xref_section, = parser.load_revision(stm)
|
86
|
+
xref_section.merge!(stm_xref_section)
|
87
|
+
seen_xref_offsets[stm] = true
|
88
|
+
end
|
89
|
+
|
90
|
+
revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
|
91
|
+
xref_section: xref_section, loader: object_loader))
|
87
92
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
93
|
+
rescue HexaPDF::MalformedPDFError
|
94
|
+
reconstructed_revision = parser.reconstructed_revision
|
95
|
+
unless revisions.empty?
|
96
|
+
reconstructed_revision.trailer.data.value = revisions.last.trailer.data.value
|
97
|
+
end
|
98
|
+
revisions << reconstructed_revision
|
91
99
|
end
|
92
100
|
|
93
|
-
document.version = parser.file_header_version
|
101
|
+
document.version = parser.file_header_version rescue '1.0'
|
94
102
|
new(document, initial_revisions: revisions, parser: parser)
|
95
103
|
end
|
96
104
|
|
data/lib/hexapdf/serializer.rb
CHANGED
@@ -88,13 +88,39 @@ module HexaPDF
|
|
88
88
|
|
89
89
|
# Creates a new Serializer object.
|
90
90
|
def initialize
|
91
|
-
@dispatcher =
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
91
|
+
@dispatcher = {
|
92
|
+
Hash => 'serialize_hash',
|
93
|
+
Array => 'serialize_array',
|
94
|
+
Symbol => 'serialize_symbol',
|
95
|
+
String => 'serialize_string',
|
96
|
+
Integer => 'serialize_integer',
|
97
|
+
Float => 'serialize_float',
|
98
|
+
Time => 'serialize_time',
|
99
|
+
TrueClass => 'serialize_trueclass',
|
100
|
+
FalseClass => 'serialize_falseclass',
|
101
|
+
NilClass => 'serialize_nilclass',
|
102
|
+
HexaPDF::Reference => 'serialize_hexapdf_reference',
|
103
|
+
HexaPDF::Object => 'serialize_hexapdf_object',
|
104
|
+
HexaPDF::Stream => 'serialize_hexapdf_stream',
|
105
|
+
HexaPDF::Dictionary => 'serialize_hexapdf_object',
|
106
|
+
HexaPDF::PDFArray => 'serialize_hexapdf_object',
|
107
|
+
HexaPDF::Rectangle => 'serialize_hexapdf_object',
|
108
|
+
}
|
109
|
+
@dispatcher.default_proc = lambda do |h, klass|
|
110
|
+
h[klass] = if klass <= HexaPDF::Stream
|
111
|
+
"serialize_hexapdf_stream"
|
112
|
+
elsif klass <= HexaPDF::Object
|
113
|
+
"serialize_hexapdf_object"
|
114
|
+
else
|
115
|
+
method = nil
|
116
|
+
klass.ancestors.each do |ancestor_klass|
|
117
|
+
name = ancestor_klass.name.to_s.downcase
|
118
|
+
name.gsub!(/::/, '_')
|
119
|
+
method = "serialize_#{name}"
|
120
|
+
break if respond_to?(method, true)
|
121
|
+
end
|
122
|
+
method
|
123
|
+
end
|
98
124
|
end
|
99
125
|
@encrypter = false
|
100
126
|
@io = nil
|
@@ -243,7 +269,7 @@ module HexaPDF
|
|
243
269
|
else
|
244
270
|
obj.dup
|
245
271
|
end
|
246
|
-
obj.gsub!(/[
|
272
|
+
obj.gsub!(/[()\\\r]/n, STRING_ESCAPE_MAP)
|
247
273
|
"(#{obj})"
|
248
274
|
end
|
249
275
|
|
@@ -129,9 +129,10 @@ module HexaPDF
|
|
129
129
|
xref_stream = false
|
130
130
|
objects_to_delete = []
|
131
131
|
rev.each do |obj|
|
132
|
-
|
132
|
+
case obj.type
|
133
|
+
when :ObjStm
|
133
134
|
objects_to_delete << obj
|
134
|
-
|
135
|
+
when :XRef
|
135
136
|
xref_stream = true
|
136
137
|
objects_to_delete << obj if xref_streams == :delete
|
137
138
|
else
|
@@ -150,9 +151,10 @@ module HexaPDF
|
|
150
151
|
objstms = [doc.wrap({Type: :ObjStm})]
|
151
152
|
old_objstms = []
|
152
153
|
rev.each do |obj|
|
153
|
-
|
154
|
+
case obj.type
|
155
|
+
when :XRef
|
154
156
|
xref_stream = true
|
155
|
-
|
157
|
+
when :ObjStm
|
156
158
|
old_objstms << obj
|
157
159
|
end
|
158
160
|
delete_fields_with_defaults(obj)
|
data/lib/hexapdf/tokenizer.rb
CHANGED
@@ -249,17 +249,18 @@ module HexaPDF
|
|
249
249
|
#
|
250
250
|
# See: PDF1.7 s7.3.3
|
251
251
|
def parse_number
|
252
|
-
|
252
|
+
val = scan_until(WHITESPACE_OR_DELIMITER_RE) || @ss.scan(/.*/)
|
253
|
+
if val.match?(/\A[+-]?\d++(?!\.)\z/)
|
253
254
|
tmp = val.to_i
|
254
255
|
# Handle object references, see PDF1.7 s7.3.10
|
255
256
|
prepare_string_scanner(10)
|
256
257
|
tmp = Reference.new(tmp, @ss[1].to_i) if @ss.scan(REFERENCE_RE)
|
257
258
|
tmp
|
258
|
-
elsif
|
259
|
+
elsif val.match?(/\A[+-]?(?:\d+\.\d*|\.\d+)\z/)
|
259
260
|
val << '0' if val.getbyte(-1) == 46 # dot '.'
|
260
261
|
Float(val)
|
261
262
|
else
|
262
|
-
|
263
|
+
TOKEN_CACHE[val] # val is keyword
|
263
264
|
end
|
264
265
|
end
|
265
266
|
|
@@ -37,6 +37,7 @@
|
|
37
37
|
require 'hexapdf/error'
|
38
38
|
require 'hexapdf/layout/style'
|
39
39
|
require 'hexapdf/layout/text_fragment'
|
40
|
+
require 'hexapdf/layout/text_layouter'
|
40
41
|
|
41
42
|
module HexaPDF
|
42
43
|
module Type
|
@@ -80,14 +81,8 @@ module HexaPDF
|
|
80
81
|
else
|
81
82
|
raise HexaPDF::Error, "Unsupported button field type"
|
82
83
|
end
|
83
|
-
when :Tx
|
84
|
+
when :Tx, :Ch
|
84
85
|
create_text_appearances
|
85
|
-
when :Ch
|
86
|
-
if @field.combo_box?
|
87
|
-
create_text_appearances
|
88
|
-
else
|
89
|
-
raise HexaPDF::Error, "List box not supported yet"
|
90
|
-
end
|
91
86
|
else
|
92
87
|
raise HexaPDF::Error, "Unsupported field type #{@field.field_type}"
|
93
88
|
end
|
@@ -206,6 +201,10 @@ module HexaPDF
|
|
206
201
|
# * The font, font size and font color are taken from the associated field's default
|
207
202
|
# appearance string. See VariableTextField.
|
208
203
|
#
|
204
|
+
# If the font is not usable by HexaPDF (which may be due to a variety of reasons, e.g. no
|
205
|
+
# associated information in the form's default resources), the font specified by the
|
206
|
+
# configuration option +acro_form.fallback_font+ will be used.
|
207
|
+
#
|
209
208
|
# * The widget's rectangle /Rect must be defined. If the height is zero, it is auto-sized
|
210
209
|
# based on the font size. If additionally the font size is zero, a font size of
|
211
210
|
# +acro_form.default_font_size+ is used. If the width is zero, the
|
@@ -222,7 +221,7 @@ module HexaPDF
|
|
222
221
|
def create_text_appearances
|
223
222
|
font_name, font_size = @field.parse_default_appearance_string
|
224
223
|
default_resources = @document.acro_form.default_resources
|
225
|
-
font = default_resources.font(font_name).font_wrapper
|
224
|
+
font = default_resources.font(font_name).font_wrapper rescue nil
|
226
225
|
unless font
|
227
226
|
fallback_font_name, fallback_font_options = @document.config['acro_form.fallback_font']
|
228
227
|
if fallback_font_name
|
@@ -245,38 +244,35 @@ module HexaPDF
|
|
245
244
|
rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
|
246
245
|
end
|
247
246
|
|
248
|
-
form = (@widget[:AP] ||= {})[:N]
|
249
|
-
|
247
|
+
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
248
|
+
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
249
|
+
form.contents = ''
|
250
250
|
form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
|
251
251
|
|
252
252
|
canvas = form.canvas
|
253
253
|
apply_background_and_border(border_style, canvas)
|
254
254
|
style.font_size = calculate_font_size(font, font_size, rect, border_style)
|
255
|
+
style.clear_cache
|
255
256
|
|
256
257
|
canvas.marked_content_sequence(:Tx) do
|
257
|
-
if
|
258
|
+
if @field.field_value || @field.concrete_field_type == :list_box
|
258
259
|
canvas.save_graphics_state do
|
259
260
|
canvas.rectangle(padding, padding, rect.width - 2 * padding,
|
260
261
|
rect.height - 2 * padding).clip_path.end_path
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
end
|
269
|
-
cap_height = font.wrapped_font.cap_height * font.scaling_factor / 1000.0 *
|
270
|
-
style.font_size
|
271
|
-
y = padding + (rect.height - 2 * padding - cap_height) / 2.0
|
272
|
-
y = padding - style.scaled_font_descender if y < 0
|
273
|
-
fragment.draw(canvas, x, y)
|
262
|
+
if @field.concrete_field_type == :multiline_text_field
|
263
|
+
draw_multiline_text(canvas, rect, style, padding)
|
264
|
+
elsif @field.concrete_field_type == :list_box
|
265
|
+
draw_list_box(canvas, rect, style, padding)
|
266
|
+
else
|
267
|
+
draw_single_line_text(canvas, rect, style, padding)
|
268
|
+
end
|
274
269
|
end
|
275
270
|
end
|
276
271
|
end
|
277
272
|
end
|
278
273
|
|
279
274
|
alias create_combo_box_appearances create_text_appearances
|
275
|
+
alias create_list_box_appearances create_text_appearances
|
280
276
|
|
281
277
|
private
|
282
278
|
|
@@ -337,6 +333,13 @@ module HexaPDF
|
|
337
333
|
canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
|
338
334
|
else
|
339
335
|
canvas.rectangle(offset, offset, width, height)
|
336
|
+
if @field.concrete_field_type == :comb_text_field
|
337
|
+
cell_width = rect.width.to_f / @field[:MaxLen]
|
338
|
+
1.upto(@field[:MaxLen] - 1) do |i|
|
339
|
+
canvas.line(i * cell_width, border_style.width,
|
340
|
+
i * cell_width, border_style.width + height)
|
341
|
+
end
|
342
|
+
end
|
340
343
|
end
|
341
344
|
end
|
342
345
|
canvas.stroke
|
@@ -381,14 +384,115 @@ module HexaPDF
|
|
381
384
|
end
|
382
385
|
end
|
383
386
|
|
387
|
+
# Draws a single line of text inside the widget's rectangle.
|
388
|
+
def draw_single_line_text(canvas, rect, style, padding)
|
389
|
+
value = @field.field_value
|
390
|
+
fragment = HexaPDF::Layout::TextFragment.create(value, style)
|
391
|
+
|
392
|
+
if @field.concrete_field_type == :comb_text_field
|
393
|
+
unless @field.key?(:MaxLen)
|
394
|
+
raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
|
395
|
+
end
|
396
|
+
new_items = []
|
397
|
+
cell_width = rect.width.to_f / @field[:MaxLen]
|
398
|
+
scaled_cell_width = cell_width / style.scaled_font_size.to_f
|
399
|
+
fragment.items.each_cons(2) do |a, b|
|
400
|
+
new_items << a << -(scaled_cell_width - a.width / 2.0 - b.width / 2.0)
|
401
|
+
end
|
402
|
+
new_items << fragment.items.last
|
403
|
+
fragment.items.replace(new_items)
|
404
|
+
fragment.clear_cache
|
405
|
+
# Adobe always seems to add 1 to the first offset...
|
406
|
+
x_offset = 1 + (cell_width - style.scaled_item_width(fragment.items[0])) / 2.0
|
407
|
+
x = case @field.text_alignment
|
408
|
+
when :left then x_offset
|
409
|
+
when :right then x_offset + cell_width * (@field[:MaxLen] - value.length)
|
410
|
+
when :center then x_offset + cell_width * ((@field[:MaxLen] - value.length) / 2)
|
411
|
+
end
|
412
|
+
else
|
413
|
+
# Adobe seems to be left/right-aligning based on twice the border width
|
414
|
+
x = case @field.text_alignment
|
415
|
+
when :left then 2 * padding
|
416
|
+
when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
|
417
|
+
when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Adobe seems to be vertically centering based on the cap height, if enough space is
|
422
|
+
# available
|
423
|
+
cap_height = style.font.wrapped_font.cap_height * style.font.scaling_factor / 1000.0 *
|
424
|
+
style.font_size
|
425
|
+
y = padding + (rect.height - 2 * padding - cap_height) / 2.0
|
426
|
+
y = padding - style.scaled_font_descender if y < 0
|
427
|
+
fragment.draw(canvas, x, y)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Draws multiple lines of text inside the widget's rectangle.
|
431
|
+
def draw_multiline_text(canvas, rect, style, padding)
|
432
|
+
items = [Layout::TextFragment.create(@field.field_value, style)]
|
433
|
+
layouter = Layout::TextLayouter.new(style)
|
434
|
+
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
435
|
+
|
436
|
+
result = nil
|
437
|
+
if style.font_size == 0 # need to auto-size text
|
438
|
+
style.font_size = 12 # Adobe seems to use this as starting point
|
439
|
+
style.clear_cache
|
440
|
+
loop do
|
441
|
+
result = layouter.fit(items, rect.width - 4 * padding, rect.height - 4 * padding)
|
442
|
+
break if result.status == :success || style.font_size <= 4 # don't make text too small
|
443
|
+
style.font_size -= 1
|
444
|
+
style.clear_cache
|
445
|
+
end
|
446
|
+
else
|
447
|
+
result = layouter.fit(items, rect.width - 4 * padding, 2**20)
|
448
|
+
end
|
449
|
+
|
450
|
+
unless result.lines.empty?
|
451
|
+
result.draw(canvas, 2 * padding, rect.height - 2 * padding - result.lines[0].height / 2.0)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# Draws the visible option items of the list box in the widget's rectangle.
|
456
|
+
def draw_list_box(canvas, rect, style, padding)
|
457
|
+
option_items = @field.option_items
|
458
|
+
top_index = @field.list_box_top_index
|
459
|
+
items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
|
460
|
+
|
461
|
+
indices = @field[:I] || []
|
462
|
+
value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
463
|
+
indices = value_indices if indices != value_indices
|
464
|
+
|
465
|
+
layouter = Layout::TextLayouter.new(style)
|
466
|
+
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
467
|
+
result = layouter.fit(items, rect.width - 4 * padding, rect.height)
|
468
|
+
|
469
|
+
unless result.lines.empty?
|
470
|
+
top_gap = style.line_spacing.gap(result.lines[0], result.lines[0])
|
471
|
+
line_height = style.line_spacing.baseline_distance(result.lines[0], result.lines[0])
|
472
|
+
canvas.fill_color(153, 193, 218) # Adobe's color for selection highlighting
|
473
|
+
indices.map! {|i| rect.height - padding - (i - top_index + 1) * line_height }.each do |y|
|
474
|
+
next if y + line_height > rect.height || y + line_height < padding
|
475
|
+
canvas.rectangle(padding, y, rect.width - 2 * padding, line_height)
|
476
|
+
end
|
477
|
+
canvas.fill if canvas.graphics_object == :path
|
478
|
+
result.draw(canvas, 2 * padding, rect.height - padding - top_gap)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
384
482
|
# Calculates the font size for text fields based on the font and font size of the default
|
385
483
|
# appearance string, the annotation rectangle and the border style.
|
386
484
|
def calculate_font_size(font, font_size, rect, border_style)
|
387
485
|
if font_size == 0
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
486
|
+
if @field.concrete_field_type == :multiline_text_field
|
487
|
+
0 # Handled by multiline drawing code
|
488
|
+
elsif @field.concrete_field_type == :list_box
|
489
|
+
12 # Seems to be Adobe's default
|
490
|
+
else
|
491
|
+
unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
|
492
|
+
font.scaling_factor / 1000.0
|
493
|
+
# The constant factor was found empirically by checking what Adobe Reader etc. do
|
494
|
+
(rect.height - 2 * border_style.width) / unit_font_size * 0.83
|
495
|
+
end
|
392
496
|
else
|
393
497
|
font_size
|
394
498
|
end
|
@@ -134,8 +134,8 @@ module HexaPDF
|
|
134
134
|
# Check boxes:: For check boxes that are in the on state the value +true+ is returned.
|
135
135
|
# Otherwise +false+ is returned.
|
136
136
|
#
|
137
|
-
# Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the
|
138
|
-
# the specific radio button that is selected is returned.
|
137
|
+
# Radio buttons:: If no radio button is selected, +nil+ is returned. Otherwise the value (a
|
138
|
+
# Symbol) of the specific radio button that is selected is returned.
|
139
139
|
def field_value
|
140
140
|
normalized_field_value(:V)
|
141
141
|
end
|
@@ -149,7 +149,8 @@ module HexaPDF
|
|
149
149
|
# +false+ for unchecking it.
|
150
150
|
#
|
151
151
|
# Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
|
152
|
-
# the
|
152
|
+
# the value (a Symbol or an object responding to +#to_sym+) of a radio
|
153
|
+
# button that should be turned on.
|
153
154
|
def field_value=(value)
|
154
155
|
normalized_field_value_set(:V, value)
|
155
156
|
end
|
@@ -179,7 +180,7 @@ module HexaPDF
|
|
179
180
|
end
|
180
181
|
end
|
181
182
|
|
182
|
-
# Returns the name used for setting the check box to the on state.
|
183
|
+
# Returns the name (a Symbol) used for setting the check box to the on state.
|
183
184
|
#
|
184
185
|
# Defaults to :Yes if no other name could be determined.
|
185
186
|
def check_box_on_name
|
@@ -187,7 +188,8 @@ module HexaPDF
|
|
187
188
|
find {|key| key != :Off } || :Yes
|
188
189
|
end
|
189
190
|
|
190
|
-
# Returns the array of values that can be used for the field value of the radio
|
191
|
+
# Returns the array of Symbol values that can be used for the field value of the radio
|
192
|
+
# button.
|
191
193
|
def radio_button_values
|
192
194
|
each_widget.map do |widget|
|
193
195
|
widget.appearance&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
|
@@ -200,8 +202,8 @@ module HexaPDF
|
|
200
202
|
# default appearance.
|
201
203
|
#
|
202
204
|
# If the widget is created for a radio button field, the +value+ argument needs to set to
|
203
|
-
# the value (a
|
204
|
-
# specific widget of the radio button set to on.
|
205
|
+
# the value (a Symbol or an object responding to +#to_sym+) this widget represents. It can
|
206
|
+
# be used with #field_value= to set this specific widget of the radio button set to on.
|
205
207
|
#
|
206
208
|
# See: Field#create_widget, AppearanceGenerator button field methods
|
207
209
|
def create_widget(page, defaults: true, value: nil, **values)
|
@@ -209,8 +211,11 @@ module HexaPDF
|
|
209
211
|
if check_box?
|
210
212
|
widget[:AP] = {N: {Yes: nil, Off: nil}}
|
211
213
|
elsif radio_button?
|
212
|
-
|
213
|
-
|
214
|
+
unless value.respond_to?(:to_sym)
|
215
|
+
raise ArgumentError, "Argument 'value' has to be provided for radio buttons " \
|
216
|
+
"and needs to respond to #to_sym"
|
217
|
+
end
|
218
|
+
widget[:AP] = {N: {value.to_sym => nil, Off: nil}}
|
214
219
|
end
|
215
220
|
next unless defaults
|
216
221
|
widget.border_style(color: 0, width: 1, style: (push_button? ? :beveled : :solid))
|
@@ -223,10 +228,12 @@ module HexaPDF
|
|
223
228
|
#
|
224
229
|
# The created appearance streams depend on the actual type of the button field. See
|
225
230
|
# AppearanceGenerator for the details.
|
226
|
-
|
231
|
+
#
|
232
|
+
# By setting +force+ to +true+ the creation of the appearances can be forced.
|
233
|
+
def create_appearances(force: false)
|
227
234
|
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
|
228
235
|
each_widget do |widget|
|
229
|
-
next if widget.appearance?
|
236
|
+
next if !force && widget.appearance?
|
230
237
|
if check_box?
|
231
238
|
appearance_generator_class.new(widget).create_check_box_appearances
|
232
239
|
elsif radio_button?
|
@@ -240,6 +247,7 @@ module HexaPDF
|
|
240
247
|
# Updates the widgets so that they reflect the current field value.
|
241
248
|
def update_widgets
|
242
249
|
return if push_button?
|
250
|
+
create_appearances
|
243
251
|
value = self[:V]
|
244
252
|
each_widget do |widget|
|
245
253
|
widget[:AS] = (widget.appearance&.normal_appearance&.value&.key?(value) ? value : :Off)
|
@@ -271,8 +279,8 @@ module HexaPDF
|
|
271
279
|
value == true ? check_box_on_name : :Off
|
272
280
|
elsif value.nil?
|
273
281
|
:Off
|
274
|
-
elsif radio_button_values.include?(value)
|
275
|
-
value
|
282
|
+
elsif radio_button_values.include?(value.to_sym)
|
283
|
+
value.to_sym
|
276
284
|
else
|
277
285
|
@document.config['acro_form.on_invalid_value'].call(self, value)
|
278
286
|
end
|