hexapdf 0.17.2 → 0.19.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/lib/hexapdf/cli/command.rb +7 -1
  4. data/lib/hexapdf/content/canvas.rb +2 -2
  5. data/lib/hexapdf/content/color_space.rb +19 -0
  6. data/lib/hexapdf/content/graphic_object/arc.rb +2 -2
  7. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +4 -4
  8. data/lib/hexapdf/content/graphic_object/solid_arc.rb +111 -10
  9. data/lib/hexapdf/content/graphics_state.rb +167 -25
  10. data/lib/hexapdf/dictionary.rb +1 -1
  11. data/lib/hexapdf/dictionary_fields.rb +1 -1
  12. data/lib/hexapdf/document/signatures.rb +221 -0
  13. data/lib/hexapdf/encryption/security_handler.rb +3 -1
  14. data/lib/hexapdf/layout/style.rb +2 -1
  15. data/lib/hexapdf/object.rb +18 -0
  16. data/lib/hexapdf/parser.rb +3 -0
  17. data/lib/hexapdf/serializer.rb +2 -0
  18. data/lib/hexapdf/task/optimize.rb +46 -3
  19. data/lib/hexapdf/type/acro_form/appearance_generator.rb +4 -4
  20. data/lib/hexapdf/type/acro_form/form.rb +39 -28
  21. data/lib/hexapdf/type/acro_form/variable_text_field.rb +56 -18
  22. data/lib/hexapdf/type/annotations/widget.rb +3 -15
  23. data/lib/hexapdf/type/font.rb +5 -0
  24. data/lib/hexapdf/type/font_type3.rb +20 -0
  25. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +16 -8
  28. data/test/hexapdf/content/test_color_space.rb +26 -0
  29. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  30. data/test/hexapdf/content/test_operator.rb +8 -3
  31. data/test/hexapdf/encryption/test_security_handler.rb +3 -1
  32. data/test/hexapdf/layout/test_style.rb +11 -0
  33. data/test/hexapdf/task/test_optimize.rb +26 -0
  34. data/test/hexapdf/test_dictionary.rb +1 -0
  35. data/test/hexapdf/test_dictionary_fields.rb +3 -2
  36. data/test/hexapdf/test_object.rb +28 -0
  37. data/test/hexapdf/test_parser.rb +11 -0
  38. data/test/hexapdf/test_writer.rb +2 -2
  39. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +8 -1
  40. data/test/hexapdf/type/acro_form/test_form.rb +15 -8
  41. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +18 -8
  42. data/test/hexapdf/type/test_font.rb +4 -0
  43. data/test/hexapdf/type/test_font_type3.rb +16 -1
  44. metadata +4 -2
@@ -318,8 +318,8 @@ module HexaPDF
318
318
  value[name] = document.add(obj)
319
319
  elsif !field.indirect && obj.kind_of?(HexaPDF::Object) && obj.indirect?
320
320
  yield("Field #{name} needs to be a direct object", true)
321
- document.delete(obj)
322
321
  value[name] = obj.value
322
+ document.delete(obj)
323
323
  end
324
324
  end
325
325
  end
@@ -293,7 +293,7 @@ module HexaPDF
293
293
  end
294
294
 
295
295
  # :nodoc:
296
- DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d\d)(?:'|'(\d\d)'?|\z)?)?\z/n
296
+ DATE_RE = /\AD:(\d{4})(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([Z+-])?(?:(\d\d)(?:'|'([0-5]\d)'?|\z)?)?\z/n
297
297
 
298
298
  # Checks if the given object is a string and converts into a Time object if possible.
299
299
  # Otherwise returns +nil+.
@@ -0,0 +1,221 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2021 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'openssl'
38
+
39
+ module HexaPDF
40
+ class Document
41
+
42
+ # This class provides methods for interacting with digital signatures of a PDF file.
43
+ class Signatures
44
+
45
+ # This is the default signing handler which provides the ability to sign a document with a
46
+ # provided certificate using the adb.pkcs7.detached algorithm.
47
+ class DefaultHandler
48
+
49
+ # Creates a new DefaultHandler with the given signing certificate, the associated signing
50
+ # key and an optional array of certificates that should also be present in the signature.
51
+ def initialize(certificate, key, certificate_chain = [])
52
+ @certificate = certificate
53
+ @key = key
54
+ @certificate_chain = certificate_chain
55
+ end
56
+
57
+ # Returns the name to be set on the /Filter key when using this signing handler.
58
+ def filter_name
59
+ :"Adobe.PPKLite"
60
+ end
61
+
62
+ # Returns the name to be set on the /SubFilter key when using this signing handler.
63
+ def sub_filter_name
64
+ :"adbe.pkcs7.detached"
65
+ end
66
+
67
+ # Returns the size of the signature that would be created.
68
+ def signature_size
69
+ sign("").size
70
+ end
71
+
72
+ # Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
73
+ # data.
74
+ def sign(data)
75
+ OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
76
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
77
+ end
78
+
79
+ end
80
+
81
+ include Enumerable
82
+
83
+ # Creates a new Signatures object for the given PDF document.
84
+ def initialize(document)
85
+ @document = document
86
+ end
87
+
88
+ # Adds a signature to the document and returns the corresponding signature object.
89
+ #
90
+ # This method will add a new signature to the document and write the updated document to the
91
+ # given file or IO stream. Afterwards the document can't be modified anymore and still retain
92
+ # a correct digital signature; create a new document based on the file or IO stream instead.
93
+ #
94
+ # +signature+::
95
+ # Can either be a signature object or +nil+. Providing a signature object provides for
96
+ # more control, e.g.:
97
+ #
98
+ # * Setting values for optional fields like /Reason and /Location.
99
+ # * Indirectly specifying which signature field should be used.
100
+ #
101
+ # If the +signature+ is not associated with an AcroForm signature field, a new signature
102
+ # field is created and added to the main AcroForm object, creating that if necessary.
103
+ #
104
+ # If the associated signature field doesn't have a widget, a non-visible one is created on
105
+ # the first page.
106
+ #
107
+ # +handler+::
108
+ # The signature handler that provides the necessary methods for signing, see
109
+ # DefaultHandler.
110
+ #
111
+ # +write_options+::
112
+ # These options will be passed on to the HexaPDF::Document#write command. Note that
113
+ # +incremental+ will be automatically set if signing an already existing file.
114
+ def add(file_or_io, handler, signature: nil, **write_options)
115
+ signature ||= @document.add({Type: :Sig})
116
+ signature[:Filter] = handler.filter_name
117
+ signature[:SubFilter] = handler.sub_filter_name
118
+ signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
119
+ signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
120
+
121
+ # Prepare signature field
122
+ form = @document.acro_form(create: true)
123
+ form.signature_flag(:signatures_exist)
124
+
125
+ signature_field = each.find {|value| value == signature }
126
+ unless signature_field
127
+ signature_field = form.create_signature_field(generate_field_name)
128
+ signature_field.field_value = signature
129
+ end
130
+
131
+ if signature_field.each_widget.to_a.empty?
132
+ signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
133
+ end
134
+
135
+ io = if file_or_io.kind_of?(String)
136
+ File.open(file_or_io, 'w+')
137
+ else
138
+ file_or_io
139
+ end
140
+
141
+ # Save the current state so that we can determine the correct /ByteRange value and set the
142
+ # values
143
+ section = @document.write(io, incremental: true, **write_options)
144
+ data = section.map {|oid, _gen, entry| [entry.pos.to_i, oid] }.sort
145
+ index = data.index {|_pos, oid| oid == signature.oid }
146
+ signature_offset = data[index][0]
147
+ signature_length = data[index + 1][0] - data[index][0]
148
+ io.pos = signature_offset
149
+ signature_data = io.read(signature_length)
150
+
151
+ io.rewind
152
+ file_data = io.read
153
+
154
+ # Calculate the offsets for the /ByteRange
155
+ contents_offset = signature_offset + signature_data.index('Contents(') + 8
156
+ offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
157
+ length2 = file_data.size - offset2
158
+ signature[:ByteRange] = [0, contents_offset, offset2, length2]
159
+
160
+ # Set the correct /ByteRange value
161
+ signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
162
+ length = match.size
163
+ result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
164
+ result.ljust(length)
165
+ end
166
+
167
+ # Now everything besides the /Contents value is correct, so we can read the contents for
168
+ # signing
169
+ file_data[signature_offset, signature_length] = signature_data
170
+ signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
171
+ signature[:Contents] = handler.sign(signed_contents)
172
+
173
+ # Set the correct /Contents value as hexstring
174
+ signature_data.sub!(/Contents\(0+\)/) do |match|
175
+ length = match.size
176
+ result = "Contents<#{signature[:Contents].unpack1('H*')}"
177
+ "#{result.ljust(length - 1, '0')}>"
178
+ end
179
+
180
+ io.pos = signature_offset
181
+ io.write(signature_data)
182
+
183
+ signature
184
+ ensure
185
+ io.close if io && io != file_or_io
186
+ end
187
+
188
+ # :call-seq:
189
+ # signatures.each {|signature| block } -> signatures
190
+ # signatures.each -> Enumerator
191
+ #
192
+ # Iterates over all signatures in the order they are found.
193
+ def each
194
+ return to_enum(__method__) unless block_given?
195
+
196
+ return [] unless (form = @document.acro_form)
197
+ form.each_field do |field|
198
+ yield(field.field_value) if field.field_type == :Sig && field.field_value
199
+ end
200
+ end
201
+
202
+ # Returns the number of signatures in the PDF document. May be zero if the document has no
203
+ # signatures.
204
+ def count
205
+ each.to_a.size
206
+ end
207
+
208
+ private
209
+
210
+ # Generates a field name for a signature field.
211
+ def generate_field_name
212
+ index = (@document.acro_form.each_field.
213
+ map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
214
+ max || 0) + 1
215
+ "Signature#{index}"
216
+ end
217
+
218
+ end
219
+
220
+ end
221
+ end
@@ -213,7 +213,9 @@ module HexaPDF
213
213
  end
214
214
 
215
215
  handler = handler.new(document)
216
- document.trailer[:Encrypt] = handler.set_up_decryption(dict, **options)
216
+ dict = document.trailer[:Encrypt] = handler.set_up_decryption(dict, **options)
217
+ HexaPDF::Object.make_direct(dict.value)
218
+ document.revisions.current.update(dict)
217
219
  document.revisions.each do |r|
218
220
  loader = r.loader
219
221
  r.loader = lambda do |xref_entry|
@@ -1069,7 +1069,8 @@ module HexaPDF
1069
1069
 
1070
1070
  # The font size scaled appropriately.
1071
1071
  def scaled_font_size
1072
- @scaled_font_size ||= calculated_font_size / 1000.0 * scaled_horizontal_scaling
1072
+ @scaled_font_size ||= calculated_font_size * font.pdf_object.glyph_scaling_factor *
1073
+ scaled_horizontal_scaling
1073
1074
  end
1074
1075
 
1075
1076
  # The character spacing scaled appropriately.
@@ -141,6 +141,24 @@ module HexaPDF
141
141
  end
142
142
  end
143
143
 
144
+ # Makes sure that the object itself as well as all nested values are direct objects.
145
+ #
146
+ # If an indirect object is found, it is turned into a direct object and the indirect object is
147
+ # deleted from the document.
148
+ def self.make_direct(object)
149
+ if object.kind_of?(HexaPDF::Object) && object.indirect?
150
+ object_to_delete = object
151
+ object = object.value
152
+ object_to_delete.document.delete(object_to_delete)
153
+ end
154
+ if object.kind_of?(Hash)
155
+ object.transform_values! {|val| make_direct(val) }
156
+ elsif object.kind_of?(Array)
157
+ object.map! {|val| make_direct(val) }
158
+ end
159
+ object
160
+ end
161
+
144
162
  # The wrapped HexaPDF::PDFData value.
145
163
  #
146
164
  # This attribute is not part of the public API!
@@ -404,6 +404,7 @@ module HexaPDF
404
404
  def reconstruct_revision
405
405
  return if @in_reconstruct_revision
406
406
  @in_reconstruct_revision = true
407
+ @header_offset = 0
407
408
 
408
409
  raise unless @document.config['parser.try_xref_reconstruction']
409
410
  msg = "#{$!} - trying cross-reference table reconstruction"
@@ -428,8 +429,10 @@ module HexaPDF
428
429
  elsif gen.kind_of?(Integer) && tok.kind_of?(Tokenizer::Token) && tok == 'obj'
429
430
  xref.add_in_use_entry(token, gen, pos)
430
431
  if linearized.nil?
432
+ pos = @tokenizer.pos
431
433
  obj = @tokenizer.next_object rescue nil
432
434
  linearized = obj.kind_of?(Hash) && obj.key?(:Linearized)
435
+ @tokenizer.pos = pos
433
436
  end
434
437
  @tokenizer.scan_until(/(?:\n|\r\n?)endobj\b/)
435
438
  end
@@ -35,6 +35,8 @@
35
35
  #++
36
36
 
37
37
  require 'time'
38
+ require 'hexapdf/object'
39
+ require 'hexapdf/stream'
38
40
  require 'hexapdf/tokenizer'
39
41
  require 'hexapdf/filter'
40
42
  require 'hexapdf/utils/lru_cache'
@@ -72,8 +72,19 @@ module HexaPDF
72
72
  # Compresses the content streams of all pages if set to +true+. Note that this can take a
73
73
  # *very* long time because each content stream has to be unfiltered, parsed, serialized
74
74
  # and then filtered again.
75
+ #
76
+ # prune_page_resources::
77
+ # Removes all unused XObjects from the resources dictionaries of all pages. It is
78
+ # recommended to also set the +compact+ argument because otherwise the unused XObjects won't
79
+ # be deleted from the document.
80
+ #
81
+ # This is sometimes necessary after importing pages from other PDF files that use a single
82
+ # resources dictionary for all pages.
75
83
  def self.call(doc, compact: false, object_streams: :preserve, xref_streams: :preserve,
76
- compress_pages: false)
84
+ compress_pages: false, prune_page_resources: false)
85
+ used_refs = compress_pages(doc) if compress_pages
86
+ prune_page_resources(doc, used_refs) if prune_page_resources
87
+
77
88
  if compact
78
89
  compact(doc, object_streams, xref_streams)
79
90
  elsif object_streams != :preserve
@@ -83,8 +94,6 @@ module HexaPDF
83
94
  else
84
95
  doc.each(only_current: false, &method(:delete_fields_with_defaults))
85
96
  end
86
-
87
- compress_pages(doc) if compress_pages
88
97
  end
89
98
 
90
99
  # Compacts the document by merging all revisions into one, deleting null and unused entries
@@ -214,12 +223,41 @@ module HexaPDF
214
223
 
215
224
  # Compresses the contents of all pages by parsing and then serializing again. The HexaPDF
216
225
  # serializer is already optimized for small output size so nothing else needs to be done.
226
+ #
227
+ # Returns a hash of the form key=>true where the keys are the used XObjects (for use with
228
+ # #prune_page_resources).
217
229
  def self.compress_pages(doc)
230
+ used_refs = {}
218
231
  doc.pages.each do |page|
219
232
  processor = SerializationProcessor.new
220
233
  HexaPDF::Content::Parser.parse(page.contents, processor)
221
234
  page.contents = processor.result
222
235
  page[:Contents].set_filter(:FlateDecode)
236
+ xobjects = page.resources[:XObject]
237
+ processor.used_references.each {|ref| used_refs[xobjects[ref]] = true }
238
+ end
239
+ used_refs
240
+ end
241
+
242
+ # Deletes all XObject entries from the resources dictionaries of all pages whose names do not
243
+ # match the keys in +used_refs+.
244
+ def self.prune_page_resources(doc, used_refs)
245
+ unless used_refs
246
+ used_refs = {}
247
+ doc.pages.each do |page|
248
+ xobjects = page.resources[:XObject]
249
+ HexaPDF::Content::Parser.parse(page.contents) do |op, operands|
250
+ used_refs[xobjects[operands[0]]] = true if op == :Do
251
+ end
252
+ end
253
+ end
254
+
255
+ doc.pages.each do |page|
256
+ xobjects = page.resources[:XObject]
257
+ xobjects.each do |key, obj|
258
+ next if used_refs[obj]
259
+ xobjects.delete(key)
260
+ end
223
261
  end
224
262
  end
225
263
 
@@ -228,14 +266,19 @@ module HexaPDF
228
266
 
229
267
  attr_reader :result #:nodoc:
230
268
 
269
+ # Contains all found references
270
+ attr_reader :used_references
271
+
231
272
  def initialize #:nodoc:
232
273
  @result = ''.b
233
274
  @serializer = HexaPDF::Serializer.new
275
+ @used_references = []
234
276
  end
235
277
 
236
278
  def process(op, operands) #:nodoc:
237
279
  @result << HexaPDF::Content::Operator::DEFAULT_OPERATORS[op].
238
280
  serialize(@serializer, *operands)
281
+ @used_references << operands[0] if op == :Do
239
282
  end
240
283
 
241
284
  end
@@ -227,8 +227,8 @@ module HexaPDF
227
227
  # Note: Rich text fields are currently not supported!
228
228
  def create_text_appearances
229
229
  default_resources = @document.acro_form.default_resources
230
- font, font_size = retrieve_font_information(default_resources)
231
- style = HexaPDF::Layout::Style.new(font: font)
230
+ font, font_size, font_color = retrieve_font_information(default_resources)
231
+ style = HexaPDF::Layout::Style.new(font: font, fill_color: font_color)
232
232
  border_style = @widget.border_style
233
233
  padding = [1, border_style.width].max
234
234
 
@@ -482,7 +482,7 @@ module HexaPDF
482
482
 
483
483
  # Returns the font wrapper and font size to be used for a variable text field.
484
484
  def retrieve_font_information(resources)
485
- font_name, font_size = @field.parse_default_appearance_string
485
+ font_name, font_size, font_color = @field.parse_default_appearance_string
486
486
  font_object = resources.font(font_name) rescue nil
487
487
  font = font_object&.font_wrapper
488
488
  unless font
@@ -498,7 +498,7 @@ module HexaPDF
498
498
  raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
499
499
  end
500
500
  end
501
- [font, font_size]
501
+ [font, font_size, font_color]
502
502
  end
503
503
 
504
504
  # Calculates the font size for text fields based on the font and font size of the default
@@ -157,8 +157,8 @@ module HexaPDF
157
157
  # The optional keyword arguments allow setting often used properties of the field:
158
158
  #
159
159
  # +font+::
160
- # The font that should be used for the text of the field. If +font_size+ or
161
- # +font_options+ is specified but +font+ isn't, the font Helvetica is used.
160
+ # The font that should be used for the text of the field. If +font_size+, +font_options+
161
+ # or +font_color+ is specified but +font+ isn't, the font Helvetica is used.
162
162
  #
163
163
  # If no font is set on the text field, the default font properties of the AcroForm form
164
164
  # are used. Note that field specific or form specific font properties have to be set.
@@ -169,15 +169,20 @@ module HexaPDF
169
169
  # A hash with font options like :variant that should be used.
170
170
  #
171
171
  # +font_size+::
172
- # The font size that should be used. If +font+ or +font_options+ is specified but
173
- # +font_size+ isn't, font size defaults to 0 (= auto-sizing).
172
+ # The font size that should be used. If +font+, +font_options+ or +font_color+ is
173
+ # specified but +font_size+ isn't, font size defaults to 0 (= auto-sizing).
174
+ #
175
+ # +font_color+::
176
+ # The font color that should be used. If +font+, +font_options+ or +font_size+ is
177
+ # specified but +font_color+ isn't, font color defaults to 0 (i.e. black).
174
178
  #
175
179
  # +align+::
176
180
  # The alignment of the text, either :left, :center or :right.
177
- def create_text_field(name, font: nil, font_options: nil, font_size: nil, align: nil)
181
+ def create_text_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil,
182
+ align: nil)
178
183
  create_field(name, :Tx) do |field|
179
184
  apply_variable_text_properties(field, font: font, font_options: font_options,
180
- font_size: font_size, align: align)
185
+ font_size: font_size, font_color: font_color, align: align)
181
186
  end
182
187
  end
183
188
 
@@ -189,11 +194,11 @@ module HexaPDF
189
194
  # The optional keyword arguments allow setting often used properties of the field, see
190
195
  # #create_text_field for details.
191
196
  def create_multiline_text_field(name, font: nil, font_options: nil, font_size: nil,
192
- align: nil)
197
+ font_color: nil, align: nil)
193
198
  create_field(name, :Tx) do |field|
