hexapdf 0.12.3 → 0.14.3

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -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/dictionary.rb +12 -6
  13. data/lib/hexapdf/dictionary_fields.rb +2 -10
  14. data/lib/hexapdf/document.rb +41 -16
  15. data/lib/hexapdf/document/files.rb +0 -1
  16. data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
  17. data/lib/hexapdf/encryption/security_handler.rb +1 -0
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
  19. data/lib/hexapdf/font/cmap.rb +1 -4
  20. data/lib/hexapdf/font/true_type/subsetter.rb +16 -3
  21. data/lib/hexapdf/font/true_type/table/head.rb +1 -0
  22. data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
  23. data/lib/hexapdf/font/true_type/table/post.rb +15 -10
  24. data/lib/hexapdf/font_loader/from_configuration.rb +2 -2
  25. data/lib/hexapdf/font_loader/from_file.rb +18 -8
  26. data/lib/hexapdf/image_loader/png.rb +3 -2
  27. data/lib/hexapdf/importer.rb +3 -2
  28. data/lib/hexapdf/layout/line.rb +1 -1
  29. data/lib/hexapdf/layout/style.rb +23 -23
  30. data/lib/hexapdf/layout/text_layouter.rb +2 -2
  31. data/lib/hexapdf/layout/text_shaper.rb +3 -2
  32. data/lib/hexapdf/object.rb +52 -25
  33. data/lib/hexapdf/parser.rb +107 -7
  34. data/lib/hexapdf/pdf_array.rb +15 -5
  35. data/lib/hexapdf/revisions.rb +29 -21
  36. data/lib/hexapdf/serializer.rb +37 -10
  37. data/lib/hexapdf/task/optimize.rb +6 -4
  38. data/lib/hexapdf/tokenizer.rb +22 -0
  39. data/lib/hexapdf/type/acro_form/appearance_generator.rb +130 -27
  40. data/lib/hexapdf/type/acro_form/button_field.rb +5 -2
  41. data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
  42. data/lib/hexapdf/type/acro_form/field.rb +35 -5
  43. data/lib/hexapdf/type/acro_form/form.rb +139 -14
  44. data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
  45. data/lib/hexapdf/type/actions/uri.rb +3 -2
  46. data/lib/hexapdf/type/annotations/widget.rb +3 -4
  47. data/lib/hexapdf/type/catalog.rb +2 -2
  48. data/lib/hexapdf/type/cid_font.rb +1 -1
  49. data/lib/hexapdf/type/file_specification.rb +1 -1
  50. data/lib/hexapdf/type/font.rb +1 -1
  51. data/lib/hexapdf/type/font_simple.rb +4 -2
  52. data/lib/hexapdf/type/font_true_type.rb +6 -2
  53. data/lib/hexapdf/type/font_type0.rb +4 -4
  54. data/lib/hexapdf/type/form.rb +6 -2
  55. data/lib/hexapdf/type/image.rb +2 -2
  56. data/lib/hexapdf/type/page.rb +21 -12
  57. data/lib/hexapdf/type/page_tree_node.rb +29 -5
  58. data/lib/hexapdf/type/resources.rb +5 -0
  59. data/lib/hexapdf/type/trailer.rb +2 -3
  60. data/lib/hexapdf/utils/object_hash.rb +0 -1
  61. data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
  62. data/lib/hexapdf/version.rb +1 -1
  63. data/test/hexapdf/common_tokenizer_tests.rb +2 -2
  64. data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
  65. data/test/hexapdf/content/test_canvas.rb +3 -3
  66. data/test/hexapdf/content/test_color_space.rb +1 -1
  67. data/test/hexapdf/encryption/test_aes.rb +4 -4
  68. data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
  69. data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
  70. data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
  71. data/test/hexapdf/font/true_type/table/test_post.rb +1 -1
  72. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  73. data/test/hexapdf/font_loader/test_from_configuration.rb +7 -3
  74. data/test/hexapdf/font_loader/test_from_file.rb +7 -0
  75. data/test/hexapdf/layout/test_text_layouter.rb +12 -5
  76. data/test/hexapdf/test_configuration.rb +2 -2
  77. data/test/hexapdf/test_dictionary.rb +8 -1
  78. data/test/hexapdf/test_dictionary_fields.rb +9 -2
  79. data/test/hexapdf/test_document.rb +18 -10
  80. data/test/hexapdf/test_object.rb +71 -26
  81. data/test/hexapdf/test_parser.rb +205 -51
  82. data/test/hexapdf/test_pdf_array.rb +8 -1
  83. data/test/hexapdf/test_revisions.rb +35 -0
  84. data/test/hexapdf/test_serializer.rb +7 -0
  85. data/test/hexapdf/test_tokenizer.rb +28 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +288 -35
  88. data/test/hexapdf/type/acro_form/test_button_field.rb +15 -0
  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 +8 -1
  96. data/test/hexapdf/type/test_page.rb +8 -1
  97. data/test/hexapdf/type/test_page_tree_node.rb +42 -0
  98. data/test/hexapdf/type/test_resources.rb +6 -0
  99. data/test/hexapdf/utils/test_bit_field.rb +2 -0
  100. data/test/hexapdf/utils/test_object_hash.rb +5 -0
  101. data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
  102. data/test/test_helper.rb +2 -0
  103. metadata +6 -12
