hexapdf 0.12.1 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +130 -0
  3. data/examples/019-acro_form.rb +41 -4
  4. data/lib/hexapdf/cli/command.rb +4 -2
  5. data/lib/hexapdf/cli/image2pdf.rb +2 -1
  6. data/lib/hexapdf/cli/info.rb +51 -2
  7. data/lib/hexapdf/cli/inspect.rb +30 -8
  8. data/lib/hexapdf/cli/merge.rb +1 -1
  9. data/lib/hexapdf/cli/split.rb +74 -14
  10. data/lib/hexapdf/configuration.rb +15 -0
  11. data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
  12. data/lib/hexapdf/content/parser.rb +1 -1
  13. data/lib/hexapdf/dictionary.rb +9 -6
  14. data/lib/hexapdf/dictionary_fields.rb +1 -9
  15. data/lib/hexapdf/document.rb +41 -16
  16. data/lib/hexapdf/document/files.rb +0 -1
  17. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  18. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  19. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  20. data/lib/hexapdf/font/cmap.rb +1 -4
  21. data/lib/hexapdf/font/true_type/subsetter.rb +12 -3
  22. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  23. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  24. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  25. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  26. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  27. data/lib/hexapdf/image_loader/png.rb +3 -2
  28. data/lib/hexapdf/importer.rb +3 -2
  29. data/lib/hexapdf/layout/line.rb +1 -1
  30. data/lib/hexapdf/layout/style.rb +23 -23
  31. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  32. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  33. data/lib/hexapdf/object.rb +52 -25
  34. data/lib/hexapdf/parser.rb +96 -4
  35. data/lib/hexapdf/pdf_array.rb +12 -5
  36. data/lib/hexapdf/revisions.rb +29 -21
  37. data/lib/hexapdf/serializer.rb +34 -8
  38. data/lib/hexapdf/task/optimize.rb +6 -4
  39. data/lib/hexapdf/tokenizer.rb +4 -3
  40. data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
  41. data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
  42. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  43. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  44. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  45. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  46. data/lib/hexapdf/type/actions/uri.rb +3 -2
  47. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  48. data/lib/hexapdf/type/catalog.rb +2 -2
  49. data/lib/hexapdf/type/cid_font.rb +1 -1
  50. data/lib/hexapdf/type/file_specification.rb +1 -1
  51. data/lib/hexapdf/type/font.rb +1 -1
  52. data/lib/hexapdf/type/font_simple.rb +4 -2
  53. data/lib/hexapdf/type/font_true_type.rb +6 -2
  54. data/lib/hexapdf/type/font_type0.rb +4 -4
  55. data/lib/hexapdf/type/form.rb +15 -2
  56. data/lib/hexapdf/type/image.rb +2 -2
  57. data/lib/hexapdf/type/page.rb +37 -13
  58. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  59. data/lib/hexapdf/type/resources.rb +1 -0
  60. data/lib/hexapdf/type/trailer.rb +2 -3
  61. data/lib/hexapdf/utils/object_hash.rb +0 -1
  62. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  63. data/lib/hexapdf/version.rb +1 -1
  64. data/test/hexapdf/common_tokenizer_tests.rb +6 -1
  65. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  66. data/test/hexapdf/content/test_canvas.rb +3 -3
  67. data/test/hexapdf/content/test_color_space.rb +1 -1
  68. data/test/hexapdf/encryption/test_aes.rb +4 -4
  69. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  70. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  71. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  72. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  73. data/test/hexapdf/font/true_type/test_subsetter.rb +5 -0
  74. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  75. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  76. data/test/hexapdf/layout/test_style.rb +1 -1
  77. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  78. data/test/hexapdf/test_configuration.rb +2 -2
  79. data/test/hexapdf/test_dictionary.rb +8 -1
  80. data/test/hexapdf/test_dictionary_fields.rb +2 -2
  81. data/test/hexapdf/test_document.rb +18 -10
  82. data/test/hexapdf/test_object.rb +71 -26
  83. data/test/hexapdf/test_parser.rb +171 -53
  84. data/test/hexapdf/test_pdf_array.rb +8 -1
  85. data/test/hexapdf/test_revisions.rb +35 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
  89. data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
  90. data/test/hexapdf/type/acro_form/test_field.rb +39 -0
  91. data/test/hexapdf/type/acro_form/test_form.rb +87 -15
  92. data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
  93. data/test/hexapdf/type/test_font_simple.rb +2 -1
  94. data/test/hexapdf/type/test_font_true_type.rb +6 -0
  95. data/test/hexapdf/type/test_form.rb +26 -1
  96. data/test/hexapdf/type/test_page.rb +45 -7
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  99. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  100. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  101. data/test/test_helper.rb +2 -0
  102. metadata +6 -11