194
199
  field.initialize_as_multiline_text_field
195
200
  apply_variable_text_properties(field, font: font, font_options: font_options,
196
- font_size: font_size, align: align)
201
+ font_size: font_size, font_color: font_color, align: align)
197
202
  end
198
203
  end
199
204
 
@@ -208,11 +213,11 @@ module HexaPDF
208
213
  # The optional keyword arguments allow setting often used properties of the field, see
209
214
  # #create_text_field for details.
210
215
  def create_comb_text_field(name, max_chars:, font: nil, font_options: nil, font_size: nil,
211
- align: nil)
216
+ font_color: nil, align: nil)
212
217
  create_field(name, :Tx) do |field|
213
218
  field.initialize_as_comb_text_field
214
219
  apply_variable_text_properties(field, font: font, font_options: font_options,
215
- font_size: font_size, align: align)
220
+ font_size: font_size, font_color: font_color, align: align)
216
221
  field[:MaxLen] = max_chars
217
222
  end
218
223
  end
@@ -224,11 +229,12 @@ module HexaPDF
224
229
  #
225
230
  # The optional keyword arguments allow setting often used properties of the field, see
226
231
  # #create_text_field for details.
227
- def create_file_select_field(name, font: nil, font_options: nil, font_size: nil, align: nil)
232
+ def create_file_select_field(name, font: nil, font_options: nil, font_size: nil,
233
+ font_color: nil, align: nil)
228
234
  create_field(name, :Tx) do |field|
