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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # The changes pdf-forms and pdftk make to the values and the field names on their way into the
5
+ # form, per data format.
6
+ module DataFormat
7
+ module_function
8
+
9
+ def apply(format, value)
10
+ case format.to_s
11
+ when "XFdf" then xfdf(value)
12
+ when "FdfHex" then value
13
+ else fdf(value)
14
+ end
15
+ end
16
+
17
+ # The name pdftk looks the field up with. In FDF the names are literal strings that pdftk reads in
18
+ # PDFDocEncoding, so a field whose name is not in PDFDocEncoding is not found and not filled.
19
+ def field_name(format, name)
20
+ case format.to_s
21
+ when "XFdf" then name
22
+ when "FdfHex" then PDF::Text.decode(name.b)
23
+ else latin9_as_pdf_doc(name)
24
+ end
25
+ end
26
+
27
+ # Fdf, the default of pdf-forms: the values are written in ISO-8859-15 and read back by pdftk in
28
+ # PDFDocEncoding, a line feed is written as an escaped carriage return and a raw carriage return
29
+ # is read as a line feed.
30
+ def fdf(value)
31
+ latin9_as_pdf_doc(value).gsub(/[\r\n]/, "\r" => "\n", "\n" => "\r")
32
+ end
33
+
34
+ # XFdf: pdftk's XML parser collapses a run of the same space, tab or line feed into one and reads
35
+ # carriage returns as line feeds.
36
+ def xfdf(value)
37
+ value.gsub(/([ \t\n])\1+/, '\1').gsub(/\r\n?/, "\n")
38
+ end
39
+
40
+ def latin9_as_pdf_doc(text)
41
+ latin = text.encode(Encoding::ISO_8859_15, invalid: :replace, undef: :replace, replace: "?")
42
+ latin.b.each_byte.map { |byte| PDF::Text.pdf_doc_char(byte) || "�" }.join
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # The metadata of a form field, with the attributes and string values pdf-forms reads from
5
+ # `pdftk dump_data_fields`. See Report for how pdftk walks the field tree.
6
+ Field = Struct.new(:name, :type, :options, :flags, :justification, :value, :value_default, :name_alt,
7
+ :max_length, keyword_init: true)
8
+
9
+ class Field
10
+ # pdftk reads the bytes of a name as Latin-1.
11
+ def self.latin1(name) = name.to_s.b.force_encoding(Encoding::ISO_8859_1).encode(Encoding::UTF_8)
12
+
13
+ def to_hash = to_h
14
+ end
15
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ class Filler
5
+ def initialize(template)
6
+ @doc = PDF::Document.open(template.to_s)
7
+ @form = Form.new(@doc)
8
+ end
9
+
10
+ def fill(data, flatten: false)
11
+ return self unless @form.present?
12
+
13
+ fields = @form.fields.to_h { |field| [field.name, field] }
14
+ data.each do |name, value|
15
+ field = fields[name.to_s]
16
+ next unless field
17
+
18
+ case field.concrete_type
19
+ when :check_box, :radio_button then select_state(field, value.to_s)
20
+ when :text_field then appearance(field).fill(value.to_s)
21
+ when :combo_box then appearance(field).fill_choice(value.to_s)
22
+ when :list_box then appearance(field).fill_list(Array(value).map(&:to_s))
23
+ end
24
+ end
25
+ Flattener.new(@doc, @form).flatten if flatten
26
+ self
27
+ end
28
+
29
+ # Not validated: like pdftk, a value longer than the field's MaxLen is kept as it is.
30
+ def write(destination)
31
+ @doc.write(destination.to_s)
32
+ end
33
+
34
+ private
35
+
36
+ # Like pdftk, an unknown state leaves the button as it is, and the template's own appearance of
37
+ # the state is drawn.
38
+ def select_state(field, value)
39
+ state = value == "Off" ? :Off : on_states(field).find { |name| state_name?(name, value) }
40
+ return unless state
41
+
42
+ field[:V] = state
43
+ field.widgets.each do |widget|
44
+ appearances = widget.normal_appearances
45
+ widget[:AS] = appearances.is_a?(Hash) && appearances.key?(state) ? state : :Off
46
+ end
47
+ end
48
+
49
+ def on_states(field)
50
+ field.widgets.flat_map do |widget|
51
+ appearances = widget.normal_appearances
52
+ appearances.is_a?(Hash) ? appearances.keys : []
53
+ end.uniq - [:Off]
54
+ end
55
+
56
+ # pdftk shows and matches the bytes of a state name as Latin-1, so its users pass "Sì" for "Sì".
57
+ def state_name?(name, value)
58
+ bytes = name.to_s.b
59
+ utf8 = bytes.dup.force_encoding(Encoding::UTF_8)
60
+ (utf8.valid_encoding? && utf8 == value) || Field.latin1(name) == value
61
+ end
62
+
63
+ def appearance(field)
64
+ Appearance.new(@doc, field, form: @form) { |resources, name| font(resources, name) }
65
+ end
66
+
67
+ # iText draws with Helvetica when the font of the field is not in its resources.
68
+ def font(resources, name)
69
+ @fonts ||= {}
70
+ @fonts[[resources.object_id, name]] ||= begin
71
+ fonts = @doc.deref(resources[:Font]) if resources.is_a?(Hash)
72
+ resource = fonts[name.to_sym] if fonts.is_a?(Hash) && name
73
+ @doc.deref(resource).is_a?(Hash) ? Font.new(name.to_sym, resource, @doc) : helvetica
74
+ end
75
+ end
76
+
77
+ def helvetica
78
+ @helvetica ||= Font.new(:Helv, @doc.add({ Type: :Font, Subtype: :Type1, BaseFont: :Helvetica,
79
+ Encoding: :WinAnsiEncoding }), @doc, fallback: true)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # Draws the appearances of the fields into their pages and removes the fields, like iText's
5
+ # PdfStamperImp#flatFields: in the order of the field names, each appearance moved to the lower left
6
+ # corner of its widget, only for widgets that are printed and not hidden.
7
+ class Flattener
8
+ HIDDEN = 1 << 1
9
+ PRINT = 1 << 2
10
+
11
+ def initialize(document, form)
12
+ @document = document
13
+ @form = form
14
+ end
15
+
16
+ def flatten
17
+ pages = @document.pages
18
+ page_of = page_index(pages)
19
+ drawings = Hash.new { |hash, key| hash[key] = [] }
20
+ removed = {}
21
+ @form.fields.sort_by(&:name).each do |field|
22
+ field.widgets.each do |widget|
23
+ index = widget.reference.is_a?(PDF::Reference) ? page_of[widget.reference.number] : nil
24
+ next unless index
25
+
26
+ appearance = appearance(widget) if visible?(widget)
27
+ drawings[index] << [appearance, *widget.rect[0, 2]] if appearance
28
+ removed[widget.reference.number] = true
29
+ end
30
+ end
31
+ pages.each_with_index do |(_reference, page, inherited), index|
32
+ remove_widgets(page, removed)
33
+ draw(page, inherited, drawings[index]) if drawings.key?(index)
34
+ end
35
+ @form.dictionary[:Fields] = []
36
+ end
37
+
38
+ private
39
+
40
+ def page_index(pages)
41
+ pages.each_with_index.with_object({}) do |((_reference, page, _inherited), index), result|
42
+ Array(@document.deref(page[:Annots])).each do |annotation|
43
+ result[annotation.number] ||= index if annotation.is_a?(PDF::Reference)
44
+ end
45
+ end
46
+ end
47
+
48
+ def visible?(widget)
49
+ flags = widget[:F].to_i
50
+ flags.anybits?(PRINT) && !flags.anybits?(HIDDEN)
51
+ end
52
+
53
+ # The normal appearance, or the one of the widget's state.
54
+ def appearance(widget)
55
+ appearances = widget[:AP]
56
+ return unless appearances.is_a?(Hash)
57
+
58
+ normal = appearances[:N]
59
+ value = @document.deref(normal)
60
+ reference = case value
61
+ when PDF::Stream then normal.is_a?(PDF::Reference) ? normal : @document.add(value)
62
+ when Hash then value[widget[:AS]] if widget[:AS]
63
+ end
64
+ stream = @document.deref(reference) if reference.is_a?(PDF::Reference)
65
+ return unless stream.is_a?(PDF::Stream)
66
+
67
+ stream[:Subtype] = :Form
68
+ reference
69
+ end
70
+
71
+ def remove_widgets(page, removed)
72
+ annotations = Array(@document.deref(page[:Annots]))
73
+ kept = annotations.reject { |annotation| annotation.is_a?(PDF::Reference) && removed[annotation.number] }
74
+ return if kept.size == annotations.size
75
+
76
+ kept.empty? ? page.delete(:Annots) : page[:Annots] = kept
77
+ end
78
+
79
+ def draw(page, inherited, drawings)
80
+ xobjects = xobjects(page, inherited)
81
+ operations = drawings.map do |reference, left, bottom|
82
+ name = next_name(xobjects)
83
+ xobjects[name] = reference
84
+ "q 1 0 0 1 #{Fillform.number(left)} #{Fillform.number(bottom)} cm #{PDF::Serializer.name(name)} Do Q\n"
85
+ end
86
+ page[:Contents] = [@document.add(PDF::Stream.new({}, "q\n".b)), *contents(page),
87
+ @document.add(PDF::Stream.new({}, "\nQ\n#{operations.join}".b))]
88
+ end
89
+
90
+ def contents(page)
91
+ contents = page[:Contents]
92
+ value = @document.deref(contents)
93
+ case value
94
+ when Array then value
95
+ when nil then []
96
+ else [contents]
97
+ end
98
+ end
99
+
100
+ def xobjects(page, inherited)
101
+ page[:Resources] = @document.deref(inherited[:Resources]).dup || {} unless page.key?(:Resources)
102
+ resources = @document.deref(page[:Resources])
103
+ xobjects = @document.deref(resources[:XObject])
104
+ resources[:XObject] = xobjects = {} unless xobjects.is_a?(Hash)
105
+ xobjects
106
+ end
107
+
108
+ def next_name(xobjects)
109
+ index = 0
110
+ index += 1 while xobjects.key?(:"Xi#{index}")
111
+ :"Xi#{index}"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # iText computes the layout with Java floats: each operation is rounded to single precision.
5
+ module Float32
6
+ module_function
7
+
8
+ def f(value) = [value].pack("e").unpack1("e")
9
+
10
+ THOUSANDTH = f(0.001)
11
+ end
12
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # The metrics and the encoding iText reads from a font of the form's resources.
5
+ class Font
6
+ # iText encodes with the Java charsets of these encodings; any other encoding goes by glyph names.
7
+ CHARSETS = { WinAnsiEncoding: Encoding::Windows_1252, MacRomanEncoding: Encoding::MacRoman }.freeze
8
+
9
+ attr_reader :key, :resource
10
+
11
+ # iText's own fallback font drops the characters it cannot show, a font of the form shows code 0.
12
+ # The resource is what the appearance stream lists for the font: a reference or a dictionary.
13
+ def initialize(key, resource, document, fallback: false)
14
+ @key = key
15
+ @resource = resource
16
+ @document = document
17
+ @dictionary = document.deref(resource)
18
+ @missing = fallback ? nil : 0
19
+ end
20
+
21
+ def encode(char)
22
+ @codes ||= {}
23
+ return @codes[char] if @codes.key?(char)
24
+
25
+ @codes[char] = differences[char] || charset_code(char) || (charset ? nil : base_codes[char]) || @missing
26
+ end
27
+
28
+ def encode_all(chars) = chars.filter_map { |char| encode(char) }
29
+
30
+ # iText's BaseFont#getWidthPoint with the integer width of the glyph.
31
+ def char_width(char, size)
32
+ code = encode(char)
33
+ f(f(f(code ? width(code).to_i : 0) * Float32::THOUSANDTH) * f(size))
34
+ end
35
+
36
+ # iText's quirk: the height of a line is measured from the lower left to the upper right corner.
37
+ def auto_size_factor = f(metric(bounding_box[2], 1) - metric(bounding_box[1], 1))
38
+
39
+ def line_factor = f(metric(bounding_box[3], 1) - metric(bounding_box[1], 1))
40
+
41
+ def top(size) = metric(bounding_box[3], size)
42
+
43
+ def bottom(size) = metric(bounding_box[1], size)
44
+
45
+ def ascent(size) = metric(metrics[1], size)
46
+
47
+ def descent(size) = metric(metrics[2], size)
48
+
49
+ private
50
+
51
+ # iText's BaseFont#getFontDescriptor: value * size / 1000 in floats.
52
+ def metric(value, size) = f(f(f(value) * f(size)) / 1000)
53
+
54
+ def f(value) = Float32.f(value)
55
+
56
+ def width(code)
57
+ @widths ||= {}
58
+ @widths[code] ||=
59
+ if (widths = deref(@dictionary[:Widths]))
60
+ first = @dictionary[:FirstChar].to_i
61
+ code.between?(first, first + widths.size - 1) ? deref(widths[code - first]).to_f : missing_width
62
+ elsif standard_font?
63
+ standard_metrics.widths[names[code]] || 0
64
+ else
65
+ missing_width
66
+ end
67
+ end
68
+
69
+ def missing_width = deref(descriptor&.[](:MissingWidth)).to_f
70
+
71
+ def encoding = deref(@dictionary[:Encoding])
72
+
73
+ def base_encoding_name
74
+ encoding.is_a?(Hash) ? encoding[:BaseEncoding] : encoding
75
+ end
76
+
77
+ def charset
78
+ return @charset if defined?(@charset)
79
+
80
+ @charset = CHARSETS[base_encoding_name]
81
+ end
82
+
83
+ # Where Ruby's MacRoman table differs from Java's.
84
+ JAVA_MAC_ROMAN = { "¤" => nil, "Ω" => 0xBD, "€" => 0xDB, "Ω" => nil, "" => 0xF0 }.freeze
85
+
86
+ def charset_code(char)
87
+ return unless charset
88
+ return JAVA_MAC_ROMAN[char] if charset == Encoding::MacRoman && JAVA_MAC_ROMAN.key?(char)
89
+
90
+ char.encode(charset).ord
91
+ rescue EncodingError
92
+ nil
93
+ end
94
+
95
+ # Glyph names by code: the base encoding with the differences of the font applied.
96
+ def names
97
+ @names ||= base_names.dup.tap do |result|
98
+ difference_names.each { |code, name| result[code] = name }
99
+ end
100
+ end
101
+
102
+ def base_names
103
+ Glyphs.encoding(base_encoding_name) || builtin_names
104
+ end
105
+
106
+ def builtin_names
107
+ standard_font? ? standard_metrics.encoding : Glyphs.encoding(:StandardEncoding)
108
+ end
109
+
110
+ def difference_names
111
+ @difference_names ||= begin
112
+ code = 0
113
+ Array(deref(encoding.is_a?(Hash) ? encoding[:Differences] : nil)).each_with_object({}) do |item, result|
114
+ item = deref(item)
115
+ if item.is_a?(Numeric)
116
+ code = item.to_i
117
+ else
118
+ result[code] = item
119
+ code += 1
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def differences
126
+ @differences ||= unicode_codes(difference_names)
127
+ end
128
+
129
+ def base_codes
130
+ @base_codes ||= unicode_codes(base_names.each_with_index.to_h { |name, code| [code, name] })
131
+ end
132
+
133
+ def unicode_codes(names_by_code)
134
+ dingbats = @dictionary[:BaseFont].to_s == "ZapfDingbats"
135
+ names_by_code.each_with_object({}) do |(code, name), result|
136
+ next unless name
137
+
138
+ char = Glyphs.unicode(name, dingbats: dingbats)
139
+ result[char] = code if char && !result.key?(char)
140
+ end
141
+ end
142
+
143
+ def bounding_box = metrics[0]
144
+
145
+ def metrics
146
+ @metrics ||=
147
+ if standard_font?
148
+ [standard_metrics.bounding_box, standard_metrics.ascender, standard_metrics.descender]
149
+ else
150
+ [deref(descriptor[:FontBBox]).map { |value| deref(value).to_f }, deref(descriptor[:Ascent]).to_f,
151
+ deref(descriptor[:Descent]).to_f]
152
+ end
153
+ end
154
+
155
+ def descriptor = deref(@dictionary[:FontDescriptor])
156
+
157
+ def standard_metrics = Glyphs.metrics(@dictionary[:BaseFont])
158
+
159
+ def standard_font?
160
+ @dictionary[:Subtype] == :Type1 && Glyphs.standard_font?(@dictionary[:BaseFont])
161
+ end
162
+
163
+ def deref(value) = @document.deref(value)
164
+ end
165
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # The interactive form of a document: its terminal fields, in the order of the field tree.
5
+ class Form
6
+ attr_reader :document, :dictionary
7
+
8
+ def initialize(document)
9
+ @document = document
10
+ @dictionary = document.deref(document.catalog&.[](:AcroForm))
11
+ end
12
+
13
+ def present? = @dictionary.is_a?(Hash)
14
+
15
+ def [](key) = present? ? @document.deref(@dictionary[key]) : nil
16
+
17
+ def fields
18
+ return [] unless present?
19
+
20
+ @fields ||= Array(self[:Fields]).flat_map { |reference| terminal_fields(reference, nil, 0) }
21
+ end
22
+
23
+ def field(name) = fields.find { |field| field.name == name }
24
+
25
+ private
26
+
27
+ def terminal_fields(reference, parent, depth)
28
+ dictionary = @document.deref(reference)
29
+ return [] unless dictionary.is_a?(Hash) && depth < 32
30
+
31
+ field = FormField.new(@document, reference, dictionary, parent)
32
+ kids = field.field_kids
33
+ kids.empty? ? [field] : kids.flat_map { |kid| terminal_fields(kid, field, depth + 1) }
34
+ end
35
+ end
36
+
37
+ # A field of the form, with the attributes it inherits from its parents.
38
+ class FormField
39
+ INHERITABLE = %i[FT Ff V DV DA Q MaxLen].freeze
40
+ PUSH_BUTTON = 1 << 16
41
+ RADIO = 1 << 15
42
+ COMBO = 1 << 17
43
+
44
+ attr_reader :document, :reference, :dictionary, :parent
45
+
46
+ def initialize(document, reference, dictionary, parent)
47
+ @document = document
48
+ @reference = reference
49
+ @dictionary = dictionary
50
+ @parent = parent
51
+ end
52
+
53
+ def [](key)
54
+ value = @document.deref(@dictionary[key])
55
+ return value unless value.nil? && INHERITABLE.include?(key) && @parent
56
+
57
+ @parent[key]
58
+ end
59
+
60
+ def []=(key, value)
61
+ @dictionary[key] = value
62
+ end
63
+
64
+ def key?(key) = @dictionary.key?(key)
65
+
66
+ # A value of the field or of the nearest parent that has it.
67
+ def lookup(key)
68
+ @dictionary.key?(key) ? @document.deref(@dictionary[key]) : @parent&.lookup(key)
69
+ end
70
+
71
+ def name
72
+ own = @dictionary[:T] && PDF::Text.decode(@document.deref(@dictionary[:T]))
73
+ [@parent&.name, own].compact.join(".")
74
+ end
75
+
76
+ def field_type = self[:FT]
77
+
78
+ def flags = self[:Ff].to_i
79
+
80
+ def concrete_type
81
+ case field_type
82
+ when :Btn then button_type
83
+ when :Ch then flags.nobits?(COMBO) ? :list_box : :combo_box
84
+ when :Tx then :text_field
85
+ else field_type
86
+ end
87
+ end
88
+
89
+ # Kids with a name are fields, the other kids are widgets.
90
+ def field_kids
91
+ kids.select { |kid| @document.deref(kid).is_a?(Hash) && @document.deref(kid).key?(:T) }
92
+ end
93
+
94
+ # The field itself when its widget is merged into it.
95
+ def widgets
96
+ widget_kids = kids.reject { |kid| @document.deref(kid).is_a?(Hash) && @document.deref(kid).key?(:T) }
97
+ references = kids.empty? ? [@reference] : widget_kids
98
+ references.filter_map do |reference|
99
+ dictionary = @document.deref(reference)
100
+ Widget.new(@document, reference, dictionary) if dictionary.is_a?(Hash)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def kids = Array(@document.deref(@dictionary[:Kids]))
107
+
108
+ def button_type
109
+ return :push_button if flags.anybits?(PUSH_BUTTON)
110
+
111
+ flags.nobits?(RADIO) ? :check_box : :radio_button
112
+ end
113
+ end
114
+
115
+ # A widget annotation of a field.
116
+ class Widget
117
+ attr_reader :reference, :dictionary
118
+
119
+ def initialize(document, reference, dictionary)
120
+ @document = document
121
+ @reference = reference
122
+ @dictionary = dictionary
123
+ end
124
+
125
+ def [](key) = @document.deref(@dictionary[key])
126
+
127
+ # A value of the widget's entries, with any reference inside it resolved.
128
+ def resolve(value)
129
+ value = @document.deref(value)
130
+ case value
131
+ when Array then value.map { |item| resolve(item) }
132
+ when Hash then value.transform_values { |item| resolve(item) }
133
+ else value
134
+ end
135
+ end
136
+
137
+ def []=(key, value)
138
+ @dictionary[key] = value
139
+ end
140
+
141
+ def key?(key) = @dictionary.key?(key)
142
+
143
+ # The normalized rectangle: [left, bottom, right, top].
144
+ def rect
145
+ left, bottom, right, top = Array(self[:Rect]).map { |value| @document.deref(value).to_f }
146
+ [[left, right].min, [bottom, top].min, [left, right].max, [bottom, top].max]
147
+ end
148
+
149
+ def normal_appearances
150
+ appearances = self[:AP]
151
+ appearances.is_a?(Hash) ? @document.deref(appearances[:N]) : nil
152
+ end
153
+
154
+ # The names of the states the widget has a normal, down and rollover appearance for.
155
+ def states_by_kind
156
+ appearances = self[:AP]
157
+ return [] unless appearances.is_a?(Hash)
158
+
159
+ %i[N D R].map do |key|
160
+ states = @document.deref(appearances[key])
161
+ states.is_a?(Hash) ? states.keys : []
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fillform
4
+ # Glyph names, the metrics of the 14 standard fonts, and the simple font encodings.
5
+ # The data files are Adobe's: the core 14 AFM files and the Adobe Glyph List, see data/.
6
+ module Glyphs
7
+ DATA = File.expand_path("../../data", __dir__)
8
+ STANDARD_FONTS = %w[Courier Courier-Bold Courier-BoldOblique Courier-Oblique Helvetica Helvetica-Bold
9
+ Helvetica-BoldOblique Helvetica-Oblique Symbol Times-Bold Times-BoldItalic Times-Italic
10
+ Times-Roman ZapfDingbats].freeze
11
+
12
+ Metrics = Struct.new(:widths, :encoding, :bounding_box, :ascender, :descender)
13
+
14
+ class << self
15
+ def standard_font?(name) = STANDARD_FONTS.include?(name.to_s)
16
+
17
+ def metrics(font_name)
18
+ @metrics ||= {}
19
+ @metrics[font_name.to_s] ||= read_afm(File.join(DATA, "afm", "#{font_name}.afm"))
20
+ end
21
+
22
+ def unicode(name, dingbats: false)
23
+ name = name.to_s
24
+ (dingbats && zapf_dingbats[name]) || glyph_list[name] || algorithmic(name)
25
+ end
26
+
27
+ # Glyph names by code of the encodings a font can name.
28
+ def encoding(name)
29
+ @encodings ||= {}
30
+ @encodings[name] ||=
31
+ case name
32
+ when :StandardEncoding then metrics("Helvetica").encoding
33
+ when :WinAnsiEncoding then charset_encoding(Encoding::Windows_1252, 0xA0 => :space, 0xAD => :hyphen)
34
+ when :MacRomanEncoding then charset_encoding(Encoding::MacRoman, 0xCA => :space)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def read_afm(path)
41
+ widths = {}
42
+ encoding = Array.new(256)
43
+ values = {}
44
+ File.foreach(path, encoding: Encoding::ISO_8859_1) do |line|
45
+ if line.start_with?("C ")
46
+ code = line[/\AC (-?\d+)/, 1].to_i
47
+ name = line[/; N (\S+)/, 1].to_sym
48
+ widths[name] = line[/WX (\S+)/, 1].to_f
49
+ encoding[code] = name if code.between?(0, 255)
50
+ elsif (match = line.match(/\A(FontBBox|Ascender|Descender) (.+)/))
51
+ values[match[1]] = match[2].split.map(&:to_f)
52
+ end
53
+ end
54
+ box = values["FontBBox"]
55
+ Metrics.new(widths, encoding, box, values.dig("Ascender", 0) || box[3], values.dig("Descender", 0) || box[1])
56
+ end
57
+
58
+ def charset_encoding(charset, overrides)
59
+ names = metrics("Helvetica").widths.keys.to_h { |name| [unicode(name), name] }
60
+ Array.new(256) do |code|
61
+ overrides.fetch(code) do
62
+ names[code.chr.force_encoding(charset).encode(Encoding::UTF_8)]
63
+ rescue EncodingError
64
+ nil
65
+ end
66
+ end
67
+ end
68
+
69
+ def glyph_list
70
+ @glyph_list ||= read_list("glyphlist.txt")
71
+ end
72
+
73
+ def zapf_dingbats
74
+ @zapf_dingbats ||= read_list("zapfdingbats.txt")
75
+ end
76
+
77
+ def read_list(file)
78
+ File.foreach(File.join(DATA, "agl", file)).each_with_object({}) do |line, list|
79
+ next if line.start_with?("#")
80
+
81
+ name, codes = line.chomp.split(";")
82
+ list[name] = codes.split.map(&:hex).pack("U*") if codes
83
+ end
84
+ end
85
+
86
+ # uniXXXX and uXXXX[XX] names, and names with a suffix like a.sc.
87
+ def algorithmic(name)
88
+ base = name.split(".").first.to_s
89
+ if (hex = base[/\Auni((?:\h{4})+)\z/, 1])
90
+ hex.scan(/\h{4}/).map(&:hex).pack("U*")
91
+ elsif (hex = base[/\Au(\h{4,6})\z/, 1])
92
+ [hex.hex].pack("U")
93
+ elsif base != name
94
+ glyph_list[base]
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end