@@ -334,6 +334,20 @@ module HexaPDF
334
334
  # The value needs to be an object that responds to \#call(document, message, position) and
335
335
  # returns +true+ if an error should be raised.
336
336
  #
337
+ # parser.try_xref_reconstruction::
338
+ # A boolean specifying whether non-recoverable parsing errors should lead to reconstructing the
339
+ # main cross-reference table.
340
+ #
341
+ # The reconstructed cross-reference table might make damaged files usable but there is no way
342
+ # to ensure that the reconstructed file is equal to the undamaged original file (though
343
+ # generally it works out).
344
+ #
345
+ # There is also the possibility that reconstructing doesn't work because the algorithm has to
346
+ # assume that the PDF was written in a certain way (which is recommended by the PDF
347
+ # specification).
348
+ #
349
+ # Defaults to +true+.
350
+ #
337
351
  # sorted_tree.max_leaf_node_size::
338
352
  # The maximum number of nodes that should be in a leaf node of a node tree.
339
353
  #
@@ -412,6 +426,7 @@ module HexaPDF
412
426
  'page.default_media_box' => :A4,
413
427
  'page.default_media_orientation' => :portrait,
414
428
  'parser.on_correctable_error' => proc { false },
429
+ 'parser.try_xref_reconstruction' => true,
415
430
  'sorted_tree.max_leaf_node_size' => 64,
416
431
  'style.layers_map' => {
417
432
  link: 'HexaPDF::Layout::Style::LinkLayer',
@@ -45,7 +45,7 @@ module HexaPDF
45
45
  # all either in clockwise or counterclockwise direction and optionally inclined in respect to
46
46
  # the x-axis.
47
47
  #
48
- # See: ELL - https://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
48
+ # See: ELL - https://spaceroots.org/documents/ellipse/elliptical-arc.pdf
49
49
  class Arc
50
50
 
51
51
  include HexaPDF::Utils::MathHelpers
@@ -202,8 +202,8 @@ module HexaPDF
202
202
  p2x_prime, p2y_prime = derivative_evaluate(eta2)
203
203
 
204
204
  result << [p2x, p2y,
205
- p1: [p1x + alpha * p1x_prime, p1y + alpha * p1y_prime],
206
- p2: [p2x - alpha * p2x_prime, p2y - alpha * p2y_prime]]
205
+ {p1: [p1x + alpha * p1x_prime, p1y + alpha * p1y_prime],
206
+ p2: [p2x - alpha * p2x_prime, p2y - alpha * p2y_prime]}]
207
207
  end
208
208
 
209
209
  result
@@ -177,7 +177,7 @@ module HexaPDF
177
177
  def parse(contents, processor = nil, &block) #:yields: object, params
178
178
  raise ArgumentError, "Argument processor or block is needed" if processor.nil? && block.nil?
179
179
  if processor.nil?
180
- block.singleton_class.alias_method(:process, :call)
180
+ block.singleton_class.send(:alias_method, :process, :call)
181
181
  processor = block
182
182
  end
183
183
 
@@ -97,7 +97,7 @@ module HexaPDF
97
97
  #
98
98
  # version:: Specifies the minimum version of the PDF specification needed for this value.
99
99
  def self.define_field(name, type:, required: false, default: nil, indirect: nil,
100
- allowed_values: nil, version: '1.2')
100
+ allowed_values: nil, version: '1.0')
101
101
  @fields ||= {}
102
102
  @fields[name] = Field.new(type, required: required, default: default, indirect: indirect,
103
103
  allowed_values: allowed_values, version: version)
@@ -155,6 +155,9 @@ module HexaPDF
155
155
  # available (see ::define_field).
156
156
  #
157
157
  # * Returns the default value if one is specified and no value is available.
158
+ #
159
+ # Note: This method may throw a "can't add a new key into hash during iteration" error in
160
+ # certain cases because it potentially modifies the underlying hash!
158
161
  def [](name)
159
162
  field = self.class.field(name)
160
163
  data = if key?(name)
@@ -163,7 +166,7 @@ module HexaPDF
163
166
  value[name] = field.default
164
167
  end
165
168
  value[name] = data = document.deref(data) if data.kind_of?(HexaPDF::Reference)
