fillform 0.1.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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE +26 -0
- data/README.md +126 -0
- data/data/afm/Courier-Bold.afm +342 -0
- data/data/afm/Courier-BoldOblique.afm +342 -0
- data/data/afm/Courier-Oblique.afm +342 -0
- data/data/afm/Courier.afm +342 -0
- data/data/afm/Helvetica-Bold.afm +2827 -0
- data/data/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/afm/Helvetica-Oblique.afm +3051 -0
- data/data/afm/Helvetica.afm +3051 -0
- data/data/afm/MustRead.html +19 -0
- data/data/afm/Symbol.afm +213 -0
- data/data/afm/Times-Bold.afm +2588 -0
- data/data/afm/Times-BoldItalic.afm +2384 -0
- data/data/afm/Times-Italic.afm +2667 -0
- data/data/afm/Times-Roman.afm +2419 -0
- data/data/afm/ZapfDingbats.afm +225 -0
- data/data/agl/LICENSE.md +26 -0
- data/data/agl/glyphlist.txt +4325 -0
- data/data/agl/zapfdingbats.txt +245 -0
- data/lib/fillform/appearance.rb +315 -0
- data/lib/fillform/border.rb +135 -0
- data/lib/fillform/data_format.rb +45 -0
- data/lib/fillform/field.rb +15 -0
- data/lib/fillform/filler.rb +82 -0
- data/lib/fillform/flattener.rb +114 -0
- data/lib/fillform/float32.rb +12 -0
- data/lib/fillform/font.rb +165 -0
- data/lib/fillform/form.rb +165 -0
- data/lib/fillform/glyphs.rb +99 -0
- data/lib/fillform/line_breaker.rb +98 -0
- data/lib/fillform/merged_widget.rb +20 -0
- data/lib/fillform/pdf/document.rb +240 -0
- data/lib/fillform/pdf/filters.rb +108 -0
- data/lib/fillform/pdf/objects.rb +48 -0
- data/lib/fillform/pdf/parser.rb +205 -0
- data/lib/fillform/pdf/serializer.rb +47 -0
- data/lib/fillform/pdf/text.rb +47 -0
- data/lib/fillform/pdf/writer.rb +51 -0
- data/lib/fillform/pdf.rb +9 -0
- data/lib/fillform/pdf_forms.rb +15 -0
- data/lib/fillform/pdftk_wrapper.rb +81 -0
- data/lib/fillform/report.rb +180 -0
- data/lib/fillform/version.rb +5 -0
- data/lib/fillform.rb +65 -0
- metadata +92 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# -----------------------------------------------------------
|
|
2
|
+
# Copyright 2002-2019 Adobe (http://www.adobe.com/).
|
|
3
|
+
#
|
|
4
|
+
# Redistribution and use in source and binary forms, with or
|
|
5
|
+
# without modification, are permitted provided that the
|
|
6
|
+
# following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# Redistributions of source code must retain the above
|
|
9
|
+
# copyright notice, this list of conditions and the following
|
|
10
|
+
# disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# Redistributions in binary form must reproduce the above
|
|
13
|
+
# copyright notice, this list of conditions and the following
|
|
14
|
+
# disclaimer in the documentation and/or other materials
|
|
15
|
+
# provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# Neither the name of Adobe nor the names of its contributors
|
|
18
|
+
# may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
22
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
23
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
24
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
27
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
28
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
29
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
30
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
31
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
32
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
33
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
34
|
+
# -----------------------------------------------------------
|
|
35
|
+
# Name: ITC Zapf Dingbats Glyph List
|
|
36
|
+
# Table version: 2.0
|
|
37
|
+
# Date: September 20, 2002
|
|
38
|
+
# URL: https://github.com/adobe-type-tools/agl-aglfn
|
|
39
|
+
#
|
|
40
|
+
# Format: two semicolon-delimited fields:
|
|
41
|
+
# (1) glyph name--upper/lowercase letters and digits
|
|
42
|
+
# (2) Unicode scalar value--four uppercase hexadecimal digits
|
|
43
|
+
#
|
|
44
|
+
a100;275E
|
|
45
|
+
a101;2761
|
|
46
|
+
a102;2762
|
|
47
|
+
a103;2763
|
|
48
|
+
a104;2764
|
|
49
|
+
a105;2710
|
|
50
|
+
a106;2765
|
|
51
|
+
a107;2766
|
|
52
|
+
a108;2767
|
|
53
|
+
a109;2660
|
|
54
|
+
a10;2721
|
|
55
|
+
a110;2665
|
|
56
|
+
a111;2666
|
|
57
|
+
a112;2663
|
|
58
|
+
a117;2709
|
|
59
|
+
a118;2708
|
|
60
|
+
a119;2707
|
|
61
|
+
a11;261B
|
|
62
|
+
a120;2460
|
|
63
|
+
a121;2461
|
|
64
|
+
a122;2462
|
|
65
|
+
a123;2463
|
|
66
|
+
a124;2464
|
|
67
|
+
a125;2465
|
|
68
|
+
a126;2466
|
|
69
|
+
a127;2467
|
|
70
|
+
a128;2468
|
|
71
|
+
a129;2469
|
|
72
|
+
a12;261E
|
|
73
|
+
a130;2776
|
|
74
|
+
a131;2777
|
|
75
|
+
a132;2778
|
|
76
|
+
a133;2779
|
|
77
|
+
a134;277A
|
|
78
|
+
a135;277B
|
|
79
|
+
a136;277C
|
|
80
|
+
a137;277D
|
|
81
|
+
a138;277E
|
|
82
|
+
a139;277F
|
|
83
|
+
a13;270C
|
|
84
|
+
a140;2780
|
|
85
|
+
a141;2781
|
|
86
|
+
a142;2782
|
|
87
|
+
a143;2783
|
|
88
|
+
a144;2784
|
|
89
|
+
a145;2785
|
|
90
|
+
a146;2786
|
|
91
|
+
a147;2787
|
|
92
|
+
a148;2788
|
|
93
|
+
a149;2789
|
|
94
|
+
a14;270D
|
|
95
|
+
a150;278A
|
|
96
|
+
a151;278B
|
|
97
|
+
a152;278C
|
|
98
|
+
a153;278D
|
|
99
|
+
a154;278E
|
|
100
|
+
a155;278F
|
|
101
|
+
a156;2790
|
|
102
|
+
a157;2791
|
|
103
|
+
a158;2792
|
|
104
|
+
a159;2793
|
|
105
|
+
a15;270E
|
|
106
|
+
a160;2794
|
|
107
|
+
a161;2192
|
|
108
|
+
a162;27A3
|
|
109
|
+
a163;2194
|
|
110
|
+
a164;2195
|
|
111
|
+
a165;2799
|
|
112
|
+
a166;279B
|
|
113
|
+
a167;279C
|
|
114
|
+
a168;279D
|
|
115
|
+
a169;279E
|
|
116
|
+
a16;270F
|
|
117
|
+
a170;279F
|
|
118
|
+
a171;27A0
|
|
119
|
+
a172;27A1
|
|
120
|
+
a173;27A2
|
|
121
|
+
a174;27A4
|
|
122
|
+
a175;27A5
|
|
123
|
+
a176;27A6
|
|
124
|
+
a177;27A7
|
|
125
|
+
a178;27A8
|
|
126
|
+
a179;27A9
|
|
127
|
+
a17;2711
|
|
128
|
+
a180;27AB
|
|
129
|
+
a181;27AD
|
|
130
|
+
a182;27AF
|
|
131
|
+
a183;27B2
|
|
132
|
+
a184;27B3
|
|
133
|
+
a185;27B5
|
|
134
|
+
a186;27B8
|
|
135
|
+
a187;27BA
|
|
136
|
+
a188;27BB
|
|
137
|
+
a189;27BC
|
|
138
|
+
a18;2712
|
|
139
|
+
a190;27BD
|
|
140
|
+
a191;27BE
|
|
141
|
+
a192;279A
|
|
142
|
+
a193;27AA
|
|
143
|
+
a194;27B6
|
|
144
|
+
a195;27B9
|
|
145
|
+
a196;2798
|
|
146
|
+
a197;27B4
|
|
147
|
+
a198;27B7
|
|
148
|
+
a199;27AC
|
|
149
|
+
a19;2713
|
|
150
|
+
a1;2701
|
|
151
|
+
a200;27AE
|
|
152
|
+
a201;27B1
|
|
153
|
+
a202;2703
|
|
154
|
+
a203;2750
|
|
155
|
+
a204;2752
|
|
156
|
+
a205;276E
|
|
157
|
+
a206;2770
|
|
158
|
+
a20;2714
|
|
159
|
+
a21;2715
|
|
160
|
+
a22;2716
|
|
161
|
+
a23;2717
|
|
162
|
+
a24;2718
|
|
163
|
+
a25;2719
|
|
164
|
+
a26;271A
|
|
165
|
+
a27;271B
|
|
166
|
+
a28;271C
|
|
167
|
+
a29;2722
|
|
168
|
+
a2;2702
|
|
169
|
+
a30;2723
|
|
170
|
+
a31;2724
|
|
171
|
+
a32;2725
|
|
172
|
+
a33;2726
|
|
173
|
+
a34;2727
|
|
174
|
+
a35;2605
|
|
175
|
+
a36;2729
|
|
176
|
+
a37;272A
|
|
177
|
+
a38;272B
|
|
178
|
+
a39;272C
|
|
179
|
+
a3;2704
|
|
180
|
+
a40;272D
|
|
181
|
+
a41;272E
|
|
182
|
+
a42;272F
|
|
183
|
+
a43;2730
|
|
184
|
+
a44;2731
|
|
185
|
+
a45;2732
|
|
186
|
+
a46;2733
|
|
187
|
+
a47;2734
|
|
188
|
+
a48;2735
|
|
189
|
+
a49;2736
|
|
190
|
+
a4;260E
|
|
191
|
+
a50;2737
|
|
192
|
+
a51;2738
|
|
193
|
+
a52;2739
|
|
194
|
+
a53;273A
|
|
195
|
+
a54;273B
|
|
196
|
+
a55;273C
|
|
197
|
+
a56;273D
|
|
198
|
+
a57;273E
|
|
199
|
+
a58;273F
|
|
200
|
+
a59;2740
|
|
201
|
+
a5;2706
|
|
202
|
+
a60;2741
|
|
203
|
+
a61;2742
|
|
204
|
+
a62;2743
|
|
205
|
+
a63;2744
|
|
206
|
+
a64;2745
|
|
207
|
+
a65;2746
|
|
208
|
+
a66;2747
|
|
209
|
+
a67;2748
|
|
210
|
+
a68;2749
|
|
211
|
+
a69;274A
|
|
212
|
+
a6;271D
|
|
213
|
+
a70;274B
|
|
214
|
+
a71;25CF
|
|
215
|
+
a72;274D
|
|
216
|
+
a73;25A0
|
|
217
|
+
a74;274F
|
|
218
|
+
a75;2751
|
|
219
|
+
a76;25B2
|
|
220
|
+
a77;25BC
|
|
221
|
+
a78;25C6
|
|
222
|
+
a79;2756
|
|
223
|
+
a7;271E
|
|
224
|
+
a81;25D7
|
|
225
|
+
a82;2758
|
|
226
|
+
a83;2759
|
|
227
|
+
a84;275A
|
|
228
|
+
a85;276F
|
|
229
|
+
a86;2771
|
|
230
|
+
a87;2772
|
|
231
|
+
a88;2773
|
|
232
|
+
a89;2768
|
|
233
|
+
a8;271F
|
|
234
|
+
a90;2769
|
|
235
|
+
a91;276C
|
|
236
|
+
a92;276D
|
|
237
|
+
a93;276A
|
|
238
|
+
a94;276B
|
|
239
|
+
a95;2774
|
|
240
|
+
a96;2775
|
|
241
|
+
a97;275B
|
|
242
|
+
a98;275C
|
|
243
|
+
a99;275D
|
|
244
|
+
a9;2720
|
|
245
|
+
# END
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
# The appearance stream iText's TextField#getAppearance builds: background and border of the widget,
|
|
5
|
+
# then the text with iText's auto font size, baseline, comb cells, alignment and line spacing.
|
|
6
|
+
class Appearance
|
|
7
|
+
COMB = 1 << 24
|
|
8
|
+
MULTILINE = 1 << 12
|
|
9
|
+
MULTILINE_MAX_AUTO_SIZE = 12
|
|
10
|
+
MIN_AUTO_SIZE = 4
|
|
11
|
+
|
|
12
|
+
DEFAULT_APPEARANCE = /(\S*)\s+([\d.]+)\s+Tf/
|
|
13
|
+
ROTATIONS = { 90 => [0, 1, -1, 0, :height, 0], 180 => [-1, 0, 0, -1, :width, :height],
|
|
14
|
+
270 => [0, -1, 1, 0, 0, :width] }.freeze
|
|
15
|
+
|
|
16
|
+
# The fonts are looked up with a block: font(resources, name).
|
|
17
|
+
def initialize(doc, field, form:, &fonts)
|
|
18
|
+
@doc = doc
|
|
19
|
+
@field = field
|
|
20
|
+
@form = form
|
|
21
|
+
@fonts = fonts
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fill(value, shown = value)
|
|
25
|
+
@field[:V] = PDF::Text.encode(value)
|
|
26
|
+
@field.widgets.each { |widget| widget[:AP] = { N: stream_for(widget, shown) } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A combo box shows the text of the option whose export value is the value.
|
|
30
|
+
def fill_choice(value)
|
|
31
|
+
option = Array(@doc.deref(@field[:Opt])).map { |item| @doc.deref(item) }.find do |item|
|
|
32
|
+
item.is_a?(Array) && PDF::Text.decode(@doc.deref(item.first).to_s) == value
|
|
33
|
+
end
|
|
34
|
+
fill(value, option ? PDF::Text.decode(@doc.deref(option.last).to_s) : value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A list box shows the options from the first selected one on, the selected ones highlighted.
|
|
38
|
+
def fill_list(values)
|
|
39
|
+
options = choice_options
|
|
40
|
+
selections = values.filter_map { |value| options.index { |export, _display| export == value } }
|
|
41
|
+
@field[:V] = values.map { |value| PDF::Text.encode(value) }
|
|
42
|
+
@field[:I] = selections
|
|
43
|
+
@field.widgets.each do |widget|
|
|
44
|
+
widget[:AP] = { N: list_stream_for(widget, options.map(&:last), selections) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
LIST_HIGHLIGHT = Color.new([10, 36, 106].map { |component| component / 255.0 })
|
|
51
|
+
|
|
52
|
+
# [export, display] of each option.
|
|
53
|
+
def choice_options
|
|
54
|
+
Array(@doc.deref(@field[:Opt])).map do |option|
|
|
55
|
+
option = @doc.deref(option)
|
|
56
|
+
texts = Array(option).map { |text| PDF::Text.decode(@doc.deref(text).to_s) }
|
|
57
|
+
[texts.first, texts.last]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def list_stream_for(widget, choices, selections)
|
|
62
|
+
configure(widget)
|
|
63
|
+
@border = Border.new(@widget, @width, @height)
|
|
64
|
+
content = @border.appearance.b
|
|
65
|
+
content << "/Tx BMC ".b << list_text(choices, selections) << "EMC ".b unless choices.empty?
|
|
66
|
+
form_xobject(content)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def merged(key) = @widget[key]
|
|
70
|
+
|
|
71
|
+
# iText reads the attributes of each widget merged with the ones of its field and the form.
|
|
72
|
+
def configure(widget)
|
|
73
|
+
@widget = MergedWidget.new(widget, @field)
|
|
74
|
+
default_appearance = (merged(:DA) || @form[:DA]).to_s.b
|
|
75
|
+
name, size = default_appearance.match(DEFAULT_APPEARANCE)&.captures
|
|
76
|
+
resources = merged(:DR) || @form[:DR]
|
|
77
|
+
@font = @fonts.call(resources, name.to_s.split("/").last)
|
|
78
|
+
@font_size = size.to_f
|
|
79
|
+
@color = default_appearance.sub(DEFAULT_APPEARANCE, "").strip
|
|
80
|
+
@width, @height = box(widget.rect)
|
|
81
|
+
@width, @height = @height, @width if [90, 270].include?(rotation)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The rotation of the widget's appearance characteristics, in multiples of 90 degrees.
|
|
85
|
+
def rotation
|
|
86
|
+
characteristics = @widget[:MK]
|
|
87
|
+
degrees = characteristics.is_a?(Hash) ? @doc.deref(characteristics[:R]).to_i : 0
|
|
88
|
+
(degrees % 90).zero? ? degrees % 360 : 0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def form_xobject(content)
|
|
92
|
+
matrix = ROTATIONS.fetch(rotation, [1, 0, 0, 1, 0, 0]).map do |value|
|
|
93
|
+
{ width: @width, height: @height }.fetch(value, value)
|
|
94
|
+
end
|
|
95
|
+
@doc.add(PDF::Stream.compressed({ Type: :XObject, Subtype: :Form,
|
|
96
|
+
BBox: [0, 0, @width, @height].map { |number| n(number).to_f },
|
|
97
|
+
Matrix: matrix.map { |number| number.is_a?(Float) ? n(number).to_f : number },
|
|
98
|
+
Resources: { Font: { @font.key => @font.resource } } }, content))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# iText's TextField#getListAppearance
|
|
102
|
+
def list_text(choices, selections)
|
|
103
|
+
inset = @border.inner_width
|
|
104
|
+
size = @font_size.zero? ? MULTILINE_MAX_AUTO_SIZE : @font_size
|
|
105
|
+
leading = f(@font.top(size) - @font.bottom(size))
|
|
106
|
+
rows = visible_choices(choices.size, selections, f(inner_height / leading).to_i + 1)
|
|
107
|
+
clip = [inset, inset, f(@width - f(2 * inset)), f(@height - f(2 * inset))].map { |number| n(number) }.join(" ")
|
|
108
|
+
top = f(inset + inner_height)
|
|
109
|
+
"q #{clip} re W n #{LIST_HIGHLIGHT.fill} #{highlights(selections, rows, top, leading)}".b <<
|
|
110
|
+
list_rows(choices, selections, rows, size, top) << "Q ".b
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def visible_choices(count, selections, fitting)
|
|
114
|
+
first = selections.first ? selections.first.clamp(0, count) : 0
|
|
115
|
+
return (first...(first + fitting)) if first + fitting <= count
|
|
116
|
+
|
|
117
|
+
([0, count - fitting].max...count)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# iText also highlights a selection just below the last row it shows.
|
|
121
|
+
def highlights(selections, rows, top, leading)
|
|
122
|
+
inset = @border.inner_width
|
|
123
|
+
selections.grep(rows.begin..rows.end).map do |index|
|
|
124
|
+
"#{n(inset)} #{n(f(top - f(f(index - rows.begin + 1) * leading)))} " \
|
|
125
|
+
"#{n(f(@width - f(2 * inset)))} #{n(leading)} re f "
|
|
126
|
+
end.join
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def list_rows(choices, selections, rows, size, top)
|
|
130
|
+
x = f(@border.inner_width * 2)
|
|
131
|
+
leading = f(@font.top(size) - @font.bottom(size))
|
|
132
|
+
baseline = f(top - @font.top(size))
|
|
133
|
+
rows.map do |index|
|
|
134
|
+
color = selections.include?(index) ? "1 g" : @color
|
|
135
|
+
chars = LineBreaker.rstrip(choices[index].gsub(/\r\n?|\n/, " ").chars)
|
|
136
|
+
y = shown_y(baseline)
|
|
137
|
+
baseline = f(baseline - leading)
|
|
138
|
+
"#{color} ".b << text_object(size, [[x, y, chars]])
|
|
139
|
+
end.join.b
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def stream_for(widget, value)
|
|
143
|
+
configure(widget)
|
|
144
|
+
@border = Border.new(@widget, @width, @height, comb_cells: comb? ? max_length : 0)
|
|
145
|
+
form_xobject("#{@border.appearance}/Tx BMC ".b << variable_text(value) << "EMC ".b)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# iText computes the box in single precision.
|
|
149
|
+
def box(rect)
|
|
150
|
+
llx, lly, urx, ury = rect.pack("e4").unpack("e4")
|
|
151
|
+
[urx - llx, ury - lly].pack("e2").unpack("e2")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def variable_text(value)
|
|
155
|
+
return "".b if value.empty?
|
|
156
|
+
|
|
157
|
+
clip = [offset, offset, f(@width - f(2 * offset)), f(@height - f(2 * offset))].map { |number| n(number) }
|
|
158
|
+
"q #{clip.join(" ")} re W n #{@color} ".b << text(value) << "Q ".b
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# iText's offX: the clipping inset
|
|
162
|
+
def offset = [@border.inner_width, padding].min
|
|
163
|
+
|
|
164
|
+
# iText's offsetX: the text inset
|
|
165
|
+
def padding = [@border.inner_width, 1].max
|
|
166
|
+
|
|
167
|
+
# iText's h: the height inside the border
|
|
168
|
+
def inner_height
|
|
169
|
+
height = f(@height - f(@border.width * 2))
|
|
170
|
+
@border.inner_width == @border.width ? height : f(height - f(@border.width * 2))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def text(value)
|
|
174
|
+
return multiline_text(value) if multiline?
|
|
175
|
+
|
|
176
|
+
chars = value.gsub(/\r\n?|\n/, " ").chars
|
|
177
|
+
size = @font_size.zero? ? auto_size(chars) : f(@font_size)
|
|
178
|
+
baseline = single_line_baseline(size)
|
|
179
|
+
comb? ? comb_text(chars, size, baseline) : single_line_text(chars, size, baseline)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# iText's ColumnText#showTextAligned: a column of 20000 points from the anchor of the alignment.
|
|
183
|
+
def single_line_text(chars, size, baseline)
|
|
184
|
+
anchor = case alignment
|
|
185
|
+
when :center then f(@width / 2)
|
|
186
|
+
when :right then f(@width - f(2 * padding))
|
|
187
|
+
else f(2 * padding)
|
|
188
|
+
end
|
|
189
|
+
left, right = case alignment
|
|
190
|
+
when :center then [f(anchor - 20_000), f(anchor + 20_000)]
|
|
191
|
+
when :right then [f(anchor - 20_000), anchor]
|
|
192
|
+
else [anchor, f(anchor + 20_000)]
|
|
193
|
+
end
|
|
194
|
+
line = LineBreaker.new(chars, f(right - left)) { |char| @font.char_width(char, size) }.lines.first
|
|
195
|
+
text_object(size, [[aligned_x(left, line.width_left), shown_y(baseline), line.chars]])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# PdfLine#indentLeft: the width left on the line moves it right, or half of it to the center.
|
|
199
|
+
def aligned_x(left, width_left)
|
|
200
|
+
case alignment
|
|
201
|
+
when :center then f(left + f(width_left / 2))
|
|
202
|
+
when :right then f(left + width_left)
|
|
203
|
+
else left
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ColumnText places the first line one leading (2 points) below the top of its column.
|
|
208
|
+
def shown_y(baseline) = f(f(baseline + 2) - 2)
|
|
209
|
+
|
|
210
|
+
def comb_text(chars, size, baseline)
|
|
211
|
+
step = f(@width / max_length)
|
|
212
|
+
start = f(f(step / 2) + f(comb_position(chars.size) * step))
|
|
213
|
+
glyphs = chars.map do |char|
|
|
214
|
+
x = f(start - f(@font.char_width(char, size) / 2))
|
|
215
|
+
start = f(start + step)
|
|
216
|
+
[x, baseline, [char]]
|
|
217
|
+
end
|
|
218
|
+
text_object(size, glyphs)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def comb_position(shown)
|
|
222
|
+
free = max_length - [max_length, shown].min
|
|
223
|
+
case alignment
|
|
224
|
+
when :center then free / 2
|
|
225
|
+
when :right then free
|
|
226
|
+
else 0
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# iText's ColumnText in a column from 2 * offsetX to the width less 2 * offsetX, whose top is one
|
|
231
|
+
# leading above the first baseline.
|
|
232
|
+
def multiline_text(value)
|
|
233
|
+
paragraphs = value.split(/\r\n|\r|\n/, -1).map(&:chars)
|
|
234
|
+
size = @font_size.zero? ? multiline_auto_size(paragraphs) : f(@font_size)
|
|
235
|
+
leading = f(size * @font.line_factor)
|
|
236
|
+
left = f(2 * padding)
|
|
237
|
+
width = f(f(@width - f(2 * padding)) - left)
|
|
238
|
+
y = f(f(f(padding + inner_height) - @font.top(size)) + leading)
|
|
239
|
+
glyphs = lines(paragraphs, size, width).map do |line|
|
|
240
|
+
y = f(y - leading)
|
|
241
|
+
[aligned_x(left, line.width_left), y, line.chars]
|
|
242
|
+
end
|
|
243
|
+
text_object(size, glyphs)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def lines(paragraphs, size, width)
|
|
247
|
+
paragraphs.flat_map do |chars|
|
|
248
|
+
LineBreaker.new(chars, width) { |char| @font.char_width(char, size) }.lines
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def text_object(size, glyphs)
|
|
253
|
+
shown = glyphs.map { |x, y, chars| "1 0 0 1 #{n(x)} #{n(y)} Tm ".b << literal(chars) << " Tj".b }
|
|
254
|
+
"BT #{PDF::Serializer.name(@font.key)} #{n(size)} Tf ".b << shown.join(" ").b << " ET ".b
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# ColumnText#getWidth measures the text at size 1 in a 20000 point column: a text of whitespace
|
|
258
|
+
# only is an empty line, measures 20000 and gets the smallest size.
|
|
259
|
+
def auto_size(chars)
|
|
260
|
+
max_size = f(inner_height / @font.auto_size_factor)
|
|
261
|
+
line = LineBreaker.new(chars, 20_000) { |char| @font.char_width(char, 1) }.lines.first
|
|
262
|
+
text_width = f(20_000 - line.width_left)
|
|
263
|
+
return [max_size, MIN_AUTO_SIZE].max if text_width.zero?
|
|
264
|
+
|
|
265
|
+
[max_size, f(f(@width - f(4 * padding)) / text_width)].min.clamp(MIN_AUTO_SIZE..)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# iText shrinks the text from at most 12 points in tenths of the range, until ColumnText fits it in
|
|
269
|
+
# a column of the field's height.
|
|
270
|
+
def multiline_auto_size(paragraphs)
|
|
271
|
+
size = f(inner_height / @font.line_factor)
|
|
272
|
+
return MIN_AUTO_SIZE if size <= MIN_AUTO_SIZE
|
|
273
|
+
|
|
274
|
+
size = MULTILINE_MAX_AUTO_SIZE if size > MULTILINE_MAX_AUTO_SIZE
|
|
275
|
+
step = [f(f(size - MIN_AUTO_SIZE) / 10), f(0.2)].max
|
|
276
|
+
width = f(@width - f(4 * padding))
|
|
277
|
+
size = f(size - step) while size > MIN_AUTO_SIZE && !fits?(paragraphs, size, width)
|
|
278
|
+
[size, MIN_AUTO_SIZE].max
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def fits?(paragraphs, size, width)
|
|
282
|
+
leading = f(@font.line_factor * size)
|
|
283
|
+
y = 0.0
|
|
284
|
+
lines(paragraphs, size, width).all? do
|
|
285
|
+
next false if f(y - leading) < -inner_height
|
|
286
|
+
|
|
287
|
+
y = f(y - leading)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def single_line_baseline(size)
|
|
292
|
+
ascent = @font.ascent(size)
|
|
293
|
+
descent = @font.descent(size)
|
|
294
|
+
baseline = f(offset + f(f(f(@height - f(2 * offset)) - ascent) / 2))
|
|
295
|
+
baseline = offset if baseline < offset
|
|
296
|
+
return baseline unless f(baseline - offset) < -descent
|
|
297
|
+
|
|
298
|
+
[f(-descent + offset), [baseline, f(f(@height - offset) - ascent)].max].min # rubocop:disable Style/ComparableClamp
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def alignment = { 1 => :center, 2 => :right }.fetch(merged(:Q).to_i, :left)
|
|
302
|
+
|
|
303
|
+
def max_length = merged(:MaxLen).to_i
|
|
304
|
+
|
|
305
|
+
def comb? = merged(:Ff).to_i.anybits?(COMB) && max_length.positive?
|
|
306
|
+
|
|
307
|
+
def multiline? = merged(:Ff).to_i.anybits?(MULTILINE)
|
|
308
|
+
|
|
309
|
+
def literal(chars) = "(#{@font.encode_all(chars).pack("C*").gsub(/[()\\]/n) { |c| "\\#{c}" }})".b
|
|
310
|
+
|
|
311
|
+
def n(number) = Fillform.number(number)
|
|
312
|
+
|
|
313
|
+
def f(value) = Float32.f(value)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
# Background and border of a widget, read and drawn like iText's BaseField#getBorderAppearance.
|
|
5
|
+
class Border
|
|
6
|
+
STYLES = { D: :dashed, B: :beveled, I: :inset, U: :underline }.freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :width, :style
|
|
9
|
+
|
|
10
|
+
def initialize(widget, box_width, box_height, comb_cells: 0)
|
|
11
|
+
@box_width = box_width
|
|
12
|
+
@box_height = box_height
|
|
13
|
+
@comb_cells = comb_cells
|
|
14
|
+
characteristics = widget.resolve(widget[:MK])
|
|
15
|
+
characteristics = {} unless characteristics.is_a?(Hash)
|
|
16
|
+
@color = Color.from(characteristics[:BC])
|
|
17
|
+
@background = Color.from(characteristics[:BG])
|
|
18
|
+
@width = @color ? 1 : 0
|
|
19
|
+
@style = :solid
|
|
20
|
+
read_style(widget)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The width the text keeps from the box: beveled and inset borders draw a second frame.
|
|
24
|
+
def inner_width = %i[beveled inset].include?(style) ? width * 2 : width
|
|
25
|
+
|
|
26
|
+
def appearance
|
|
27
|
+
operations = []
|
|
28
|
+
operations << "#{@background.fill} 0 0 #{n(@box_width)} #{n(@box_height)} re f" if @background
|
|
29
|
+
operations.concat(border)
|
|
30
|
+
operations.empty? ? "" : "q #{operations.join(" ")} Q "
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# iText reads the legacy /Border array only when there is no border style dictionary.
|
|
36
|
+
def read_style(widget)
|
|
37
|
+
if (style = widget.resolve(widget[:BS])).is_a?(Hash)
|
|
38
|
+
@width = style[:W] if style.key?(:W)
|
|
39
|
+
@style = STYLES.fetch(style[:S], :solid)
|
|
40
|
+
elsif widget.key?(:Border) && (border = widget.resolve(widget[:Border])).is_a?(Array) && border.length >= 3
|
|
41
|
+
@width = border[2]
|
|
42
|
+
@style = :dashed if border.length >= 4
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def border
|
|
47
|
+
case style
|
|
48
|
+
when :underline then stroked? ? [stroke_setup, underline] : []
|
|
49
|
+
when :beveled then frame + ["1 g", top_frame, (@background || Color.white).darker.fill, bottom_frame]
|
|
50
|
+
when :inset then frame + ["0.5 g", top_frame, "0.75 g", bottom_frame]
|
|
51
|
+
else frame(dashed: style == :dashed) + comb_dividers
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def comb_dividers
|
|
56
|
+
return [] unless stroked? && @comb_cells > 1
|
|
57
|
+
|
|
58
|
+
step = @box_width / @comb_cells
|
|
59
|
+
bottom = n(width / 2.0)
|
|
60
|
+
top = n(@box_height - (width / 2.0))
|
|
61
|
+
lines = (1...@comb_cells).map { |cell| "#{n(step * cell)} #{bottom} m #{n(step * cell)} #{top} l" }
|
|
62
|
+
["#{lines.join(" ")} S"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def stroked? = width.positive? && @color
|
|
66
|
+
|
|
67
|
+
def underline = "0 #{n(width / 2.0)} m #{n(@box_width)} #{n(width / 2.0)} l S"
|
|
68
|
+
|
|
69
|
+
def stroke_setup = "#{@color.stroke} #{n(width)} w"
|
|
70
|
+
|
|
71
|
+
def frame(dashed: false)
|
|
72
|
+
return [] unless stroked?
|
|
73
|
+
|
|
74
|
+
inset = width / 2.0
|
|
75
|
+
["#{"[3] 0 d " if dashed}#{stroke_setup}",
|
|
76
|
+
"#{n(inset)} #{n(inset)} #{n(@box_width - width)} #{n(@box_height - width)} re S"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def top_frame
|
|
80
|
+
path([[width, width], [width, @box_height - width], [@box_width - width, @box_height - width],
|
|
81
|
+
[@box_width - (2 * width), @box_height - (2 * width)], [2 * width, @box_height - (2 * width)],
|
|
82
|
+
[2 * width, 2 * width], [width, width]])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bottom_frame
|
|
86
|
+
path([[width, width], [@box_width - width, width], [@box_width - width, @box_height - width],
|
|
87
|
+
[@box_width - (2 * width), @box_height - (2 * width)], [@box_width - (2 * width), 2 * width],
|
|
88
|
+
[2 * width, 2 * width], [width, width]])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def path(points)
|
|
92
|
+
(x, y), *rest = points
|
|
93
|
+
"#{n(x)} #{n(y)} m #{rest.map { |px, py| "#{n(px)} #{n(py)} l" }.join(" ")} f"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def n(number) = Fillform.number(number)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A color of the widget's appearance characteristics, rounded like a java.awt.Color.
|
|
100
|
+
class Color
|
|
101
|
+
def self.from(components)
|
|
102
|
+
components = Array(components).map(&:to_f)
|
|
103
|
+
new(components) unless components.empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.white = new([1, 1, 1])
|
|
107
|
+
|
|
108
|
+
def initialize(components)
|
|
109
|
+
@components = components.size == 3 ? components.map { |c| (c * 255).round / 255.0 } : components
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fill = "#{values} #{operator}"
|
|
113
|
+
|
|
114
|
+
def stroke = "#{values} #{operator.upcase}"
|
|
115
|
+
|
|
116
|
+
# java.awt.Color#darker, always in RGB
|
|
117
|
+
def darker = Color.new(rgb.map { |c| ((c * 255).round * 0.7).floor / 255.0 })
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def rgb
|
|
122
|
+
case @components.size
|
|
123
|
+
when 1 then @components * 3
|
|
124
|
+
when 4
|
|
125
|
+
cyan, magenta, yellow, black = @components
|
|
126
|
+
[cyan, magenta, yellow].map { |c| (1 - c - black).clamp(0.0, 1.0) }
|
|
127
|
+
else @components
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def values = @components.map { |c| Fillform.number(c) }.join(" ")
|
|
132
|
+
|
|
133
|
+
def operator = { 1 => "g", 3 => "rg", 4 => "k" }.fetch(@components.size)
|
|
134
|
+
end
|
|
135
|
+
end
|