acrofill 0.3.0 → 0.4.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 +27 -1
- data/README.md +12 -7
- data/lib/acrofill/appearance.rb +75 -40
- data/lib/acrofill/flattener.rb +8 -1
- data/lib/acrofill/fonts.rb +52 -2
- data/lib/acrofill/form.rb +21 -2
- data/lib/acrofill/metrics.rb +48 -5
- data/lib/acrofill/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 633b173570db39e5b16714fd23e558a6832ed36f38a549802eb9dcda64967854
|
|
4
|
+
data.tar.gz: 26d94b282728f297c7c61d51ed817749d8435ecdd6dd4962a11942ac0af30fe9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5aad0c14c51e0d9e28a2c4c09b03d840b01c91ef83908ea0d31674f12ff0e4da8c33e7918600ba8ef409313a4faf0e8cfad30b325b3e23fa5c2786643fcdc97
|
|
7
|
+
data.tar.gz: 18be3697b53f7fe00d6d815a8cd7e0755ca3eef63eccc02e4f9dba181c4a14df648b05289e10ba9bf92b4e0ff62032ff9b1f42a6464a4ed3b75cd2c09b693261
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Comb fields (`/Ff` bit 25 with `/MaxLen`) lay each character out centered
|
|
15
|
+
in its own cell, `/Q` choosing which run of cells the value occupies,
|
|
16
|
+
matching pdftk cell for cell. They previously rendered as plain text.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A value is now written in the codes its font actually draws it with. A
|
|
21
|
+
`/DR` font whose `/Encoding` carries a `/Differences` array moves glyphs
|
|
22
|
+
to other codes; writing the raw bytes drew whatever glyph happened to sit
|
|
23
|
+
there — a font mapping "A" to code 90 rendered "AZ" as "ZA". Widths
|
|
24
|
+
follow the emitted code, so measurement and rendering stay in step.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Flattening no longer stamps widgets a viewer would not display: `/F`
|
|
29
|
+
Hidden (bit 2) was already dropped, and NoView (bit 6, "not on screen",
|
|
30
|
+
PDF 32000 §12.5.3) now is too. pdftk stamps both, but burning in a
|
|
31
|
+
widget the template author concealed makes hidden values permanently
|
|
32
|
+
visible, so parity loses here. Widgets that are merely non-printing are
|
|
33
|
+
still stamped: they are what the viewer shows.
|
|
34
|
+
|
|
10
35
|
## [0.3.0] - 2026-07-27
|
|
11
36
|
|
|
12
37
|
### Fixed
|
|
@@ -142,7 +167,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
142
167
|
- `PdfForms`-compatible entry points (`Acrofill.new`, `fill_form`, `fields`,
|
|
143
168
|
`field_names`).
|
|
144
169
|
|
|
145
|
-
[Unreleased]: https://github.com/stiig/acrofill/compare/v0.
|
|
170
|
+
[Unreleased]: https://github.com/stiig/acrofill/compare/v0.4.0...HEAD
|
|
171
|
+
[0.4.0]: https://github.com/stiig/acrofill/compare/v0.3.0...v0.4.0
|
|
146
172
|
[0.3.0]: https://github.com/stiig/acrofill/compare/v0.2.0...v0.3.0
|
|
147
173
|
[0.2.0]: https://github.com/stiig/acrofill/compare/v0.1.2...v0.2.0
|
|
148
174
|
[0.1.2]: https://github.com/stiig/acrofill/compare/v0.1.1...v0.1.2
|
data/README.md
CHANGED
|
@@ -91,11 +91,16 @@ your own templates with:
|
|
|
91
91
|
ruby benchmark/geometry_diff.rb path/to/form.pdf
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
where pdftk
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
Flattened output was checked the same way: on those four forms every
|
|
95
|
+
stamped fragment lands where pdftk stamps it, bar the fields covered by the
|
|
96
|
+
deliberate differences below.
|
|
97
|
+
|
|
98
|
+
Four differences are deliberate: acrofill shrinks an overlong value to fit
|
|
99
|
+
where pdftk clips it; it writes Windows-1252 for non-ASCII values where
|
|
100
|
+
pdftk emits UTF-8 bytes into a WinAnsi font (which renders as mojibake); it
|
|
101
|
+
caps auto-sized (`0 Tf`) text at 12pt where pdftk scales it to fill the box;
|
|
102
|
+
and it drops widgets flagged Hidden or NoView when flattening instead of
|
|
103
|
+
burning them into the page. See the changelog for the measured numbers.
|
|
99
104
|
|
|
100
105
|
## Performance
|
|
101
106
|
|
|
@@ -194,6 +199,7 @@ Supported:
|
|
|
194
199
|
rather than all measured as Helvetica.
|
|
195
200
|
- Checkboxes and radio groups (`/Btn`) — state selection via `/V`+`/AS`
|
|
196
201
|
using the template's own appearance states.
|
|
202
|
+
- Comb fields (`/Ff` bit 25) — one character centered per `/MaxLen` cell.
|
|
197
203
|
- Choice fields (`/Ch`) — value set and rendered like text.
|
|
198
204
|
- Flattening — every visible widget appearance is stamped into the page
|
|
199
205
|
content; widget annotations and the AcroForm dictionary are removed.
|
|
@@ -204,8 +210,7 @@ Not supported (rejected or ignored, never a hard crash):
|
|
|
204
210
|
|
|
205
211
|
- Encrypted documents (raise `Acrofill::Error`), XFA forms, digital
|
|
206
212
|
signatures, JavaScript actions.
|
|
207
|
-
- Push buttons;
|
|
208
|
-
dropped on fill.
|
|
213
|
+
- Push buttons; rich text (`/RV`) is dropped on fill.
|
|
209
214
|
- Glyphs outside Windows-1252 in generated appearances (stored values
|
|
210
215
|
keep full Unicode; unrenderable glyphs appear as `?`).
|
|
211
216
|
|
data/lib/acrofill/appearance.rb
CHANGED
|
@@ -7,6 +7,9 @@ module Acrofill
|
|
|
7
7
|
PADDING = 2.0
|
|
8
8
|
# pdftk offsets the first multiline row by 1pt from the box top.
|
|
9
9
|
TOP_OFFSET = 1.0
|
|
10
|
+
|
|
11
|
+
# The drawable area of one widget: its size and the field's /Q.
|
|
12
|
+
Box = Struct.new(:width, :height, :align)
|
|
10
13
|
# Colour-setting operators allowed in a /DA string, and their operand counts.
|
|
11
14
|
COLOR_OP_ARITY = { 'g' => 1, 'rg' => 3, 'k' => 4 }.freeze
|
|
12
15
|
|
|
@@ -18,51 +21,65 @@ module Acrofill
|
|
|
18
21
|
|
|
19
22
|
# Returns a Reference to the new appearance XObject, or nil when the
|
|
20
23
|
# widget geometry is unusable.
|
|
21
|
-
def build(field_node, widget, value, multiline: false)
|
|
24
|
+
def build(field_node, widget, value, multiline: false, comb: nil)
|
|
22
25
|
rect = normalized_rect(widget[:Rect] || @doc.inherited_value(field_node, :Rect))
|
|
23
26
|
return nil unless rect
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return nil if width <= 0 || height <= 0
|
|
28
|
+
box = Box.new(rect[2] - rect[0], rect[3] - rect[1], alignment(field_node))
|
|
29
|
+
return nil if box.width <= 0 || box.height <= 0
|
|
28
30
|
|
|
29
31
|
font_name, size, color_ops = parse_da(field_node)
|
|
30
32
|
font = @fonts.metrics(font_name)
|
|
31
|
-
|
|
33
|
+
size, body = draw(value, font, size, box, multiline: multiline, comb: comb)
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
size = size.clamp(2.0, 144.0)
|
|
37
|
-
multiline_body(value, font, size, width, height, align)
|
|
38
|
-
else
|
|
39
|
-
text = printable_text(value)
|
|
40
|
-
size = [height * 0.66, 12.0].min if size.zero?
|
|
41
|
-
size = shrink_to_fit(text, font, size, width)
|
|
42
|
-
"#{fmt(line_x(text, font, size, width, align))} #{fmt(baseline(height, font, size))} Td\n" \
|
|
43
|
-
"(#{escape_literal(text)}) Tj\n"
|
|
44
|
-
end
|
|
35
|
+
@doc.add(StreamObject.new(appearance_dict(font_name, box),
|
|
36
|
+
content(font_name, size, color_ops, body).b))
|
|
37
|
+
end
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
content << "#{color_ops}\n" unless color_ops.empty?
|
|
48
|
-
content << "/#{font_name} #{fmt(size)} Tf\n"
|
|
49
|
-
content << body
|
|
50
|
-
content << "ET\nQ\nEMC\n"
|
|
39
|
+
private
|
|
51
40
|
|
|
52
|
-
|
|
41
|
+
def fmt(num)
|
|
42
|
+
Serializer.format_number(num.to_f)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The operators drawing +value+, and the point size they were laid out
|
|
46
|
+
# at — auto-sizing and shrink-to-fit both adjust the /DA size.
|
|
47
|
+
def draw(value, font, size, box, multiline:, comb:)
|
|
48
|
+
if multiline
|
|
49
|
+
size = (size <= 0 ? 12.0 : size).clamp(2.0, 144.0)
|
|
50
|
+
[size, multiline_body(value, font, size, box)]
|
|
51
|
+
elsif comb
|
|
52
|
+
size = auto_size(size, box.height).clamp(2.0, 144.0)
|
|
53
|
+
[size, comb_body(font.encode(printable_text(value)), font, size, box, comb)]
|
|
54
|
+
else
|
|
55
|
+
text = font.encode(printable_text(value))
|
|
56
|
+
size = shrink_to_fit(text, font, auto_size(size, box.height), box.width)
|
|
57
|
+
[size, "#{fmt(line_x(text, font, size, box))} #{fmt(baseline(box.height, font, size))} Td\n" \
|
|
58
|
+
"(#{escape_literal(text)}) Tj\n"]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A /DA size of 0 means "fit the box"; acrofill caps that at 12pt.
|
|
63
|
+
def auto_size(size, height)
|
|
64
|
+
size.zero? ? [height * 0.66, 12.0].min : size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def content(font_name, size, color_ops, body)
|
|
68
|
+
stream = +"/Tx BMC\nq\nBT\n"
|
|
69
|
+
stream << "#{color_ops}\n" unless color_ops.empty?
|
|
70
|
+
stream << "/#{font_name} #{fmt(size)} Tf\n"
|
|
71
|
+
stream << body
|
|
72
|
+
stream << "ET\nQ\nEMC\n"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def appearance_dict(font_name, box)
|
|
76
|
+
{
|
|
53
77
|
Type: :XObject,
|
|
54
78
|
Subtype: :Form,
|
|
55
79
|
FormType: 1,
|
|
56
|
-
BBox: [0, 0, width, height],
|
|
80
|
+
BBox: [0, 0, box.width, box.height],
|
|
57
81
|
Resources: { Font: { font_name.to_sym => @fonts.reference(font_name) } }
|
|
58
82
|
}
|
|
59
|
-
@doc.add(StreamObject.new(dict, content.b))
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def fmt(num)
|
|
65
|
-
Serializer.format_number(num.to_f)
|
|
66
83
|
end
|
|
67
84
|
|
|
68
85
|
# Where pdftk puts a single line's baseline: the ascent box centered in
|
|
@@ -84,31 +101,49 @@ module Acrofill
|
|
|
84
101
|
align.is_a?(Integer) ? align : 0
|
|
85
102
|
end
|
|
86
103
|
|
|
87
|
-
def line_x(text, font, size,
|
|
104
|
+
def line_x(text, font, size, box)
|
|
88
105
|
text_width = Metrics.string_width(text, font.widths, size)
|
|
89
|
-
case align
|
|
90
|
-
when 1 then [(width - text_width) / 2.0, PADDING].max
|
|
91
|
-
when 2 then [width - PADDING - text_width, PADDING].max
|
|
106
|
+
case box.align
|
|
107
|
+
when 1 then [(box.width - text_width) / 2.0, PADDING].max
|
|
108
|
+
when 2 then [box.width - PADDING - text_width, PADDING].max
|
|
92
109
|
else PADDING
|
|
93
110
|
end
|
|
94
111
|
end
|
|
95
112
|
|
|
113
|
+
# A comb field divides its box into /MaxLen equal cells and centers one
|
|
114
|
+
# character in each; /Q picks the run of cells the value occupies. Each
|
|
115
|
+
# glyph is positioned absolutely, the way pdftk writes it.
|
|
116
|
+
def comb_body(text, font, size, box, cells)
|
|
117
|
+
cell = box.width / cells.to_f
|
|
118
|
+
chars = text.chars
|
|
119
|
+
first = case box.align
|
|
120
|
+
when 1 then [(cells - chars.size) / 2, 0].max
|
|
121
|
+
when 2 then [cells - chars.size, 0].max
|
|
122
|
+
else 0
|
|
123
|
+
end
|
|
124
|
+
ty = fmt(baseline(box.height, font, size))
|
|
125
|
+
chars.each_with_index.map do |char, index|
|
|
126
|
+
x = ((first + index + 0.5) * cell) - (Metrics.string_width(char, font.widths, size) / 2.0)
|
|
127
|
+
"1 0 0 1 #{fmt(x)} #{ty} Tm\n(#{escape_literal(char)}) Tj\n"
|
|
128
|
+
end.join
|
|
129
|
+
end
|
|
130
|
+
|
|
96
131
|
# Greedy word wrap, top-down, honouring explicit line breaks. Lines
|
|
97
132
|
# that would fall below the box are clipped by the BBox.
|
|
98
|
-
def multiline_body(value, font, size,
|
|
99
|
-
max_width = width - (2 * PADDING)
|
|
133
|
+
def multiline_body(value, font, size, box)
|
|
134
|
+
max_width = box.width - (2 * PADDING)
|
|
100
135
|
lines = value.to_s.split(/\r\n|[\r\n]/).flat_map do |paragraph|
|
|
101
|
-
wrap_line(printable_text(paragraph), font, size, max_width)
|
|
136
|
+
wrap_line(font.encode(printable_text(paragraph)), font, size, max_width)
|
|
102
137
|
end
|
|
103
138
|
|
|
104
139
|
# pdftk spaces rows by the font's FontBBox extent and drops the first
|
|
105
140
|
# baseline by that extent from the top of the box.
|
|
106
141
|
leading = font.line_height(size)
|
|
107
|
-
first_y = height - font.top(size) + TOP_OFFSET
|
|
142
|
+
first_y = box.height - font.top(size) + TOP_OFFSET
|
|
108
143
|
body = "#{fmt(leading)} TL\n"
|
|
109
144
|
previous_x = 0.0
|
|
110
145
|
lines.each_with_index do |line, index|
|
|
111
|
-
x = line_x(line, font, size,
|
|
146
|
+
x = line_x(line, font, size, box)
|
|
112
147
|
body << "#{fmt(x - previous_x)} #{index.zero? ? fmt(first_y) : '0'} Td\n"
|
|
113
148
|
body << "(#{escape_literal(line)}) Tj\nT*\n"
|
|
114
149
|
previous_x = x
|
data/lib/acrofill/flattener.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Acrofill
|
|
|
6
6
|
# comes from the template, so each one is validated before use.
|
|
7
7
|
class Flattener
|
|
8
8
|
HIDDEN_FLAG = 2
|
|
9
|
+
NOVIEW_FLAG = 32
|
|
10
|
+
INVISIBLE = HIDDEN_FLAG | NOVIEW_FLAG
|
|
9
11
|
|
|
10
12
|
def initialize(doc)
|
|
11
13
|
@doc = doc
|
|
@@ -61,7 +63,12 @@ module Acrofill
|
|
|
61
63
|
# form's /Matrix is applied to its BBox, and the resulting extent is
|
|
62
64
|
# mapped onto the annotation rectangle.
|
|
63
65
|
def stamp_operations(page, widget)
|
|
64
|
-
|
|
66
|
+
# Flattening produces the document as it is displayed, so a widget the
|
|
67
|
+
# viewer would not show is dropped rather than burned in: /F Hidden,
|
|
68
|
+
# and NoView, which means "not on screen" (PDF 32000 §12.5.3). pdftk
|
|
69
|
+
# stamps both, but stamping Hidden makes values the template author
|
|
70
|
+
# concealed permanently visible, so parity loses here.
|
|
71
|
+
return nil if annotation_flags(widget).anybits?(INVISIBLE)
|
|
65
72
|
|
|
66
73
|
ap_ref = normal_appearance(widget)
|
|
67
74
|
xobject = @doc.deref(ap_ref)
|
data/lib/acrofill/fonts.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Acrofill
|
|
|
15
15
|
# widths /Widths + /FirstChar, else the standard-14 table
|
|
16
16
|
# ascender /FontDescriptor /Ascent, else standard-14 AFM, else 800
|
|
17
17
|
# FontBBox /FontDescriptor /FontBBox, else standard-14 AFM, else 900/-200
|
|
18
|
+
# codes /Encoding /Differences remap the value's bytes
|
|
18
19
|
#
|
|
19
20
|
# The standard-14 fallbacks apply only to a BaseFont that literally names
|
|
20
21
|
# one of the fourteen; a face merely *resembling* one (ArialMT and friends)
|
|
@@ -29,6 +30,7 @@ module Acrofill
|
|
|
29
30
|
# not consult /MissingWidth (verified — setting it changes nothing).
|
|
30
31
|
OUT_OF_RANGE_WIDTH = 0
|
|
31
32
|
SUBSET_PREFIX = /\A[A-Z]{6}\+/
|
|
33
|
+
IDENTITY = Array.new(256) { |code| code }.freeze
|
|
32
34
|
|
|
33
35
|
def initialize(doc, acroform)
|
|
34
36
|
@doc = doc
|
|
@@ -82,8 +84,56 @@ module Acrofill
|
|
|
82
84
|
name = base_font_name(dict)
|
|
83
85
|
standard = Metrics.standard_font(name)
|
|
84
86
|
ascender, descender, top, bottom = vertical(dict, standard)
|
|
85
|
-
Metrics::Font.new(widths(dict) || Metrics.widths_for(name),
|
|
86
|
-
|
|
87
|
+
Metrics::Font.new(widths(dict) || Metrics.widths_for(name),
|
|
88
|
+
ascender, descender, top, bottom, remap(dict)).freeze
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A 256-entry code translation table when /Encoding /Differences moves
|
|
92
|
+
# glyphs to codes other than their WinAnsi ones, else nil. Without it a
|
|
93
|
+
# value is drawn with whatever glyphs happen to sit at its bytes: a font
|
|
94
|
+
# that puts "A" at code 90 would render "AZ" as "ZA".
|
|
95
|
+
def remap(dict)
|
|
96
|
+
differences = differences(dict)
|
|
97
|
+
return nil if differences.empty?
|
|
98
|
+
|
|
99
|
+
by_code = {}
|
|
100
|
+
Metrics::WIN_ANSI_GLYPHS.each_with_index do |glyph, index|
|
|
101
|
+
by_code[index + Metrics::FIRST_CODE] = glyph if glyph
|
|
102
|
+
end
|
|
103
|
+
differences.each { |code, glyph| by_code[code] = glyph }
|
|
104
|
+
|
|
105
|
+
at = {}
|
|
106
|
+
by_code.each { |code, glyph| at[glyph] ||= code }
|
|
107
|
+
table = Array.new(256) { |code| code }
|
|
108
|
+
Metrics::WIN_ANSI_GLYPHS.each_with_index do |glyph, index|
|
|
109
|
+
code = glyph && at[glyph]
|
|
110
|
+
table[index + Metrics::FIRST_CODE] = code if code
|
|
111
|
+
end
|
|
112
|
+
table == IDENTITY ? nil : table.freeze
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# /Differences is a flat array where an integer restarts the code
|
|
116
|
+
# counter and each following name takes the next code (PDF 32000
|
|
117
|
+
# §9.6.6.1).
|
|
118
|
+
def differences(dict)
|
|
119
|
+
encoding = @doc.deref(dict[:Encoding])
|
|
120
|
+
return {} unless encoding.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
entries = @doc.deref(encoding[:Differences])
|
|
123
|
+
return {} unless entries.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
code = nil
|
|
126
|
+
entries.each_with_object({}) do |raw, result|
|
|
127
|
+
item = @doc.deref(raw)
|
|
128
|
+
case item
|
|
129
|
+
when Integer then code = item
|
|
130
|
+
when Symbol
|
|
131
|
+
next unless code&.between?(0, 255)
|
|
132
|
+
|
|
133
|
+
result[code] = item # a glyph name, matching Metrics::WIN_ANSI_GLYPHS
|
|
134
|
+
code += 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
87
137
|
end
|
|
88
138
|
|
|
89
139
|
def base_font_name(dict)
|
data/lib/acrofill/form.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Acrofill
|
|
|
6
6
|
class Form
|
|
7
7
|
MULTILINE_FLAG = 1 << 12
|
|
8
8
|
PUSHBUTTON_FLAG = 1 << 16
|
|
9
|
+
COMB_FLAG = 1 << 24
|
|
9
10
|
|
|
10
11
|
Field = Struct.new(:name, :type, :value, :states, keyword_init: true)
|
|
11
12
|
|
|
@@ -81,15 +82,23 @@ module Acrofill
|
|
|
81
82
|
true
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
def build_appearance(node, widget, value, multiline, comb)
|
|
86
|
+
return nil if value.empty?
|
|
87
|
+
|
|
88
|
+
@appearance.build(node, widget, value, multiline: multiline, comb: comb)
|
|
89
|
+
end
|
|
90
|
+
|
|
84
91
|
def fill_text(group, value)
|
|
85
92
|
node = group[:node]
|
|
86
93
|
node[:V] = pdf_text_string(value)
|
|
87
94
|
node.delete(:RV)
|
|
88
95
|
node.delete(:I)
|
|
89
|
-
|
|
96
|
+
flags = field_flags(node)
|
|
97
|
+
multiline = flags.anybits?(MULTILINE_FLAG)
|
|
98
|
+
comb = comb_cells(node, flags, multiline)
|
|
90
99
|
group[:widgets].each do |widget|
|
|
91
100
|
widget.delete(:AS)
|
|
92
|
-
ap_ref =
|
|
101
|
+
ap_ref = build_appearance(node, widget, value, multiline, comb)
|
|
93
102
|
# An unusable geometry yields no appearance; dropping /AP is still
|
|
94
103
|
# required, or the widget would keep rendering the *previous* value
|
|
95
104
|
# while /V already holds the new one.
|
|
@@ -102,6 +111,16 @@ module Acrofill
|
|
|
102
111
|
true
|
|
103
112
|
end
|
|
104
113
|
|
|
114
|
+
# How many cells a comb field lays its value out in, or nil when the
|
|
115
|
+
# field is not a comb. The flag only means anything alongside /MaxLen
|
|
116
|
+
# and on a single-line field (PDF 32000 §12.7.4.3).
|
|
117
|
+
def comb_cells(node, flags, multiline)
|
|
118
|
+
return nil if multiline || !flags.anybits?(COMB_FLAG)
|
|
119
|
+
|
|
120
|
+
cells = @doc.deref(@doc.inherited_value(node, :MaxLen))
|
|
121
|
+
cells.is_a?(Integer) && cells.positive? ? cells : nil
|
|
122
|
+
end
|
|
123
|
+
|
|
105
124
|
# Checkboxes and radio groups carry per-state appearance streams, so
|
|
106
125
|
# filling only selects a state: /V on the field, /AS on each widget.
|
|
107
126
|
# The value may be a state name ("Yes"), or anything non-empty when the
|
data/lib/acrofill/metrics.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Acrofill
|
|
|
16
16
|
FIRST_CODE = 32
|
|
17
17
|
LAST_CODE = 255
|
|
18
18
|
WINDOWS_1252 = Encoding::Windows_1252
|
|
19
|
+
# Encodings whose bytes are already the codes the font is drawn with.
|
|
20
|
+
BYTE_ENCODINGS = [Encoding::Windows_1252, Encoding::BINARY].freeze
|
|
19
21
|
|
|
20
22
|
# The standard-14 text cuts, indexed by bold + 2 * italic per family.
|
|
21
23
|
CUTS = {
|
|
@@ -158,8 +160,48 @@ module Acrofill
|
|
|
158
160
|
'Courier-BoldOblique' => [629, -157, 801, -250].freeze
|
|
159
161
|
}.freeze
|
|
160
162
|
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
+
# WinAnsiEncoding glyph names, index = code - 32. Used to re-encode a
|
|
164
|
+
# value for a font whose /Encoding remaps codes: the value is turned
|
|
165
|
+
# into glyph names, then into the codes *this* font draws them at.
|
|
166
|
+
WIN_ANSI_GLYPHS = [
|
|
167
|
+
:space, :exclam, :quotedbl, :numbersign, :dollar, :percent, :ampersand, :quotesingle,
|
|
168
|
+
:parenleft, :parenright, :asterisk, :plus, :comma, :hyphen, :period, :slash, :zero,
|
|
169
|
+
:one, :two, :three, :four, :five, :six, :seven, :eight, :nine, :colon, :semicolon,
|
|
170
|
+
:less, :equal, :greater, :question, :at, :A, :B, :C, :D, :E, :F, :G, :H, :I, :J, :K,
|
|
171
|
+
:L, :M, :N, :O, :P, :Q, :R, :S, :T, :U, :V, :W, :X, :Y, :Z, :bracketleft, :backslash,
|
|
172
|
+
:bracketright, :asciicircum, :underscore, :grave, :a, :b, :c, :d, :e, :f, :g, :h, :i,
|
|
173
|
+
:j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z, :braceleft, :bar,
|
|
174
|
+
:braceright, :asciitilde, :controlDEL, :Euro, nil, :quotesinglbase, :florin,
|
|
175
|
+
:quotedblbase, :ellipsis, :dagger, :daggerdbl, :circumflex, :perthousand, :Scaron,
|
|
176
|
+
:guilsinglleft, :OE, nil, :Zcaron, nil, nil, :quoteleft, :quoteright, :quotedblleft,
|
|
177
|
+
:quotedblright, :bullet, :endash, :emdash, :tilde, :trademark, :scaron,
|
|
178
|
+
:guilsinglright, :oe, nil, :zcaron, :Ydieresis, :space, :exclamdown, :cent, :sterling,
|
|
179
|
+
:currency, :yen, :brokenbar, :section, :dieresis, :copyright, :ordfeminine,
|
|
180
|
+
:guillemotleft, :logicalnot, :hyphen, :registered, :macron, :degree, :plusminus,
|
|
181
|
+
:twosuperior, :threesuperior, :acute, :mu, :paragraph, :periodcentered, :cedilla,
|
|
182
|
+
:onesuperior, :ordmasculine, :guillemotright, :onequarter, :onehalf, :threequarters,
|
|
183
|
+
:questiondown, :Agrave, :Aacute, :Acircumflex, :Atilde, :Adieresis, :Aring, :AE,
|
|
184
|
+
:Ccedilla, :Egrave, :Eacute, :Ecircumflex, :Edieresis, :Igrave, :Iacute, :Icircumflex,
|
|
185
|
+
:Idieresis, :Eth, :Ntilde, :Ograve, :Oacute, :Ocircumflex, :Otilde, :Odieresis,
|
|
186
|
+
:multiply, :Oslash, :Ugrave, :Uacute, :Ucircumflex, :Udieresis, :Yacute, :Thorn,
|
|
187
|
+
:germandbls, :agrave, :aacute, :acircumflex, :atilde, :adieresis, :aring, :ae,
|
|
188
|
+
:ccedilla, :egrave, :eacute, :ecircumflex, :edieresis, :igrave, :iacute, :icircumflex,
|
|
189
|
+
:idieresis, :eth, :ntilde, :ograve, :oacute, :ocircumflex, :otilde, :odieresis,
|
|
190
|
+
:divide, :oslash, :ugrave, :uacute, :ucircumflex, :udieresis, :yacute, :thorn,
|
|
191
|
+
:ydieresis
|
|
192
|
+
].freeze
|
|
193
|
+
|
|
194
|
+
# Everything the appearance code needs about one resolved face. +remap+
|
|
195
|
+
# is a 256-entry code translation table, or nil when the font draws
|
|
196
|
+
# WinAnsi codes as they are.
|
|
197
|
+
Font = Struct.new(:widths, :ascender, :descender, :bbox_top, :bbox_bottom, :remap) do
|
|
198
|
+
# Windows-1252 text as the byte codes this font draws it with.
|
|
199
|
+
def encode(text)
|
|
200
|
+
return text unless remap
|
|
201
|
+
|
|
202
|
+
text.b.each_byte.map { |code| remap[code] }.pack('C*')
|
|
203
|
+
end
|
|
204
|
+
|
|
163
205
|
def ascent(size) = ascender * size / 1000.0
|
|
164
206
|
|
|
165
207
|
def descent(size) = -descender * size / 1000.0
|
|
@@ -225,10 +267,11 @@ module Acrofill
|
|
|
225
267
|
(BOLD.match?(lower) ? 1 : 0) + (ITALIC.match?(lower) ? 2 : 0)
|
|
226
268
|
end
|
|
227
269
|
|
|
228
|
-
# Appearance text is already Windows-1252
|
|
229
|
-
# that measuring and rendering
|
|
270
|
+
# Appearance text is already Windows-1252, or raw font codes after
|
|
271
|
+
# #encode; anything else is converted so that measuring and rendering
|
|
272
|
+
# agree byte for byte.
|
|
230
273
|
def self.to_win_ansi(str)
|
|
231
|
-
return str if str.encoding
|
|
274
|
+
return str if BYTE_ENCODINGS.include?(str.encoding)
|
|
232
275
|
|
|
233
276
|
str.encode(WINDOWS_1252, invalid: :replace, undef: :replace, replace: '?')
|
|
234
277
|
end
|
data/lib/acrofill/version.rb
CHANGED