166
- if data.class == HexaPDF::Object || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
169
+ if data.instance_of?(HexaPDF::Object) || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
167
170
  data = data.value
168
171
  end
169
172
  if (result = field&.convert(data, document))
@@ -182,7 +185,7 @@ module HexaPDF
182
185
  raise ArgumentError, "Only Symbol (Name) keys are allowed to be used in PDF dictionaries"
183
186
  end
184
187
 
185
- if value[name].class == HexaPDF::Object && !data.kind_of?(HexaPDF::Object) &&
188
+ if value[name].instance_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Object) &&
186
189
  !data.kind_of?(HexaPDF::Reference)
187
190
  value[name].value = data
188
191
  else
@@ -255,7 +258,7 @@ module HexaPDF
255
258
 
256
259
  # Iterates over all currently set fields and those that are required.
257
260
  def each_set_key_or_required_field #:yields: name, field
258
- value.each_key {|name| yield(name, self.class.field(name)) }
261
+ value.keys.each {|name| yield(name, self.class.field(name)) }
259
262
  self.class.each_field do |name, field|
260
263
  yield(name, field) if field.required? && !value.key?(name)
261
264
  end
@@ -273,7 +276,7 @@ module HexaPDF
273
276
  # Check that required fields are set
274
277
  if field.required? && obj.nil?
275
278
  yield("Required field #{name} is not set", field.default?)
276
- self[name] = obj = field.default
279
+ self[name] = obj = field.default if field.default?
277
280
  end
278
281
 
279
282
  # Check if the document version is set high enough
@@ -301,7 +304,7 @@ module HexaPDF
301
304
 
302
305
  # Check the value of the field against the allowed values.
303
306
  if field.allowed_values && !field.allowed_values.include?(obj)
304
- yield("Field #{name} does not contain an allowed value")
307
+ yield("Field #{name} does not contain an allowed value: #{obj.inspect}")
305
308
  end
306
309
 
307
310
  # Check if field value needs to be (in)direct
@@ -151,17 +151,9 @@ module HexaPDF
151
151
  # Returns a duplicated default value, automatically taking unduplicatable classes into
152
152
  # account.
153
153
  def default
154
- duplicatable_default? ? @default.dup : @default
154
+ @default.dup
155
155
  end
156
156
 
157
- # Returns +true+ if the default value can safely be duplicated with #dup.
158
- def duplicatable_default?
159
- @duplicatable_default ||= HexaPDF::Object::NOT_DUPLICATABLE_CLASSES.none? do |klass|
160
- @default.kind_of?(klass)
161
- end
162
- end
163
- private :duplicatable_default?
164
-
165
157
  # Returns +true+ if the given object is valid for this field.
166
158
  def valid_object?(obj)
167
159
  type.any? {|t| obj.kind_of?(t) } ||
@@ -69,15 +69,35 @@ module HexaPDF
69
69
 
70
70
  autoload(:Composer, 'hexapdf/composer')
71
71
 
72
+ # == HexaPDF::Document
73
+ #
72
74
  # Represents one PDF document.
73
75
  #
74
76
  # A PDF document consists of (indirect) objects, so the main job of this class is to provide
75
77
  # methods for working with these objects. However, since a PDF document may also be
76
78
  # incrementally updated and can therefore contain one or more revisions, there are also methods
77
- # to work with these revisions.
79
+ # for working with these revisions.
78
80
  #
79
81
  # Note: This class provides everything to work on PDF documents on a low-level basis. This means
80
- # that there are no convenience methods for higher PDF functionality whatsoever.
82
+ # that there are no convenience methods for higher PDF functionality. Those can be found in the
83
+ # objects linked from here, like #catalog.
84
+ #
85
+ # == Known Messages
86
+ #
87
+ # The document object provides a basic message dispatch system via #register_listener and
88
+ # #dispatch_message.
89
+ #
90
+ # Following are the messages that are used by HexaPDF itself:
91
+ #
92
+ # :complete_objects::
93
+ # This message is called before the first step of writing a document. Listeners should
94
+ # complete PDF objects that are missing some information.
95
+ #
96
+ # For example, the font system uses this message to complete the font objects with
97
+ # information that is only available once all the used glyphs are known.
98
+ #
99
+ # :before_write::
100
+ # This message is called before a document is actually serialized and written.
81
101
  class Document
82
102
 
83
103
  autoload(:Pages, 'hexapdf/document/pages')
@@ -400,11 +420,11 @@ module HexaPDF
400
420
  # object in the PDF document. The block may either accept only the object or the object and the
