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,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
# Breaks a paragraph into lines like iText's BidiLine: the paragraph loses its trailing whitespace,
|
|
5
|
+
# a line ends after the last split character before the first character that does not fit, or
|
|
6
|
+
# before that character, and only the first line keeps its leading whitespace. Each line comes with
|
|
7
|
+
# the width left on it, computed in floats like iText does, which aligns it.
|
|
8
|
+
class LineBreaker
|
|
9
|
+
NO_PRINT = /[--]/
|
|
10
|
+
CJK = /[⺀-힟豈-︰-﹏。-゚]/
|
|
11
|
+
|
|
12
|
+
Line = Struct.new(:chars, :width_left)
|
|
13
|
+
|
|
14
|
+
def self.whitespace?(char) = char <= " "
|
|
15
|
+
|
|
16
|
+
def self.blank?(char) = whitespace?(char) || NO_PRINT.match?(char)
|
|
17
|
+
|
|
18
|
+
def self.rstrip(chars)
|
|
19
|
+
last = chars.rindex { |char| !blank?(char) }
|
|
20
|
+
last ? chars[0..last] : []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(chars, width, &char_width)
|
|
24
|
+
@chars = self.class.rstrip(chars)
|
|
25
|
+
@width = Float32.f(width)
|
|
26
|
+
@char_width = char_width
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def lines
|
|
30
|
+
return [Line.new([], 0.0)] if @chars.empty?
|
|
31
|
+
|
|
32
|
+
result = []
|
|
33
|
+
position = 0
|
|
34
|
+
while position < @chars.size
|
|
35
|
+
position += 1 while position.positive? && position < @chars.size && self.class.blank?(@chars[position])
|
|
36
|
+
break if position == @chars.size
|
|
37
|
+
|
|
38
|
+
line, position = next_line(position)
|
|
39
|
+
result << line
|
|
40
|
+
end
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# The branches of BidiLine#processLine, with the width each one leaves.
|
|
47
|
+
def next_line(start)
|
|
48
|
+
fitting, last_split, left = scan(start)
|
|
49
|
+
return [Line.new(@chars[start..], left), @chars.size] if fitting == @chars.size
|
|
50
|
+
return [Line.new([@chars[start]], 0.0), start + 1] if fitting == start
|
|
51
|
+
|
|
52
|
+
last = last_visible(start, fitting - 1)
|
|
53
|
+
return [Line.new(@chars[start...fitting], left), fitting] if last < start
|
|
54
|
+
if last_split.nil? || last_split >= last
|
|
55
|
+
return [Line.new(@chars[start..last], f(left + width(last + 1, fitting - 1))), fitting]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
split_last = last_visible(start, last_split)
|
|
59
|
+
split_last = last_split if split_last < start
|
|
60
|
+
[Line.new(@chars[start..split_last], f(@width - width(start, split_last))), last_split + 1]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The index of the first character that does not fit, the last split character up to it, and the
|
|
64
|
+
# width left.
|
|
65
|
+
def scan(start)
|
|
66
|
+
left = @width
|
|
67
|
+
last_split = nil
|
|
68
|
+
index = start
|
|
69
|
+
while index < @chars.size
|
|
70
|
+
char = @chars[index]
|
|
71
|
+
unless NO_PRINT.match?(char)
|
|
72
|
+
char_width = @char_width.call(char)
|
|
73
|
+
last_split = index if split?(char)
|
|
74
|
+
break if f(left - char_width).negative?
|
|
75
|
+
|
|
76
|
+
left = f(left - char_width)
|
|
77
|
+
end
|
|
78
|
+
index += 1
|
|
79
|
+
end
|
|
80
|
+
[index, last_split, left]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# BidiLine#getWidth
|
|
84
|
+
def width(first, last)
|
|
85
|
+
(first..last).reduce(0.0) do |sum, index|
|
|
86
|
+
NO_PRINT.match?(@chars[index]) ? sum : f(sum + @char_width.call(@chars[index]))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def last_visible(start, finish)
|
|
91
|
+
finish.downto(start).find { |index| !self.class.blank?(@chars[index]) } || (start - 1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def split?(char) = char <= " " || char == "-" || CJK.match?(char)
|
|
95
|
+
|
|
96
|
+
def f(value) = Float32.f(value)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
# A widget with the attributes of its field and the field's parents, like iText's merged
|
|
5
|
+
# dictionary: the widget's own entries win.
|
|
6
|
+
class MergedWidget
|
|
7
|
+
def initialize(widget, field)
|
|
8
|
+
@widget = widget
|
|
9
|
+
@field = field
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key) = @widget.key?(key) ? @widget[key] : @field.lookup(key)
|
|
13
|
+
|
|
14
|
+
def key?(key) = @widget.key?(key) || !@field.lookup(key).nil?
|
|
15
|
+
|
|
16
|
+
def resolve(value) = @widget.resolve(value)
|
|
17
|
+
|
|
18
|
+
def rect = @widget.rect
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
module PDF
|
|
5
|
+
# A PDF file as a store of indirect objects. Objects are read when they are first used, and
|
|
6
|
+
# writing a document writes every object reachable from the trailer, as a single revision.
|
|
7
|
+
class Document
|
|
8
|
+
Entry = Struct.new(:type, :position, :index)
|
|
9
|
+
|
|
10
|
+
attr_reader :trailer, :version
|
|
11
|
+
|
|
12
|
+
def self.open(path) = new(File.binread(path))
|
|
13
|
+
|
|
14
|
+
def initialize(data = nil)
|
|
15
|
+
@objects = {}
|
|
16
|
+
@entries = {}
|
|
17
|
+
@object_streams = {}
|
|
18
|
+
if data
|
|
19
|
+
@data = data.b
|
|
20
|
+
@version = @data[/\A%PDF-(\d\.\d)/, 1] || "1.4"
|
|
21
|
+
load_cross_references
|
|
22
|
+
else
|
|
23
|
+
@version = "1.7"
|
|
24
|
+
@trailer = {}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deref(value)
|
|
29
|
+
value.is_a?(Reference) ? object(value.number) : value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def object(number)
|
|
33
|
+
return @objects[number] if @objects.key?(number)
|
|
34
|
+
|
|
35
|
+
@objects[number] = read_object(number)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add(value)
|
|
39
|
+
number = [@entries.keys.max.to_i, @objects.keys.max.to_i].max + 1
|
|
40
|
+
@objects[number] = value
|
|
41
|
+
Reference.new(number, 0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def catalog = deref(@trailer[:Root])
|
|
45
|
+
|
|
46
|
+
def pages
|
|
47
|
+
result = []
|
|
48
|
+
collect_pages(catalog[:Pages], {}, result)
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write(path)
|
|
53
|
+
File.binwrite(path, Writer.new(self, reachable).to_pdf)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Object numbers reachable from the trailer, in ascending order.
|
|
57
|
+
def reachable
|
|
58
|
+
seen = {}
|
|
59
|
+
pending = [@trailer[:Root], @trailer[:Info]].compact
|
|
60
|
+
until pending.empty?
|
|
61
|
+
value = pending.pop
|
|
62
|
+
case value
|
|
63
|
+
when Reference
|
|
64
|
+
next if seen.key?(value.number)
|
|
65
|
+
|
|
66
|
+
seen[value.number] = true
|
|
67
|
+
pending << object(value.number)
|
|
68
|
+
when Hash then pending.concat(value.values)
|
|
69
|
+
when Array then pending.concat(value)
|
|
70
|
+
when Stream then pending.concat(value.dictionary.values)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
seen.keys.select { |number| @objects.key?(number) && !@objects[number].nil? }.sort
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
INHERITED_PAGE_KEYS = %i[Resources MediaBox CropBox Rotate].freeze
|
|
79
|
+
|
|
80
|
+
# [reference, page dictionary, inherited attributes] in page order.
|
|
81
|
+
def collect_pages(reference, inherited, result, depth = 0)
|
|
82
|
+
node = deref(reference)
|
|
83
|
+
return unless node.is_a?(Hash) && depth < 64
|
|
84
|
+
|
|
85
|
+
if node[:Type] == :Pages || node.key?(:Kids)
|
|
86
|
+
inherited = inherited.merge(node.slice(*INHERITED_PAGE_KEYS))
|
|
87
|
+
Array(deref(node[:Kids])).each { |kid| collect_pages(kid, inherited, result, depth + 1) }
|
|
88
|
+
else
|
|
89
|
+
result << [reference, node, inherited]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_object(number)
|
|
94
|
+
entry = @entries[number]
|
|
95
|
+
return nil unless entry.is_a?(Entry)
|
|
96
|
+
|
|
97
|
+
entry.type == :compressed ? compressed_object(entry.position, entry.index) : object_at(number, entry.position)
|
|
98
|
+
rescue MalformedError
|
|
99
|
+
raise if @rebuilt
|
|
100
|
+
|
|
101
|
+
rebuild_cross_references
|
|
102
|
+
@objects.delete(number)
|
|
103
|
+
read_object(number)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def object_at(number, position)
|
|
107
|
+
parser = Parser.new(@data, position) { |length| deref(length) }
|
|
108
|
+
found, _generation, value = parser.parse_indirect
|
|
109
|
+
raise MalformedError, "object #{number} is not at #{position}" unless found == number
|
|
110
|
+
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def compressed_object(stream_number, index)
|
|
115
|
+
offsets, data = object_stream(stream_number)
|
|
116
|
+
return nil unless offsets && index < offsets.size
|
|
117
|
+
|
|
118
|
+
Parser.new(data, offsets[index][1]).parse_object
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def object_stream(number)
|
|
122
|
+
@object_streams[number] ||= begin
|
|
123
|
+
stream = object(number)
|
|
124
|
+
data = stream.data(self)
|
|
125
|
+
first = stream[:First].to_i
|
|
126
|
+
header = Parser.new(data)
|
|
127
|
+
pairs = Array.new(stream[:N].to_i) { [header.parse_object, first + header.parse_object] }
|
|
128
|
+
[pairs, data]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def load_cross_references
|
|
133
|
+
position = @data.rindex("startxref") or return rebuild_cross_references
|
|
134
|
+
position = @data[(position + 9)..][/\A\s*(\d+)/, 1].to_i
|
|
135
|
+
visited = {}
|
|
136
|
+
until position.nil? || visited.key?(position)
|
|
137
|
+
visited[position] = true
|
|
138
|
+
trailer = read_section(position)
|
|
139
|
+
@trailer ||= trailer
|
|
140
|
+
read_section(trailer[:XRefStm]) if trailer[:XRefStm].is_a?(Integer)
|
|
141
|
+
position = trailer[:Prev]
|
|
142
|
+
end
|
|
143
|
+
raise MalformedError, "no catalog" unless @trailer && @trailer[:Root]
|
|
144
|
+
rescue MalformedError, ArgumentError, NoMethodError, TypeError
|
|
145
|
+
rebuild_cross_references
|
|
146
|
+
ensure
|
|
147
|
+
raise Fillform::Error, "encrypted PDF files are not supported" if @trailer&.key?(:Encrypt)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def read_section(position)
|
|
151
|
+
parser = Parser.new(@data, position)
|
|
152
|
+
parser.skip_whitespace
|
|
153
|
+
parser.scanner.skip(/xref\b/) ? read_table(parser) : read_stream_section(parser)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def read_table(parser)
|
|
157
|
+
scanner = parser.scanner
|
|
158
|
+
while scanner.skip(/\s*(\d+)\s+(\d+)[ \t]*[\r\n]+/)
|
|
159
|
+
first = scanner[1].to_i
|
|
160
|
+
scanner[2].to_i.times do |i|
|
|
161
|
+
scanner.skip(/\s*(\d+)\s+(\d+)\s+([nf])/) or raise MalformedError, "broken cross-reference table"
|
|
162
|
+
register(first + i, Entry.new(:offset, scanner[1].to_i)) if scanner[3] == "n"
|
|
163
|
+
register(first + i, :free) if scanner[3] == "f"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
scanner.skip(/\s*trailer/) or raise MalformedError, "no trailer"
|
|
167
|
+
parser.parse_object
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def read_stream_section(parser)
|
|
171
|
+
_number, _generation, stream = parser.parse_indirect
|
|
172
|
+
raise MalformedError, "no cross-reference stream" unless stream.is_a?(Stream) && stream[:Type] == :XRef
|
|
173
|
+
|
|
174
|
+
widths = stream[:W]
|
|
175
|
+
index = stream[:Index] || [0, stream[:Size]]
|
|
176
|
+
data = stream.data(self).bytes
|
|
177
|
+
index.each_slice(2).with_object(data) do |(first, count), bytes|
|
|
178
|
+
count.times do |i|
|
|
179
|
+
fields = widths.map { |width| bytes.shift(width).reduce(0) { |sum, byte| (sum << 8) | byte } }
|
|
180
|
+
register_stream_entry(first + i, widths[0].zero? ? 1 : fields[0], fields[1], fields[2])
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
stream.dictionary
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def register_stream_entry(number, type, field, index)
|
|
187
|
+
case type
|
|
188
|
+
when 0 then register(number, :free)
|
|
189
|
+
when 1 then register(number, Entry.new(:offset, field))
|
|
190
|
+
when 2 then register(number, Entry.new(:compressed, field, index))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The newest revision is read first, so an object keeps its first entry.
|
|
195
|
+
def register(number, entry)
|
|
196
|
+
@entries[number] = entry unless @entries.key?(number)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Finds the objects by scanning the file, like readers do with a broken cross-reference table.
|
|
200
|
+
def rebuild_cross_references
|
|
201
|
+
@rebuilt = true
|
|
202
|
+
@entries = {}
|
|
203
|
+
@objects.clear
|
|
204
|
+
@object_streams.clear
|
|
205
|
+
@data.scan(/(?<!\d)(\d+)\s+(\d+)\s+obj\b/) do
|
|
206
|
+
@entries[::Regexp.last_match(1).to_i] = Entry.new(:offset, ::Regexp.last_match.begin(0))
|
|
207
|
+
end
|
|
208
|
+
@entries.to_a.each { |number, entry| register_compressed_objects(number, entry.position) }
|
|
209
|
+
@trailer = rebuilt_trailer or raise MalformedError, "no catalog found"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The object that comes last in the file is the newest one.
|
|
213
|
+
def register_compressed_objects(number, position)
|
|
214
|
+
stream = object(number)
|
|
215
|
+
return unless stream.is_a?(Stream) && stream[:Type] == :ObjStm
|
|
216
|
+
|
|
217
|
+
object_stream(number)[0].each_with_index do |(member, _offset), index|
|
|
218
|
+
current = @entries[member]
|
|
219
|
+
newer = current.nil? || position > file_position(current)
|
|
220
|
+
@entries[member] = Entry.new(:compressed, number, index) if newer
|
|
221
|
+
end
|
|
222
|
+
rescue MalformedError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def file_position(entry) = entry.type == :offset ? entry.position : @entries[entry.position].position
|
|
227
|
+
|
|
228
|
+
def rebuilt_trailer
|
|
229
|
+
position = @data.rindex(/trailer\s*<</)
|
|
230
|
+
trailer = position ? Parser.new(@data, @data.index("<<", position)).parse_object : {}
|
|
231
|
+
return trailer if deref(trailer[:Root]).is_a?(Hash)
|
|
232
|
+
|
|
233
|
+
catalog = @entries.keys.find { |number| object(number).is_a?(Hash) && object(number)[:Type] == :Catalog }
|
|
234
|
+
catalog && trailer.merge(Root: Reference.new(catalog, 0))
|
|
235
|
+
rescue MalformedError
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fillform
|
|
4
|
+
module PDF
|
|
5
|
+
# The stream filters a form template needs to be read: Flate with predictors, and the ASCII ones.
|
|
6
|
+
module Filters
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def decode(data, filters, parameters)
|
|
10
|
+
filters = [filters].compact unless filters.is_a?(Array)
|
|
11
|
+
parameters = [parameters] unless parameters.is_a?(Array)
|
|
12
|
+
filters.each_with_index.reduce(data) do |result, (filter, index)|
|
|
13
|
+
apply(filter, result, parameters[index].is_a?(Hash) ? parameters[index] : {})
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def apply(filter, data, parameters)
|
|
18
|
+
case filter
|
|
19
|
+
when :FlateDecode, :Fl then predict(inflate(data), parameters)
|
|
20
|
+
when :ASCIIHexDecode, :AHx then hex(data)
|
|
21
|
+
when :ASCII85Decode, :A85 then ascii85(data)
|
|
22
|
+
else raise MalformedError, "unsupported stream filter #{filter}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Tolerates truncated and trailing bytes like PDF readers do.
|
|
27
|
+
def inflate(data)
|
|
28
|
+
inflater = Zlib::Inflate.new
|
|
29
|
+
result = +""
|
|
30
|
+
begin
|
|
31
|
+
result << inflater.inflate(data)
|
|
32
|
+
rescue Zlib::BufError, Zlib::DataError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
result.b
|
|
36
|
+
ensure
|
|
37
|
+
inflater&.close
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def predict(data, parameters)
|
|
41
|
+
predictor = parameters[:Predictor].to_i
|
|
42
|
+
return data if predictor < 2
|
|
43
|
+
|
|
44
|
+
colors = (parameters[:Colors] || 1).to_i
|
|
45
|
+
bits = (parameters[:BitsPerComponent] || 8).to_i
|
|
46
|
+
columns = (parameters[:Columns] || 1).to_i
|
|
47
|
+
pixel = [(colors * bits / 8.0).ceil, 1].max
|
|
48
|
+
row = (columns * colors * bits / 8.0).ceil
|
|
49
|
+
predictor == 2 ? tiff(data, row, pixel) : png(data, row, pixel)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def png(data, row, pixel)
|
|
53
|
+
previous = Array.new(row, 0)
|
|
54
|
+
result = []
|
|
55
|
+
data.bytes.each_slice(row + 1) do |type, *line|
|
|
56
|
+
line.fill(0, line.size...row)
|
|
57
|
+
line.each_index do |i|
|
|
58
|
+
left = i >= pixel ? line[i - pixel] : 0
|
|
59
|
+
above = previous[i]
|
|
60
|
+
above_left = i >= pixel ? previous[i - pixel] : 0
|
|
61
|
+
line[i] = (line[i] + png_predictor(type, left, above, above_left)) & 0xFF
|
|
62
|
+
end
|
|
63
|
+
result.concat(line)
|
|
64
|
+
previous = line
|
|
65
|
+
end
|
|
66
|
+
result.pack("C*")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def png_predictor(type, left, above, above_left)
|
|
70
|
+
case type
|
|
71
|
+
when 1 then left
|
|
72
|
+
when 2 then above
|
|
73
|
+
when 3 then (left + above) / 2
|
|
74
|
+
when 4 then paeth(left, above, above_left)
|
|
75
|
+
else 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def paeth(left, above, above_left)
|
|
80
|
+
estimate = left + above - above_left
|
|
81
|
+
distances = [left, above, above_left].map { |value| (estimate - value).abs }
|
|
82
|
+
return left if distances[0] <= distances[1] && distances[0] <= distances[2]
|
|
83
|
+
|
|
84
|
+
distances[1] <= distances[2] ? above : above_left
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tiff(data, row, pixel)
|
|
88
|
+
data.bytes.each_slice(row).flat_map do |line|
|
|
89
|
+
line.each_index { |i| line[i] = (line[i] + line[i - pixel]) & 0xFF if i >= pixel }
|
|
90
|
+
line
|
|
91
|
+
end.pack("C*")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def hex(data)
|
|
95
|
+
digits = data.delete("^0-9A-Fa-f>").split(">").first.to_s
|
|
96
|
+
[digits.length.odd? ? "#{digits}0" : digits].pack("H*")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ascii85(data)
|
|
100
|
+
text = data.delete(" \t\r\n\f\0").delete_prefix("<~").split("~>").first.to_s.gsub("z", "!!!!!")
|
|
101
|
+
text.scan(/.{1,5}/).map do |group|
|
|
102
|
+
value = group.ljust(5, "u").bytes.reduce(0) { |sum, byte| (sum * 85) + byte - 33 }
|
|
103
|
+
[value].pack("N")[0, group.length - 1]
|
|
104
|
+
end.join.b
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Fillform
|
|
6
|
+
module PDF
|
|
7
|
+
class MalformedError < Fillform::Error; end
|
|
8
|
+
|
|
9
|
+
# Dictionaries are Hashes with Symbol keys, names are Symbols, strings are binary Strings.
|
|
10
|
+
Reference = Struct.new(:number, :generation) do
|
|
11
|
+
def to_s = "#{number} #{generation} R"
|
|
12
|
+
alias_method :inspect, :to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Stream
|
|
16
|
+
attr_accessor :dictionary, :raw
|
|
17
|
+
|
|
18
|
+
# A new stream is compressed, a stream read from a file keeps its encoded bytes.
|
|
19
|
+
def self.compressed(dictionary, data)
|
|
20
|
+
new(dictionary.merge(Filter: :FlateDecode), Zlib::Deflate.deflate(data))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(dictionary, raw)
|
|
24
|
+
@dictionary = dictionary
|
|
25
|
+
@raw = raw
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key) = @dictionary[key]
|
|
29
|
+
|
|
30
|
+
def []=(key, value)
|
|
31
|
+
@dictionary[key] = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def key?(key) = @dictionary.key?(key)
|
|
35
|
+
|
|
36
|
+
def data(document = nil)
|
|
37
|
+
Filters.decode(@raw, resolve(document, @dictionary[:Filter]), resolve(document, @dictionary[:DecodeParms]))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def resolve(document, value)
|
|
43
|
+
value = document.deref(value) if document
|
|
44
|
+
value.is_a?(Array) && document ? value.map { |item| document.deref(item) } : value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|