hexapdf 0.27.0 → 0.29.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 +100 -11
- data/examples/019-acro_form.rb +14 -3
- data/examples/023-images.rb +30 -0
- data/examples/024-digital-signatures.rb +23 -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 +13 -14
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document.rb +27 -24
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- 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/type.rb +0 -1
- 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/{type/signature → digital_signature}/common.rb +34 -4
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- 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_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +3 -9
- 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/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 +69 -16
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
data/lib/hexapdf/object.rb
CHANGED
@@ -159,6 +159,11 @@ module HexaPDF
|
|
159
159
|
object
|
160
160
|
end
|
161
161
|
|
162
|
+
# Returns +nil+ to end the recursion for field searching in Dictionary.field.
|
163
|
+
def self.field(_name)
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
162
167
|
# The wrapped HexaPDF::PDFData value.
|
163
168
|
#
|
164
169
|
# This attribute is not part of the public API!
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -70,6 +70,19 @@ module HexaPDF
|
|
70
70
|
!@reconstructed_revision.nil?
|
71
71
|
end
|
72
72
|
|
73
|
+
# Returns +true+ if the PDF file is a linearized file.
|
74
|
+
def linearized?
|
75
|
+
@linearized ||=
|
76
|
+
begin
|
77
|
+
@tokenizer.pos = @header_offset
|
78
|
+
3.times { @tokenizer.next_token } # parse: oid gen obj
|
79
|
+
obj = @tokenizer.next_object
|
80
|
+
obj.kind_of?(Hash) && obj.key?(:Linearized)
|
81
|
+
rescue MalformedPDFError
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
73
86
|
# Loads the indirect (potentially compressed) object specified by the given cross-reference
|
74
87
|
# entry.
|
75
88
|
#
|
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,6 +93,10 @@ 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)
|
98
102
|
offset = trailer[:Prev] # Get possible next offset before overwriting trailer
|
data/lib/hexapdf/tokenizer.rb
CHANGED
@@ -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
|