hexapdf 0.12.3 → 0.14.3
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 +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
|