hexapdf 0.13.0 → 0.14.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 +51 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/document.rb +10 -4
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/object.rb +22 -0
- data/lib/hexapdf/parser.rb +23 -1
- data/lib/hexapdf/pdf_array.rb +2 -2
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +127 -27
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
- data/lib/hexapdf/type/acro_form/choice_field.rb +64 -10
- data/lib/hexapdf/type/acro_form/form.rb +133 -10
- data/lib/hexapdf/type/acro_form/text_field.rb +68 -3
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type0.rb +3 -3
- data/lib/hexapdf/type/form.rb +4 -1
- data/lib/hexapdf/type/page.rb +5 -5
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +9 -1
- data/test/hexapdf/test_document.rb +14 -6
- data/test/hexapdf/test_object.rb +27 -0
- data/test/hexapdf/test_parser.rb +46 -0
- data/test/hexapdf/test_pdf_array.rb +1 -1
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +286 -34
- 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_form.rb +83 -11
- data/test/hexapdf/type/acro_form/test_text_field.rb +75 -1
- data/test/hexapdf/type/test_form.rb +7 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/test_helper.rb +2 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cea16b918ff9aa6e7b32759295ef4ab38c899bcbd227d76ad42e0c971360239
|
4
|
+
data.tar.gz: 932c5edf01114a59d0a64776f304e29f3c8865a2c2c52c340064180464aabad7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5883c5788487830b0403459b38b4ed1761c1015688977e9823f3c572f1ad645b06eb0578b185ce26f7f02c560050dd8ec7c09e8524b59cd35df4fd6abd1fb4aa
|
7
|
+
data.tar.gz: cdda51a089c86f27319fe424c9a74dc599ed60860338ef49958cd6a820141fa87a0624f2c657565e3f1b4a2392300807b89886178da6af62d22fa03fb543e372
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,54 @@
|
|
1
|
+
## 0.14.0 - 2020-12-30
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Support for creating AcroForm multiline text fields and their appearances
|
6
|
+
* Support for creating AcroForm comb text fields and their appearances
|
7
|
+
* Support for creating AcroForm password fields and their appearances
|
8
|
+
* Support for creating AcroForm file select fields and their appearances
|
9
|
+
* Support for creating AcroForm list box appearances
|
10
|
+
* [HexaPDF::Type::AcroForm::ChoiceField#list_box_top_index] and its setter
|
11
|
+
method
|
12
|
+
* [HexaPDF::Type::AcroForm::ChoiceField#update_widgets] to create appearances if
|
13
|
+
they don't exist
|
14
|
+
* Methods for caching data to [HexaPDF::Object]
|
15
|
+
* Support for splitting by page size to CLI command `hexapdf split`
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
|
19
|
+
* [HexaPDF::Utils::ObjectHash#oids] to be public instead of private
|
20
|
+
* Cross-reference table parsing to handle invalidly numbered main sections
|
21
|
+
* [HexaPDF::Document#cache] and [HexaPDF::Object#cache] to allow updating
|
22
|
+
values for existing keys
|
23
|
+
* Appearance creation methods of AcroForm objects to allow forcing the creation
|
24
|
+
of new appearances
|
25
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator#create_text_appearances] to
|
26
|
+
re-use existing form objects
|
27
|
+
* AcroForm field creation methods to allow specifying often used field
|
28
|
+
properties
|
29
|
+
|
30
|
+
### Fixed
|
31
|
+
|
32
|
+
* Missing usage of `:sort` flag for AcroForm choice fields
|
33
|
+
* Setting the `/I` field for AcroForm list boxes with multiple selection
|
34
|
+
* [HexaPDF::Layout::TextLayouter::SimpleLineWrapping] to remove glue items
|
35
|
+
(whitespace) before a hard line break
|
36
|
+
* Infinite loop when reconstructing the cross-reference table
|
37
|
+
* [HexaPDF::Type::AcroForm::ChoiceField] to support export values for option
|
38
|
+
items
|
39
|
+
* AcroForm text field appearance creation to only create a new appearance if the
|
40
|
+
field's value has changed
|
41
|
+
* AcroForm choice field appearance creation to only create a new appearance if
|
42
|
+
the involved dictionary fields' values have changed
|
43
|
+
* [HexaPDF::Type::AcroForm::ChoiceField#list_box_top_index=] to raise an error
|
44
|
+
if no option items are set
|
45
|
+
* [HexaPDF::PDFArray#to_ary] to return an array with preprocessed values
|
46
|
+
* [HexaPDF::Type::Form#contents=] to clear cached values to avoid returning e.g.
|
47
|
+
an invalid canvas object later
|
48
|
+
* [HexaPDF::Type::AcroForm::ButtonField#update_widgets] to create appearances if
|
49
|
+
they don't exist
|
50
|
+
|
51
|
+
|
1
52
|
## 0.13.0 - 2020-11-15
|
2
53
|
|
3
54
|
### Added
|
data/examples/019-acro_form.rb
CHANGED
@@ -42,10 +42,47 @@ rb = form.create_radio_button("Radio")
|
|
42
42
|
end
|
43
43
|
rb.field_value = :button0
|
44
44
|
|
45
|
-
canvas.text("Text
|
46
|
-
|
47
|
-
|
48
|
-
tx.
|
45
|
+
canvas.text("Text fields", at: [50, 450])
|
46
|
+
|
47
|
+
canvas.text("Single line", at: [70, 420])
|
48
|
+
tx = form.create_text_field("Single Line", font_size: 16)
|
49
|
+
widget = tx.create_widget(page, Rect: [200, 415, 500, 435])
|
49
50
|
tx.field_value = "A sample test string!"
|
50
51
|
|
52
|
+
canvas.text("Multiline", at: [70, 390])
|
53
|
+
tx = form.create_multiline_text_field("Multiline", font_size: 0, align: :right)
|
54
|
+
widget = tx.create_widget(page, Rect: [200, 325, 500, 405])
|
55
|
+
widget.border_style(color: 0, width: 1)
|
56
|
+
tx.field_value = "A sample test string! " * 30 + "\nNew line\n\nAnother line"
|
57
|
+
|
58
|
+
canvas.text("Password", at: [70, 300])
|
59
|
+
tx = form.create_password_field("Password", font_size: 16)
|
60
|
+
widget = tx.create_widget(page, Rect: [200, 295, 500, 315])
|
61
|
+
|
62
|
+
canvas.text("File select", at: [70, 270])
|
63
|
+
tx = form.create_file_select_field("File Select", font_size: 16)
|
64
|
+
widget = tx.create_widget(page, Rect: [200, 265, 500, 285])
|
65
|
+
tx.field_value = "path/to/file.pdf"
|
66
|
+
|
67
|
+
canvas.text("Comb", at: [70, 240])
|
68
|
+
tx = form.create_comb_text_field("Comb field", max_chars: 10, font_size: 16, align: :center)
|
69
|
+
widget = tx.create_widget(page, Rect: [200, 220, 500, 255])
|
70
|
+
widget.border_style(color: [30, 128, 0], width: 1)
|
71
|
+
tx.field_value = 'Hello'
|
72
|
+
|
73
|
+
canvas.text("Combo Box", at: [50, 170])
|
74
|
+
cb = form.create_combo_box("Combo Box", font_size: 12, editable: true,
|
75
|
+
option_items: ['Value 1', 'Another value', 'Choose me!'])
|
76
|
+
widget = cb.create_widget(page, Rect: [200, 150, 500, 185])
|
77
|
+
widget.border_style(width: 1)
|
78
|
+
cb.field_value = 'Another value'
|
79
|
+
|
80
|
+
canvas.text("List Box", at: [50, 120])
|
81
|
+
lb = form.create_list_box("List Box", font_size: 15, align: :center, multi_select: true,
|
82
|
+
option_items: 1.upto(7).map {|i| "Value #{i}" })
|
83
|
+
widget = lb.create_widget(page, Rect: [200, 50, 500, 135])
|
84
|
+
widget.border_style(width: 1)
|
85
|
+
lb.list_box_top_index = 1
|
86
|
+
lb.field_value = ['Value 6', 'Value 2']
|
87
|
+
|
51
88
|
doc.write('acro_form.pdf', optimize: true)
|
data/lib/hexapdf/cli/split.rb
CHANGED
@@ -44,16 +44,28 @@ module HexaPDF
|
|
44
44
|
|
45
45
|
def initialize #:nodoc:
|
46
46
|
super('split', takes_commands: false)
|
47
|
-
short_desc("Split a PDF file
|
47
|
+
short_desc("Split a PDF file")
|
48
48
|
long_desc(<<~EOF)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
The default strategy is to split a PDF into individual pages, i.e. splitting is done by
|
50
|
+
page number. It is also possible to split by page size where pages with the same page size
|
51
|
+
get put into the same output PDF.
|
52
|
+
|
53
|
+
If no OUTPUT_SPEC is specified, the resulting PDF files are named <PDF>_0001.pdf,
|
54
|
+
<PDF>_0002.pdf, ... when splitting by page number and <PDF>_A4.pdf, <PDF>_Letter.pdf, ...
|
55
|
+
when splitting by page size.
|
56
|
+
|
57
|
+
To specify a custom name, provide the OUTPUT_SPEC argument. It can contain a printf-style
|
58
|
+
format definition like '%04d' to specify the place where the page number should be
|
59
|
+
inserted. In case of splitting by page size, the place of the format defintion is replaced
|
60
|
+
with the name of the page size, e.g. A4 or Letter.
|
53
61
|
|
54
62
|
The optimization and encryption options are applied to each created output file.
|
55
63
|
EOF
|
56
64
|
|
65
|
+
options.on("--strategy STRATEGY", "-s", [:page_number, :page_size], "Defines how the PDF " \
|
66
|
+
"file should be split: page_number or page_size (default: page_number)") do |s|
|
67
|
+
@strategy = s
|
68
|
+
end
|
57
69
|
options.on("--password PASSWORD", "-p", String,
|
58
70
|
"The password for decryption. Use - for reading from standard input.") do |pwd|
|
59
71
|
@password = (pwd == '-' ? read_password : pwd)
|
@@ -62,23 +74,71 @@ module HexaPDF
|
|
62
74
|
define_encryption_options
|
63
75
|
|
64
76
|
@password = nil
|
77
|
+
@strategy = :page_number
|
65
78
|
end
|
66
79
|
|
67
80
|
def execute(pdf, output_spec = pdf.sub(/\.pdf$/i, '_%04d.pdf')) #:nodoc:
|
68
|
-
output_spec = output_spec.sub('%', '%<page>')
|
69
81
|
with_document(pdf, password: @password) do |doc|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
out.pages.add(out.import(page))
|
75
|
-
apply_encryption_options(out)
|
76
|
-
apply_optimization_options(out)
|
77
|
-
write_document(out, output_file)
|
82
|
+
if @strategy == :page_number
|
83
|
+
split_by_page_number(doc, output_spec)
|
84
|
+
else
|
85
|
+
split_by_page_size(doc, output_spec)
|
78
86
|
end
|
79
87
|
end
|
80
88
|
end
|
81
89
|
|
90
|
+
private
|
91
|
+
|
92
|
+
# Splits the document into individual pages.
|
93
|
+
def split_by_page_number(doc, output_spec)
|
94
|
+
doc.pages.each_with_index do |page, index|
|
95
|
+
output_file = sprintf(output_spec, index + 1)
|
96
|
+
maybe_raise_on_existing_file(output_file)
|
97
|
+
out = HexaPDF::Document.new
|
98
|
+
out.pages.add(out.import(page))
|
99
|
+
apply_encryption_options(out)
|
100
|
+
apply_optimization_options(out)
|
101
|
+
write_document(out, output_file)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Splits the document into files based on the page sizes.
|
106
|
+
def split_by_page_size(doc, output_spec)
|
107
|
+
output_spec = output_spec.sub(/%.*?[a-zA-Z]/, '%s')
|
108
|
+
out_files = Hash.new do |hash, key|
|
109
|
+
output_file = sprintf(output_spec, key)
|
110
|
+
maybe_raise_on_existing_file(output_file)
|
111
|
+
out = HexaPDF::Document.new
|
112
|
+
out.config['output_file'] = output_file
|
113
|
+
hash[key] = out
|
114
|
+
end
|
115
|
+
|
116
|
+
doc.pages.each do |page|
|
117
|
+
out = out_files[page_size_name(page.box(:media).value)]
|
118
|
+
out.pages.add(out.import(page))
|
119
|
+
end
|
120
|
+
|
121
|
+
out_files.each_value do |out|
|
122
|
+
apply_encryption_options(out)
|
123
|
+
apply_optimization_options(out)
|
124
|
+
write_document(out, out.config['output_file'])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Tries to retrieve a page size name based on the media box. If this is not possible, the
|
129
|
+
# returned page size name consists of width x height.
|
130
|
+
def page_size_name(media_box)
|
131
|
+
@page_name_cache ||= {}
|
132
|
+
return @page_name_cache[media_box] if @page_name_cache.key?(media_box)
|
133
|
+
|
134
|
+
paper_size = HexaPDF::Type::Page::PAPER_SIZE.find do |_name, box|
|
135
|
+
box.each_with_index.all? {|entry, index| (entry - media_box[index]).abs < 5 }
|
136
|
+
end
|
137
|
+
|
138
|
+
@page_name_cache[media_box] =
|
139
|
+
paper_size ? paper_size[0] : "%.0fx%.0f" % media_box.values_at(2, 3)
|
140
|
+
end
|
141
|
+
|
82
142
|
end
|
83
143
|
|
84
144
|
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -469,15 +469,21 @@ module HexaPDF
|
|
469
469
|
@listeners[name]&.each {|obj| obj.call(*args) }
|
470
470
|
end
|
471
471
|
|
472
|
-
|
473
|
-
|
472
|
+
UNSET = ::Object.new # :nordoc:
|
473
|
+
|
474
|
+
# Caches and returns the given +value+ or the value of the given block using the given
|
475
|
+
# +pdf_data+ and +key+ arguments as composite cache key. If a cached value already exists and
|
476
|
+
# +update+ is +false+, the cached value is just returned.
|
477
|
+
#
|
478
|
+
# Set +update+ to +true+ to force an update of the cached value.
|
474
479
|
#
|
475
480
|
# This facility can be used to cache expensive operations in PDF objects that are easy to
|
476
481
|
# compute again.
|
477
482
|
#
|
478
483
|
# Use #clear_cache to clear the cache if necessary.
|
479
|
-
def cache(pdf_data, key, value =
|
480
|
-
@cache[pdf_data][key]
|
484
|
+
def cache(pdf_data, key, value = UNSET, update: false)
|
485
|
+
return @cache[pdf_data][key] if cached?(pdf_data, key) && !update
|
486
|
+
@cache[pdf_data][key] = (value == UNSET ? yield : value)
|
481
487
|
end
|
482
488
|
|
483
489
|
# Returns +true+ if there is a value cached for the composite key consisting of the given
|
@@ -388,7 +388,7 @@ module HexaPDF
|
|
388
388
|
end
|
389
389
|
when :penalty
|
390
390
|
if item.penalty <= -Penalty::INFINITY
|
391
|
-
add_box_item(item.item) if item.
|
391
|
+
add_box_item(item.item) if item.width > 0
|
392
392
|
break unless yield(create_unjustified_line, item)
|
393
393
|
reset_after_line_break(index + 1)
|
394
394
|
elsif item.penalty >= Penalty::INFINITY
|
@@ -458,7 +458,7 @@ module HexaPDF
|
|
458
458
|
end
|
459
459
|
when :penalty
|
460
460
|
if item.penalty <= -Penalty::INFINITY
|
461
|
-
add_box_item(item.item) if item.
|
461
|
+
add_box_item(item.item) if item.width > 0
|
462
462
|
break unless (action = yield(create_unjustified_line, item))
|
463
463
|
reset_after_line_break_variable_width(index + 1, true, action)
|
464
464
|
elsif item.penalty >= Penalty::INFINITY
|
data/lib/hexapdf/object.rb
CHANGED
@@ -284,6 +284,28 @@ module HexaPDF
|
|
284
284
|
obj
|
285
285
|
end
|
286
286
|
|
287
|
+
# Caches and returns the given +value+ or the value of the block under the given cache key. If
|
288
|
+
# there is already a cached value for the key and +update+ is +false+, it is just returned.
|
289
|
+
#
|
290
|
+
# Set +update+ to +true+ to force an update of the cached value.
|
291
|
+
#
|
292
|
+
# This uses Document#cache internally.
|
293
|
+
def cache(key, value = Document::UNSET, update: false, &block)
|
294
|
+
document.cache(@data, key, value, update: update, &block)
|
295
|
+
end
|
296
|
+
|
297
|
+
# Returns +true+ if there is a cached value for the given key.
|
298
|
+
#
|
299
|
+
# This uses Document#cached? internally.
|
300
|
+
def cached?(key)
|
301
|
+
document.cached?(@data, key)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Clears the cache for this object.
|
305
|
+
def clear_cache
|
306
|
+
document.clear_cache(@data)
|
307
|
+
end
|
308
|
+
|
287
309
|
# Compares this object to another object.
|
288
310
|
#
|
289
311
|
# If the other object does not respond to +oid+ or +gen+, +nil+ is returned. Otherwise objects
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -267,6 +267,27 @@ module HexaPDF
|
|
267
267
|
raise_malformed("Trailer is #{trailer.class} instead of dictionary ", pos: @tokenizer.pos)
|
268
268
|
end
|
269
269
|
|
270
|
+
unless trailer[:Prev] || xref.max_oid == 0 || xref.entry?(0)
|
271
|
+
first_entry = xref[xref.oids[0]]
|
272
|
+
test_entry = xref[xref.oids[-1]]
|
273
|
+
@tokenizer.pos = test_entry.pos + @header_offset
|
274
|
+
test_oid = @tokenizer.next_token
|
275
|
+
first_oid = first_entry.oid
|
276
|
+
|
277
|
+
force_failure = !first_entry.free? || first_entry.gen != 65535 ||
|
278
|
+
!test_oid.kind_of?(Integer) || xref.oids[-1] - test_oid != first_oid
|
279
|
+
maybe_raise("Main cross-reference section has invalid numbering",
|
280
|
+
pos: offset + @header_offset, force: force_failure)
|
281
|
+
|
282
|
+
new_xref = XRefSection.new
|
283
|
+
xref.oids.each do |oid|
|
284
|
+
entry = xref[oid]
|
285
|
+
entry.oid -= first_oid
|
286
|
+
new_xref.send(:[]=, entry.oid, entry.gen, entry)
|
287
|
+
end
|
288
|
+
xref = new_xref
|
289
|
+
end
|
290
|
+
|
270
291
|
[xref, trailer]
|
271
292
|
end
|
272
293
|
|
@@ -359,8 +380,9 @@ module HexaPDF
|
|
359
380
|
xref = XRefSection.new
|
360
381
|
@tokenizer.pos = 0
|
361
382
|
while true
|
383
|
+
@tokenizer.skip_whitespace
|
362
384
|
pos = @tokenizer.pos
|
363
|
-
@tokenizer.scan_until(/(\n|\r\n?)
|
385
|
+
@tokenizer.scan_until(/(\n|\r\n?)+/)
|
364
386
|
next_new_line_pos = @tokenizer.pos
|
365
387
|
@tokenizer.pos = pos
|
366
388
|
|
data/lib/hexapdf/pdf_array.rb
CHANGED
@@ -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,35 @@ 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
|
+
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
249
|
+
form.contents = ''
|
254
250
|
form[:Resources] = HexaPDF::Object.deep_copy(default_resources)
|
255
251
|
|
256
252
|
canvas = form.canvas
|
257
253
|
apply_background_and_border(border_style, canvas)
|
258
254
|
style.font_size = calculate_font_size(font, font_size, rect, border_style)
|
255
|
+
style.clear_cache
|
259
256
|
|
260
257
|
canvas.marked_content_sequence(:Tx) do
|
261
|
-
if
|
258
|
+
if @field.field_value || @field.concrete_field_type == :list_box
|
262
259
|
canvas.save_graphics_state do
|
263
260
|
canvas.rectangle(padding, padding, rect.width - 2 * padding,
|
264
261
|
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)
|
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
|
278
269
|
end
|
279
270
|
end
|
280
271
|
end
|
281
272
|
end
|
282
273
|
|
283
274
|
alias create_combo_box_appearances create_text_appearances
|
275
|
+
alias create_list_box_appearances create_text_appearances
|
284
276
|
|
285
277
|
private
|
286
278
|
|
@@ -341,6 +333,13 @@ module HexaPDF
|
|
341
333
|
canvas.circle(rect.width / 2.0, rect.height / 2.0, [width / 2.0, height / 2.0].min)
|
342
334
|
else
|
343
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
|
344
343
|
end
|
345
344
|
end
|
346
345
|
canvas.stroke
|
@@ -385,14 +384,115 @@ module HexaPDF
|
|
385
384
|
end
|
386
385
|
end
|
387
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
|
+
|
388
482
|
# Calculates the font size for text fields based on the font and font size of the default
|
389
483
|
# appearance string, the annotation rectangle and the border style.
|
390
484
|
def calculate_font_size(font, font_size, rect, border_style)
|
391
485
|
if font_size == 0
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
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
|
396
496
|
else
|
397
497
|
font_size
|
398
498
|
end
|