401
421
  # revision it is in.
402
422
  #
403
- # By default, only the current version of each object is returned which implies that each
404
- # object number is yielded exactly once. If the +current+ option is +false+, all stored
405
- # objects from newest to oldest are returned, not only the current version of each object.
423
+ # By default, only the current version of each object is returned which implies that each object
424
+ # number is yielded exactly once. If the +only_current+ option is +false+, all stored objects
425
+ # from newest to oldest are returned, not only the current version of each object.
406
426
  #
407
- # The +current+ option can make a difference because the document can contain multiple
427
+ # The +only_current+ option can make a difference because the document can contain multiple
408
428
  # revisions:
409
429
  #
410
430
  # * Multiple revisions may contain objects with the same object and generation numbers, e.g.
@@ -442,19 +462,28 @@ module HexaPDF
442
462
  end
443
463
 
444
464
  # Dispatches the message +name+ with the given arguments to all registered listeners.
465
+ #
466
+ # See the main Document documentation for an overview of messages that are used by HexaPDF
467
+ # itself.
445
468
  def dispatch_message(name, *args)
446
469
  @listeners[name]&.each {|obj| obj.call(*args) }
447
470
  end
448
471
 
449
- # Caches the value or the return value of the given block using the given Object::PDFData and
450
- # key arguments as composite hash key. If a cached value already exists, it is just returned.
472
+ UNSET = ::Object.new # :nordoc:
473
+
474
+ # Caches and returns the given +value+ or the value of the given block using the given
475
+ # +pdf_data+ and +key+ arguments as composite cache key. If a cached value already exists and
476
+ # +update+ is +false+, the cached value is just returned.
477
+ #
478
+ # Set +update+ to +true+ to force an update of the cached value.
451
479
  #
452
480
  # This facility can be used to cache expensive operations in PDF objects that are easy to
453
481
  # compute again.
454
482
  #
455
483
  # Use #clear_cache to clear the cache if necessary.
456
- def cache(pdf_data, key, value = nil)
457
- @cache[pdf_data][key] ||= value || yield
484
+ def cache(pdf_data, key, value = UNSET, update: false)
485
+ return @cache[pdf_data][key] if cached?(pdf_data, key) && !update
486
+ @cache[pdf_data][key] = (value == UNSET ? yield : value)
458
487
  end
459
488
 
460
489
  # Returns +true+ if there is a value cached for the composite key consisting of the given
@@ -594,13 +623,9 @@ module HexaPDF
594
623
  # If a block is given, it is called on validation problems.
595
624
  #
596
625
  # See HexaPDF::Object#validate for more information.
597
- def validate(auto_correct: true, only_loaded: false) #:yield: object, msg, correctable
598
- cur_obj = trailer
599
- block = (block_given? ? lambda {|msg, correctable| yield(cur_obj, msg, correctable) } : nil)
600
-
626
+ def validate(auto_correct: true, only_loaded: false, &block) #:yield: msg, correctable, object
601
627
  result = trailer.validate(auto_correct: auto_correct, &block)
602
628
  each(only_current: false, only_loaded: only_loaded) do |obj|
603
- cur_obj = obj
604
629
  result &&= obj.validate(auto_correct: auto_correct, &block)
605
630
  end
606
631
  result
@@ -643,7 +668,7 @@ module HexaPDF
643
668
  end
644
669
 
645
670
  if validate
646
- self.validate(auto_correct: true) do |obj, msg, correctable|
671
+ self.validate(auto_correct: true) do |msg, correctable, obj|
647
672
  next if correctable
648
673
  raise HexaPDF::Error, "Validation error for (#{obj.oid},#{obj.gen}): #{msg}"
649
674
  end
@@ -117,7 +117,6 @@ module HexaPDF
117
117
 
118
118
  @document.pages.each do |page|
119
119
  page[:Annots]&.each do |annot|
120
- annot = @document.deref(annot)
121
120
  next unless annot[:Subtype] == :FileAttachment
122
121
  spec = @document.deref(annot[:FS])
123
122
  yield(spec) unless seen.key?(spec)
@@ -49,7 +49,7 @@ module HexaPDF
49
49
 
50
50
  # Creates a new FastARC4 object using the given encryption key.
51
51
  def initialize(key)
52
- @cipher = OpenSSL::Cipher::RC4.new
52
+ @cipher = OpenSSL::Cipher.new('rc4')
53
53
  @cipher.key_len = key.length
54
54
  @cipher.key = key
55
55
  end