229
235
  field.initialize_as_file_select_field
230
236
  apply_variable_text_properties(field, font: font, font_options: font_options,
231
- font_size: font_size, align: align)
237
+ font_size: font_size, font_color: font_color, align: align)
232
238
  end
233
239
  end
234
240
 
@@ -239,11 +245,12 @@ module HexaPDF
239
245
  #
240
246
  # The optional keyword arguments allow setting often used properties of the field, see
241
247
  # #create_text_field for details.
242
- def create_password_field(name, font: nil, font_options: nil, font_size: nil, align: nil)
248
+ def create_password_field(name, font: nil, font_options: nil, font_size: nil,
249
+ font_color: nil, align: nil)
243
250
  create_field(name, :Tx) do |field|
244
251
  field.initialize_as_password_field
245
252
  apply_variable_text_properties(field, font: font, font_options: font_options,
246
- font_size: font_size, align: align)
253
+ font_size: font_size, font_color: font_color, align: align)
247
254
  end
248
255
  end
249
256
 
@@ -280,13 +287,13 @@ module HexaPDF
280
287
  # +font+, +font_options+, +font_size+ and +align+::
281
288
  # See #create_text_field
282
289
  def create_combo_box(name, option_items: nil, editable: nil, font: nil,
283
- font_options: nil, font_size: nil, align: nil)
290
+ font_options: nil, font_size: nil, font_color: nil, align: nil)
284
291
  create_field(name, :Ch) do |field|
