hexapdf 0.12.3 → 0.14.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +132 -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/dictionary.rb +12 -6
- data/lib/hexapdf/dictionary_fields.rb +2 -10
- 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 +16 -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 +107 -7
- data/lib/hexapdf/pdf_array.rb +15 -5
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +37 -10
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +22 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
- 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 +6 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +21 -12
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +5 -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 +2 -2
- 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 +10 -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_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 +9 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +205 -51
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_serializer.rb +7 -0
- data/test/hexapdf/test_tokenizer.rb +28 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
- data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
- 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 +8 -1
- data/test/hexapdf/type/test_page.rb +8 -1
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/type/test_resources.rb +6 -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 -12
data/lib/hexapdf/pdf_array.rb
CHANGED
@@ -65,8 +65,11 @@ module HexaPDF
|
|
65
65
|
# * Returns the native Ruby object for values with class HexaPDF::Object. However, all
|
66
66
|
# subclasses of HexaPDF::Object are returned as is (it makes no sense, for example, to return
|
67
67
|
# the hash that describes the Catalog instead of the Catalog object).
|
68
|
+
#
|
69
|
+
# Note: Hash or Array values will always be returned as-is, i.e. not wrapped with Dictionary or
|
70
|
+
# PDFArray.
|
68
71
|
def [](arg1, arg2 = nil)
|
69
|
-
data = value[arg1,
|
72
|
+
data = arg2 ? value[arg1, arg2] : value[arg1]
|
70
73
|
return if data.nil?
|
71
74
|
|
72
75
|
if arg2 || arg1.kind_of?(Range)
|
@@ -83,7 +86,7 @@ module HexaPDF
|
|
83
86
|
# subclasses) and the given data has not (including subclasses), the data is stored inside the
|
84
87
|
# HexaPDF::Object.
|
85
88
|
def []=(index, data)
|
86
|
-
if value[index].
|
89
|
+
if value[index].instance_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Object) &&
|
87
90
|
!data.kind_of?(HexaPDF::Reference)
|
88
91
|
value[index].value = data
|
89
92
|
else
|
@@ -113,6 +116,13 @@ module HexaPDF
|
|
113
116
|
value.delete_at(index)
|
114
117
|
end
|
115
118
|
|
119
|
+
# Deletes all values from the PDFArray that are equal to the given object.
|
120
|
+
#
|
121
|
+
# Returns the last deleted item, or +nil+ if no matching item is found.
|
122
|
+
def delete(object)
|
123
|
+
value.delete(object)
|
124
|
+
end
|
125
|
+
|
116
126
|
# :call-seq:
|
117
127
|
# array.slice!(index) -> obj or nil
|
118
128
|
# array.slice!(start, length) -> new_array or nil
|
@@ -174,9 +184,9 @@ module HexaPDF
|
|
174
184
|
self
|
175
185
|
end
|
176
186
|
|
177
|
-
# Returns
|
187
|
+
# Returns an array containing the preprocessed values (like in #[]).
|
178
188
|
def to_ary
|
179
|
-
|
189
|
+
each.to_a
|
180
190
|
end
|
181
191
|
|
182
192
|
private
|
@@ -196,7 +206,7 @@ module HexaPDF
|
|
196
206
|
data = document.deref(data)
|
197
207
|
value[index] = data if index
|
198
208
|
end
|
199
|
-
if data.
|
209
|
+
if data.instance_of?(HexaPDF::Object) || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
|
200
210
|
data = data.value
|
201
211
|
end
|
202
212
|
data
|
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
|
|
@@ -317,6 +343,7 @@ module HexaPDF
|
|
317
343
|
@io << data.freeze
|
318
344
|
end
|
319
345
|
@io << "\nendstream"
|
346
|
+
@in_object = false
|
320
347
|
|
321
348
|
nil
|
322
349
|
else
|
@@ -324,12 +351,12 @@ module HexaPDF
|
|
324
351
|
obj.value[:Length] = data.size
|
325
352
|
|
326
353
|
str = serialize_hash(obj.value)
|
354
|
+
@in_object = false
|
355
|
+
|
327
356
|
str << "stream\n"
|
328
357
|
str << data
|
329
358
|
str << "\nendstream"
|
330
359
|
end
|
331
|
-
ensure
|
332
|
-
@in_object = false
|
333
360
|
end
|
334
361
|
|
335
362
|
# Invokes the correct serialization method for the object.
|
@@ -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
@@ -188,6 +188,28 @@ module HexaPDF
|
|
188
188
|
token
|
189
189
|
end
|
190
190
|
|
191
|
+
# Returns a single integer or keyword token read from the current position and advances the scan
|
192
|
+
# pointer. If the current position doesn't contain such a token, +nil+ is returned without
|
193
|
+
# advancing the scan pointer. The value +NO_MORE_TOKENS+ is returned if there are no more tokens
|
194
|
+
# available.
|
195
|
+
#
|
196
|
+
# Initial runs of whitespace characters are ignored.
|
197
|
+
#
|
198
|
+
# Note: This is a special method meant for use with reconstructing the cross-reference table!
|
199
|
+
def next_integer_or_keyword
|
200
|
+
skip_whitespace
|
201
|
+
byte = @ss.string.getbyte(@ss.pos) || -1
|
202
|
+
if 48 <= byte && byte <= 57
|
203
|
+
parse_number
|
204
|
+
elsif (97 <= byte && byte <= 122) || (65 <= byte && byte <= 90)
|
205
|
+
parse_keyword
|
206
|
+
elsif byte == -1 # we reached the end of the file
|
207
|
+
NO_MORE_TOKENS
|
208
|
+
else
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
191
213
|
# Reads the byte (an integer) at the current position and advances the scan pointer.
|
192
214
|
def next_byte
|
193
215
|
prepare_string_scanner(1)
|
@@ -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
|
@@ -249,38 +244,38 @@ module HexaPDF
|
|
249
244
|
rect.height = style.scaled_y_max - style.scaled_y_min + 2 * padding
|
250
245
|
end
|
251
246
|
|
252
|
-
form = (@widget[:AP] ||= {})[:N]
|
253
|
-
|
247
|
+
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
248
|
+
# Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
|
249
|
+
# key; we can do this since we know this has to be a Form object
|
250
|
+
form = @document.wrap(form, type: :XObject, subtype: :Form) unless form[:Subtype] == :Form
|
251
|
+
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
252
|
+
form.contents = ''
|
254
253
|
form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
|
255
254
|
|
256
255
|
canvas = form.canvas
|
257
256
|
apply_background_and_border(border_style, canvas)
|
258
257
|
style.font_size = calculate_font_size(font, font_size, rect, border_style)
|
258
|
+
style.clear_cache
|
259
259
|
|
260
260
|
canvas.marked_content_sequence(:Tx) do
|
261
|
-
if
|
261
|
+
if @field.field_value || @field.concrete_field_type == :list_box
|
262
262
|
canvas.save_graphics_state do
|
263
263
|
canvas.rectangle(padding, padding, rect.width - 2 * padding,
|
264
264
|
rect.height - 2 * padding).clip_path.end_path
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
end
|
273
|
-
cap_height = font.wrapped_font.cap_height * font.scaling_factor / 1000.0 *
|
274
|
-
style.font_size
|
275
|
-
y = padding + (rect.height - 2 * padding - cap_height) / 2.0
|
276
|
-
y = padding - style.scaled_font_descender if y < 0
|
277
|
-
fragment.draw(canvas, x, y)
|
265
|
+
if @field.concrete_field_type == :multiline_text_field
|
266
|
+
draw_multiline_text(canvas, rect, style, padding)
|
267
|
+
elsif @field.concrete_field_type == :list_box
|
268
|
+
draw_list_box(canvas, rect, style, padding)
|
269
|
+
else
|
270
|
+
draw_single_line_text(canvas, rect, style, padding)
|
271
|
+
end
|
278
272
|
end
|
279
273
|
end
|
280
274
|
end
|
281
275
|
end
|
282
276
|
|
283
277
|
alias create_combo_box_appearances create_text_appearances
|
278
|
+
alias create_list_box_appearances create_text_appearances
|
284
279
|
|
285
280
|
private
|
286
281
|
|
@@ -341,6 +336,13 @@ module HexaPDF
|
|
341
336
|
canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
|
342
337
|
else
|
343
338
|
canvas.rectangle(offset, offset, width, height)
|
339
|
+
if @field.concrete_field_type == :comb_text_field
|
340
|
+
cell_width = rect.width.to_f / @field[:MaxLen]
|
341
|
+
1.upto(@field[:MaxLen] - 1) do |i|
|
342
|
+
canvas.line(i * cell_width, border_style.width,
|
343
|
+
i * cell_width, border_style.width + height)
|
344
|
+
end
|
345
|
+
end
|
344
346
|
end
|
345
347
|
end
|
346
348
|
canvas.stroke
|
@@ -385,14 +387,115 @@ module HexaPDF
|
|
385
387
|
end
|
386
388
|
end
|
387
389
|
|
390
|
+
# Draws a single line of text inside the widget's rectangle.
|
391
|
+
def draw_single_line_text(canvas, rect, style, padding)
|
392
|
+
value = @field.field_value
|
393
|
+
fragment = HexaPDF::Layout::TextFragment.create(value, style)
|
394
|
+
|
395
|
+
if @field.concrete_field_type == :comb_text_field
|
396
|
+
unless @field.key?(:MaxLen)
|
397
|
+
raise HexaPDF::Error, "Missing or invalid dictionary field /MaxLen for comb text field"
|
398
|
+
end
|
399
|
+
new_items = []
|
400
|
+
cell_width = rect.width.to_f / @field[:MaxLen]
|
401
|
+
scaled_cell_width = cell_width / style.scaled_font_size.to_f
|
402
|
+
fragment.items.each_cons(2) do |a, b|
|
403
|
+
new_items << a << -(scaled_cell_width - a.width / 2.0 - b.width / 2.0)
|
404
|
+
end
|
405
|
+
new_items << fragment.items.last
|
406
|
+
fragment.items.replace(new_items)
|
407
|
+
fragment.clear_cache
|
408
|
+
# Adobe always seems to add 1 to the first offset...
|
409
|
+
x_offset = 1 + (cell_width - style.scaled_item_width(fragment.items[0])) / 2.0
|
410
|
+
x = case @field.text_alignment
|
411
|
+
when :left then x_offset
|
412
|
+
when :right then x_offset + cell_width * (@field[:MaxLen] - value.length)
|
413
|
+
when :center then x_offset + cell_width * ((@field[:MaxLen] - value.length) / 2)
|
414
|
+
end
|
415
|
+
else
|
416
|
+
# Adobe seems to be left/right-aligning based on twice the border width
|
417
|
+
x = case @field.text_alignment
|
418
|
+
when :left then 2 * padding
|
419
|
+
when :right then [rect.width - 2 * padding - fragment.width, 2 * padding].max
|
420
|
+
when :center then [(rect.width - fragment.width) / 2.0, 2 * padding].max
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# Adobe seems to be vertically centering based on the cap height, if enough space is
|
425
|
+
# available
|
426
|
+
cap_height = style.font.wrapped_font.cap_height * style.font.scaling_factor / 1000.0 *
|
427
|
+
style.font_size
|
428
|
+
y = padding + (rect.height - 2 * padding - cap_height) / 2.0
|
429
|
+
y = padding - style.scaled_font_descender if y < 0
|
430
|
+
fragment.draw(canvas, x, y)
|
431
|
+
end
|
432
|
+
|
433
|
+
# Draws multiple lines of text inside the widget's rectangle.
|
434
|
+
def draw_multiline_text(canvas, rect, style, padding)
|
435
|
+
items = [Layout::TextFragment.create(@field.field_value, style)]
|
436
|
+
layouter = Layout::TextLayouter.new(style)
|
437
|
+
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
438
|
+
|
439
|
+
result = nil
|
440
|
+
if style.font_size == 0 # need to auto-size text
|
441
|
+
style.font_size = 12 # Adobe seems to use this as starting point
|
442
|
+
style.clear_cache
|
443
|
+
loop do
|
444
|
+
result = layouter.fit(items, rect.width - 4 * padding, rect.height - 4 * padding)
|
445
|
+
break if result.status == :success || style.font_size <= 4 # don't make text too small
|
446
|
+
style.font_size -= 1
|
447
|
+
style.clear_cache
|
448
|
+
end
|
449
|
+
else
|
450
|
+
result = layouter.fit(items, rect.width - 4 * padding, 2**20)
|
451
|
+
end
|
452
|
+
|
453
|
+
unless result.lines.empty?
|
454
|
+
result.draw(canvas, 2 * padding, rect.height - 2 * padding - result.lines[0].height / 2.0)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Draws the visible option items of the list box in the widget's rectangle.
|
459
|
+
def draw_list_box(canvas, rect, style, padding)
|
460
|
+
option_items = @field.option_items
|
461
|
+
top_index = @field.list_box_top_index
|
462
|
+
items = [Layout::TextFragment.create(option_items[top_index..-1].join("\n"), style)]
|
463
|
+
|
464
|
+
indices = @field[:I] || []
|
465
|
+
value_indices = [@field.field_value].flatten.compact.map {|val| option_items.index(val) }
|
466
|
+
indices = value_indices if indices != value_indices
|
467
|
+
|
468
|
+
layouter = Layout::TextLayouter.new(style)
|
469
|
+
layouter.style.align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
470
|
+
result = layouter.fit(items, rect.width - 4 * padding, rect.height)
|
471
|
+
|
472
|
+
unless result.lines.empty?
|
473
|
+
top_gap = style.line_spacing.gap(result.lines[0], result.lines[0])
|
474
|
+
line_height = style.line_spacing.baseline_distance(result.lines[0], result.lines[0])
|
475
|
+
canvas.fill_color(153, 193, 218) # Adobe's color for selection highlighting
|
476
|
+
indices.map! {|i| rect.height - padding - (i - top_index + 1) * line_height }.each do |y|
|
477
|
+
next if y + line_height > rect.height || y + line_height < padding
|
478
|
+
canvas.rectangle(padding, y, rect.width - 2 * padding, line_height)
|
479
|
+
end
|
480
|
+
canvas.fill if canvas.graphics_object == :path
|
481
|
+
result.draw(canvas, 2 * padding, rect.height - padding - top_gap)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
388
485
|
# Calculates the font size for text fields based on the font and font size of the default
|
389
486
|
# appearance string, the annotation rectangle and the border style.
|
390
487
|
def calculate_font_size(font, font_size, rect, border_style)
|
391
488
|
if font_size == 0
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
489
|
+
if @field.concrete_field_type == :multiline_text_field
|
490
|
+
0 # Handled by multiline drawing code
|
491
|
+
elsif @field.concrete_field_type == :list_box
|
492
|
+
12 # Seems to be Adobe's default
|
493
|
+
else
|
494
|
+
unit_font_size = (font.wrapped_font.bounding_box[3] - font.wrapped_font.bounding_box[1]) *
|
495
|
+
font.scaling_factor / 1000.0
|
496
|
+
# The constant factor was found empirically by checking what Adobe Reader etc. do
|
497
|
+
(rect.height - 2 * border_style.width) / unit_font_size * 0.83
|
498
|
+
end
|
396
499
|
else
|
397
500
|
font_size
|
398
501
|
end
|