@@ -72,6 +72,7 @@ module HexaPDF
72
72
  super
73
73
  unless [1, 2, 4, 5].include?(value[:V])
74
74
  yield("Value of /V is not one of 1, 2, 4 or 5", false)
75
+ return
75
76
  end
76
77
  if value[:V] == 2 && (!key?(:Length) || value[:Length] < 40 ||
77
78
  value[:Length] > 128 || value[:Length] % 8 != 0)
@@ -69,6 +69,7 @@ module HexaPDF
69
69
  when 6
70
70
  if !key?(:OE) || !key?(:UE) || !key?(:Perms)
71
71
  yield("Value of /OE, /UE or /Perms is missing for dictionary revision 6", false)
72
+ return
72
73
  end
73
74
  if value[:U].length != 48 || value[:O].length != 48 || value[:UE].length != 32 ||
74
75
  value[:OE].length != 32 || value[:Perms].length != 16
@@ -100,10 +100,7 @@ module HexaPDF
100
100
  # The writing mode of the CMap: 0 for horizontal, 1 for vertical writing.
101
101
  attr_accessor :wmode
102
102
 
103
- attr_reader :codespace_ranges #: nodoc:
104
- attr_reader :cid_mapping # :nodoc:
105
- attr_reader :cid_range_mappings # :nodoc:
106
- attr_reader :unicode_mapping # :nodoc:
103
+ attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping # :nodoc:
107
104
  protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping
108
105
 
109
106
  # Creates a new CMap object.
@@ -63,6 +63,12 @@ module HexaPDF
63
63
  def use_glyph(glyph_id)
64
64
  return @glyph_map[glyph_id] if @glyph_map.key?(glyph_id)
65
65
  @last_id += 1
66
+ # Handle codes for ASCII characters \r, (, ) and \ specially so that they never appear in
67
+ # the output (PDF serialization would need to escape them)
68
+ if @last_id == 13 || @last_id == 40 || @last_id == 41 || @last_id == 92
69
+ @glyph_map[:"s#{@last_id}"] = @last_id
70
+ @last_id += 1
71
+ end
66
72
  @glyph_map[glyph_id] = @last_id
67
73
  end
68
74
 
@@ -107,7 +113,7 @@ module HexaPDF
107
113
  locations = []
108
114
 
109
115
  @glyph_map.each_key do |old_gid|
110
- glyph = orig_glyf[old_gid]
116
+ glyph = orig_glyf[old_gid.kind_of?(Symbol) ? 0 : old_gid]
111
117
  locations << table.size
112
118
  data = glyph.raw_data
113
119
  if glyph.compound?
@@ -134,7 +140,7 @@ module HexaPDF
134
140
  hmtx = @font[:hmtx]
135
141
  data = ''.b
136
142
  @glyph_map.each_key do |old_gid|
137
- metric = hmtx[old_gid]
143
+ metric = hmtx[old_gid.kind_of?(Symbol) ? 0 : old_gid]
138
144
  data << [metric.advance_width, metric.left_side_bearing].pack('n2')
139
145
  end
140
146
  data
@@ -166,7 +172,10 @@ module HexaPDF
166
172
  # Adds the components of compound glyphs to the subset.
167
173
  def add_glyph_components
168
174
  glyf = @font[:glyf]
169
- @glyph_map.keys.each {|gid| glyf[gid].components&.each {|cgid| use_glyph(cgid) } }
175
+ @glyph_map.keys.each do |gid|
176
+ next if gid.kind_of?(Symbol)
177
+ glyf[gid].components&.each {|cgid| use_glyph(cgid) }
178
+ end
170
179
  end
171
180
 
172
181
  end
@@ -76,6 +76,7 @@ module HexaPDF
76
76
 
77
77
  # Apple Mac style information.
78
78
  attr_accessor :mac_style
79
+
79
80
  bit_field(:mac_style, {bold: 0, italic: 1, underline: 2, outline: 3, shadow: 4,
80
81
  condensed: 5, extended: 6})
81
82
 
@@ -65,6 +65,7 @@ module HexaPDF
65
65
 
66
66
  # Characteristics and properties of this font.
67
67
  attr_accessor :type
68
+
68
69
  bit_field(:type, {restricted_license_embedding: 1, preview_and_print_embedding: 2,
69
70
  editable_embedding: 3, no_subsetting: 8, bitmap_embedding_only: 9})
70
71
 
@@ -112,6 +113,7 @@ module HexaPDF
112
113
 