285
292
  field.initialize_as_combo_box
286
293
  field.option_items = option_items if option_items
287
294
  field.flag(:edit) if editable
288
295
  apply_variable_text_properties(field, font: font, font_options: font_options,
289
- font_size: font_size, align: align)
296
+ font_size: font_size, font_color: font_color, align: align)
290
297
  end
291
298
  end
292
299
 
@@ -306,13 +313,13 @@ module HexaPDF
306
313
  # +font+, +font_options+, +font_size+ and +align+::
307
314
  # See #create_text_field.
308
315
  def create_list_box(name, option_items: nil, multi_select: nil, font: nil,
309
- font_options: nil, font_size: nil, align: nil)
316
+ font_options: nil, font_size: nil, font_color: nil, align: nil)
310
317
  create_field(name, :Ch) do |field|
311
318
  field.initialize_as_list_box
312
319
  field.option_items = option_items if option_items
313
320
  field.flag(:multi_select) if multi_select
314
321
  apply_variable_text_properties(field, font: font, font_options: font_options,
315
- font_size: font_size, align: align)
322
+ font_size: font_size, font_color: font_color, align: align)
316
323
  end
317
324
  end
318
325
 
@@ -322,13 +329,16 @@ module HexaPDF
322
329
  type: :XXResources)
