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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE +26 -0
  4. data/README.md +126 -0
  5. data/data/afm/Courier-Bold.afm +342 -0
  6. data/data/afm/Courier-BoldOblique.afm +342 -0
  7. data/data/afm/Courier-Oblique.afm +342 -0
  8. data/data/afm/Courier.afm +342 -0
  9. data/data/afm/Helvetica-Bold.afm +2827 -0
  10. data/data/afm/Helvetica-BoldOblique.afm +2827 -0
  11. data/data/afm/Helvetica-Oblique.afm +3051 -0
  12. data/data/afm/Helvetica.afm +3051 -0
  13. data/data/afm/MustRead.html +19 -0
  14. data/data/afm/Symbol.afm +213 -0
  15. data/data/afm/Times-Bold.afm +2588 -0
  16. data/data/afm/Times-BoldItalic.afm +2384 -0
  17. data/data/afm/Times-Italic.afm +2667 -0
  18. data/data/afm/Times-Roman.afm +2419 -0
  19. data/data/afm/ZapfDingbats.afm +225 -0
  20. data/data/agl/LICENSE.md +26 -0
  21. data/data/agl/glyphlist.txt +4325 -0
  22. data/data/agl/zapfdingbats.txt +245 -0
  23. data/lib/fillform/appearance.rb +315 -0
  24. data/lib/fillform/border.rb +135 -0
  25. data/lib/fillform/data_format.rb +45 -0
  26. data/lib/fillform/field.rb +15 -0
  27. data/lib/fillform/filler.rb +82 -0
  28. data/lib/fillform/flattener.rb +114 -0
  29. data/lib/fillform/float32.rb +12 -0
  30. data/lib/fillform/font.rb +165 -0
  31. data/lib/fillform/form.rb +165 -0
  32. data/lib/fillform/glyphs.rb +99 -0
  33. data/lib/fillform/line_breaker.rb +98 -0
  34. data/lib/fillform/merged_widget.rb +20 -0
  35. data/lib/fillform/pdf/document.rb +240 -0
  36. data/lib/fillform/pdf/filters.rb +108 -0
  37. data/lib/fillform/pdf/objects.rb +48 -0
  38. data/lib/fillform/pdf/parser.rb +205 -0
  39. data/lib/fillform/pdf/serializer.rb +47 -0
  40. data/lib/fillform/pdf/text.rb +47 -0
  41. data/lib/fillform/pdf/writer.rb +51 -0
  42. data/lib/fillform/pdf.rb +9 -0
  43. data/lib/fillform/pdf_forms.rb +15 -0
  44. data/lib/fillform/pdftk_wrapper.rb +81 -0
  45. data/lib/fillform/report.rb +180 -0
  46. data/lib/fillform/version.rb +5 -0
  47. data/lib/fillform.rb +65 -0
  48. 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