113
114
  # Information concerning the nature of the font patterns.
114
115
  attr_accessor :selection
116
+
115
117
  bit_field(:selection, {italic: 0, underscore: 1, negative: 2, outlined: 3, strikeout: 4,
116
118
  bold: 5, regular: 6, use_typo_metrics: 7, wws: 8, oblique: 9})
117
119
 
@@ -99,18 +99,23 @@ module HexaPDF
99
99
  @max_mem_type42, @min_mem_type1, @max_mem_type1 = read_formatted(24, 's>2N5')
100
100
 
101
101
  sub_table_length = directory_entry.length - 32
102
- @glyph_names = case @format
103
- when 1 then Format1.parse(io, sub_table_length)
104
- when 2 then Format2.parse(io, sub_table_length)
105
- when 3 then Format3.parse(io, sub_table_length)
106
- when 4 then Format4.parse(io, sub_table_length)
107
- else
108
- if font.config['font.true_type.unknown_format'] == :raise
109
- raise HexaPDF::Error, "Unsupported post table format: #{@format}"
102
+ cur_pos = io.pos
103
+ @glyph_names = lambda do |glyph_id|
104
+ io.pos = cur_pos
105
+ @glyph_names = case @format
106
+ when 1 then Format1.parse(io, sub_table_length)
107
+ when 2 then Format2.parse(io, sub_table_length)
108
+ when 3 then Format3.parse(io, sub_table_length)
109
+ when 4 then Format4.parse(io, sub_table_length)
110
110
  else
111
- []
111
+ if font.config['font.true_type.unknown_format'] == :raise
112
+ raise HexaPDF::Error, "Unsupported post table format: #{@format}"
113
+ else
114
+ []
115
+ end
112
116
  end
113
- end
117
+ @glyph_names[glyph_id]
118
+ end
114
119
  end
115
120
 
116
121
  # 'post' table format 1
@@ -63,8 +63,8 @@ module HexaPDF
63
63
  file = document.config['font.map'].dig(name, variant)
64
64
  return nil if file.nil?
65
65
 
66
- unless File.file?(file)
67
- raise HexaPDF::Error, "The configured font file #{file} does not exist"
66
+ unless file.kind_of?(HexaPDF::Font::TrueType::Font) || File.file?(file)
67
+ raise HexaPDF::Error, "The configured font file #{file} is not a valid value"
68
68
  end
69
69
  FromFile.call(document, file, subset: subset)
70
70
  end
@@ -39,26 +39,36 @@ require 'hexapdf/font/true_type_wrapper'
39
39
  module HexaPDF
40
40
  module FontLoader
41
41
 
42
- # This module interprets the font name as file name and tries to load it.
42
+ # This module interprets the font name either as file name and tries to load it, or as font
43
+ # object to be wrapped directly.
43
44
  module FromFile
44
45
 
45
- # Loads the given font by interpreting the font name as file name.
46
+ # :call-seq:
47
+ # FromFile.call(document, file_name, subset: true, **) -> wrapped_font
48
+ # FromFile.call(document, font_object, subset: true, **) -> wrapped_font
46
49
  #
47
- # The file object representing the font file is *not* closed and if needed must be closed by
48
- # the caller once the font is not needed anymore.
50
+ # Returns an appropriate font wrapper for the given file name or font object.
51
+ #
52
+ # If a file name is given, the file object representing the font file is *not* closed and if
53
+ # needed must be closed by the caller once the font is not needed anymore.
54
+ #
55
+ # The first form using a file name is easier to use in one-off cases. However, if multiple
56
+ # documents always refer to the same font, the second form is better to avoid re-parsing the
57
+ # font file.
49
58
  #
50
59
  # +document+::
51
60
  # The PDF document to associate the font object with.
52
61
  #
53
- # +name+::
54
- # The file name.
62
+ # +file_name+/+font_object+::
63
+ # The file name or TrueType font object.
55
64
  #
56
65
  # +subset+::
57
66
  # Specifies whether the font should be subset if possible.
58
67
  def self.call(document, name, subset: true, **)
59
- return nil unless File.file?(name)
68
+ is_font = name.kind_of?(HexaPDF::Font::TrueType::Font)
69
+ return nil unless is_font || File.file?(name)
60
70
 
61
- font = HexaPDF::Font::TrueType::Font.new(File.open(name, 'rb'))
71
+ font = is_font ? name : HexaPDF::Font::TrueType::Font.new(File.open(name, 'rb'))
62
72
  HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: subset)
63
73
  end
64
74