hexapdf 0.13.0 → 0.14.0
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 +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
|