323
330
  end
324
331
 
325
- # Sets the global default appearance string using the provided values.
332
+ # Sets the global default appearance string using the provided values or the default values
333
+ # which provide a sane default.
326
334
  #
327
- # The default argument values are a sane default. If +font_size+ is set to 0, the font size
328
- # is calculated using the height/width of the field.
329
- def set_default_appearance_string(font: 'Helvetica', font_size: 0)
330
- name = default_resources.add_font(document.fonts.add(font).pdf_object)
331
- self[:DA] = "0 g /#{name} #{font_size} Tf"
335
+ # See VariableTextField::create_appearance_string for information on the arguments.
336
+ def set_default_appearance_string(font: 'Helvetica', font_options: {}, font_size: 0,
337
+ font_color: 0)
338
+ self[:DA] = VariableTextField.create_appearance_string(document, font: font,
339
+ font_options: font_options,
340
+ font_size: font_size,
341
+ font_color: font_color)
332
342
  end
333
343
 
334
344
  # Sets the /NeedAppearances field to +true+.
@@ -420,11 +430,12 @@ module HexaPDF
420
430
 
421
431
  # Applies the given variable field properties to the field.
422
432
  def apply_variable_text_properties(field, font: nil, font_options: nil, font_size: nil,
423
- align: nil)
424
- if font || font_options || font_size
433
+ font_color: nil, align: nil)
434
+ if font || font_options || font_size || font_color
425
435
  field.set_default_appearance_string(font: font || 'Helvetica',
426
436
  font_options: font_options || {},
427
- font_size: font_size || 0)
437
+ font_size: font_size || 0,
438
+ font_color: font_color || 0)
428
439
  end
429
440
  field.text_alignment(align) if align
430
441
  end
@@ -437,7 +448,7 @@ module HexaPDF
437
448
  yield("When the field /DA is present, the field /DR must also be present")
438
449
  return
439
450
  end
440
- font_name, _ = VariableTextField.parse_appearance_string(da)
451
+ font_name, = VariableTextField.parse_appearance_string(da)
441
452
  if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name])
442
453
  yield("The font specified in /DA is not in the /DR resource dictionary")
443
454
  end