@@ -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
@@ -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,12 @@ 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: If field information is available for the entry, a Hash or Array value will always be
160
+ # wrapped by Dictionary or PDFArray. Otherwise, the value will be returned as-is.
161
+ #
162
+ # Note: This method may throw a "can't add a new key into hash during iteration" error in
163
+ # certain cases because it potentially modifies the underlying hash!
158
164
  def [](name)
159
165
  field = self.class.field(name)
160
166
  data = if key?(name)
@@ -163,7 +169,7 @@ module HexaPDF
163
169
  value[name] = field.default
164
170
  end
165
171
  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?)
172
+ if data.instance_of?(HexaPDF::Object) || (data.kind_of?(HexaPDF::Object) && data.value.nil?)
167
173
  data = data.value
168
174
  end
169
175
  if (result = field&.convert(data, document))
@@ -182,7 +188,7 @@ module HexaPDF
182
188
  raise ArgumentError, "Only Symbol (Name) keys are allowed to be used in PDF dictionaries"
183
189
  end
184
190
 
185
- if value[name].class == HexaPDF::Object && !data.kind_of?(HexaPDF::Object) &&
191
+ if value[name].instance_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Object) &&
186
192
  !data.kind_of?(HexaPDF::Reference)
187
193
  value[name].value = data
188
194
  else
@@ -255,7 +261,7 @@ module HexaPDF
255
261
 
256
262
  # Iterates over all currently set fields and those that are required.
257
263
  def each_set_key_or_required_field #:yields: name, field
258
- value.each_key {|name| yield(name, self.class.field(name)) }
264
+ value.keys.each {|name| yield(name, self.class.field(name)) }
259
265
  self.class.each_field do |name, field|
260
266
  yield(name, field) if field.required? && !value.key?(name)
261
267
  end
@@ -273,7 +279,7 @@ module HexaPDF
273
279
  # Check that required fields are set
274
280
  if field.required? && obj.nil?
275
281
  yield("Required field #{name} is not set", field.default?)
276
- self[name] = obj = field.default
282
+ self[name] = obj = field.default if field.default?
277
283
  end
278
284
 
279
285
  # Check if the document version is set high enough
@@ -301,7 +307,7 @@ module HexaPDF
301
307
 
302
308
  # Check the value of the field against the allowed values.
303
309
  if field.allowed_values && !field.allowed_values.include?(obj)
304
- yield("Field #{name} does not contain an allowed value")
310
+ yield("Field #{name} does not contain an allowed value: #{obj.inspect}")
305
311
  end
306
312
 
307
313
  # 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) } ||
@@ -352,7 +344,7 @@ module HexaPDF
352
344
  # Wraps a given array in the Rectangle class. Otherwise returns +nil+.
353
345
  def self.convert(data, _type, document)
354
346
  return unless data.kind_of?(Array) || data.kind_of?(HexaPDF::PDFArray)
355
- document.wrap(data, type: Rectangle)
347
+ data.empty? ? document.wrap(nil) : document.wrap(data, type: Rectangle)
356
348
  end
357
349
 
358
350
  end
@@ -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,16 @@ 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 (13), (, ) (40, 41) and \ (92) specially so that
67
+ # they never appear in the output (PDF serialization would need to escape them)
68
+ if @last_id == 13 || @last_id == 40 || @last_id == 92
69
+ @glyph_map[:"s#{@last_id}"] = @last_id
70
+ if @last_id == 40
71
+ @last_id += 1
72
+ @glyph_map[:"s#{@last_id}"] = @last_id
73
+ end
74
+ @last_id += 1
75
+ end
66
76
  @glyph_map[glyph_id] = @last_id
67
77
  end
68
78
 
@@ -107,7 +117,7 @@ module HexaPDF
107
117
  locations = []
108
118
 
109
119
  @glyph_map.each_key do |old_gid|
110
- glyph = orig_glyf[old_gid]
120
+ glyph = orig_glyf[old_gid.kind_of?(Symbol) ? 0 : old_gid]
111
121
  locations << table.size
112
122
  data = glyph.raw_data
113
123
  if glyph.compound?
@@ -134,7 +144,7 @@ module HexaPDF
134
144
  hmtx = @font[:hmtx]
135
145
  data = ''.b
136
146
  @glyph_map.each_key do |old_gid|
137
- metric = hmtx[old_gid]
147
+ metric = hmtx[old_gid.kind_of?(Symbol) ? 0 : old_gid]
138
148
  data << [metric.advance_width, metric.left_side_bearing].pack('n2')
139
149
  end
140
150
  data
@@ -166,7 +176,10 @@ module HexaPDF
166
176
  # Adds the components of compound glyphs to the subset.
167
177
  def add_glyph_components
168
178
  glyf = @font[:glyf]
169
- @glyph_map.keys.each {|gid| glyf[gid].components&.each {|cgid| use_glyph(cgid) } }
179
+ @glyph_map.keys.each do |gid|
180
+ next if gid.kind_of?(Symbol)
181
+ glyf[gid].components&.each {|cgid| use_glyph(cgid) }
182
+ end
170
183
  end
171
184
 
172
185
  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