hexapdf 0.4.0 → 0.5.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/CONTRIBUTERS +1 -1
  4. data/README.md +5 -5
  5. data/VERSION +1 -1
  6. data/examples/emoji-smile.png +0 -0
  7. data/examples/emoji-wink.png +0 -0
  8. data/examples/graphics.rb +9 -8
  9. data/examples/standard_pdf_fonts.rb +2 -1
  10. data/examples/text_box_alignment.rb +47 -0
  11. data/examples/text_box_inline_boxes.rb +56 -0
  12. data/examples/text_box_line_wrapping.rb +57 -0
  13. data/examples/text_box_shapes.rb +166 -0
  14. data/examples/text_box_styling.rb +72 -0
  15. data/examples/truetype.rb +3 -4
  16. data/lib/hexapdf/cli/optimize.rb +2 -2
  17. data/lib/hexapdf/configuration.rb +8 -6
  18. data/lib/hexapdf/content/canvas.rb +8 -5
  19. data/lib/hexapdf/content/parser.rb +3 -2
  20. data/lib/hexapdf/content/processor.rb +14 -3
  21. data/lib/hexapdf/document.rb +1 -0
  22. data/lib/hexapdf/document/fonts.rb +2 -1
  23. data/lib/hexapdf/document/pages.rb +23 -0
  24. data/lib/hexapdf/font/invalid_glyph.rb +78 -0
  25. data/lib/hexapdf/font/true_type/font.rb +14 -3
  26. data/lib/hexapdf/font/true_type/table.rb +1 -0
  27. data/lib/hexapdf/font/true_type/table/cmap.rb +1 -1
  28. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +1 -0
  29. data/lib/hexapdf/font/true_type/table/glyf.rb +4 -0
  30. data/lib/hexapdf/font/true_type/table/kern.rb +170 -0
  31. data/lib/hexapdf/font/true_type/table/post.rb +5 -1
  32. data/lib/hexapdf/font/true_type_wrapper.rb +71 -24
  33. data/lib/hexapdf/font/type1/afm_parser.rb +3 -2
  34. data/lib/hexapdf/font/type1/character_metrics.rb +0 -9
  35. data/lib/hexapdf/font/type1/font.rb +11 -0
  36. data/lib/hexapdf/font/type1/font_metrics.rb +6 -1
  37. data/lib/hexapdf/font/type1_wrapper.rb +51 -7
  38. data/lib/hexapdf/font_loader/standard14.rb +1 -1
  39. data/lib/hexapdf/layout.rb +51 -0
  40. data/lib/hexapdf/layout/inline_box.rb +95 -0
  41. data/lib/hexapdf/layout/line_fragment.rb +333 -0
  42. data/lib/hexapdf/layout/numeric_refinements.rb +56 -0
  43. data/lib/hexapdf/layout/style.rb +365 -0
  44. data/lib/hexapdf/layout/text_box.rb +727 -0
  45. data/lib/hexapdf/layout/text_fragment.rb +206 -0
  46. data/lib/hexapdf/layout/text_shaper.rb +155 -0
  47. data/lib/hexapdf/task.rb +0 -1
  48. data/lib/hexapdf/task/dereference.rb +1 -1
  49. data/lib/hexapdf/tokenizer.rb +3 -2
  50. data/lib/hexapdf/type/font_descriptor.rb +2 -1
  51. data/lib/hexapdf/type/font_type0.rb +3 -1
  52. data/lib/hexapdf/type/form.rb +12 -4
  53. data/lib/hexapdf/version.rb +1 -1
  54. data/test/hexapdf/common_tokenizer_tests.rb +7 -0
  55. data/test/hexapdf/content/common.rb +8 -0
  56. data/test/hexapdf/content/test_canvas.rb +10 -22
  57. data/test/hexapdf/content/test_processor.rb +4 -1
  58. data/test/hexapdf/document/test_pages.rb +16 -0
  59. data/test/hexapdf/font/test_invalid_glyph.rb +34 -0
  60. data/test/hexapdf/font/test_true_type_wrapper.rb +25 -11
  61. data/test/hexapdf/font/test_type1_wrapper.rb +26 -10
  62. data/test/hexapdf/font/true_type/table/common.rb +27 -0
  63. data/test/hexapdf/font/true_type/table/test_cmap.rb +14 -20
  64. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +7 -0
  65. data/test/hexapdf/font/true_type/table/test_glyf.rb +8 -6
  66. data/test/hexapdf/font/true_type/table/test_head.rb +9 -13
  67. data/test/hexapdf/font/true_type/table/test_hhea.rb +16 -23
  68. data/test/hexapdf/font/true_type/table/test_hmtx.rb +4 -7
  69. data/test/hexapdf/font/true_type/table/test_kern.rb +61 -0
  70. data/test/hexapdf/font/true_type/table/test_loca.rb +7 -13
  71. data/test/hexapdf/font/true_type/table/test_maxp.rb +4 -9
  72. data/test/hexapdf/font/true_type/table/test_name.rb +14 -17
  73. data/test/hexapdf/font/true_type/table/test_os2.rb +3 -5
  74. data/test/hexapdf/font/true_type/table/test_post.rb +21 -19
  75. data/test/hexapdf/font/true_type/test_font.rb +4 -0
  76. data/test/hexapdf/font/type1/common.rb +6 -0
  77. data/test/hexapdf/font/type1/test_afm_parser.rb +9 -0
  78. data/test/hexapdf/font/type1/test_font.rb +6 -0
  79. data/test/hexapdf/layout/test_inline_box.rb +40 -0
  80. data/test/hexapdf/layout/test_line_fragment.rb +206 -0
  81. data/test/hexapdf/layout/test_style.rb +143 -0
  82. data/test/hexapdf/layout/test_text_box.rb +640 -0
  83. data/test/hexapdf/layout/test_text_fragment.rb +208 -0
  84. data/test/hexapdf/layout/test_text_shaper.rb +64 -0
  85. data/test/hexapdf/task/test_dereference.rb +1 -0
  86. data/test/hexapdf/test_writer.rb +2 -2
  87. data/test/hexapdf/type/test_font_descriptor.rb +4 -2
  88. data/test/hexapdf/type/test_font_type0.rb +7 -0
  89. data/test/hexapdf/type/test_form.rb +12 -0
  90. metadata +29 -2
@@ -0,0 +1,72 @@
1
+ # ## Text Box Styling
2
+ #
3
+ # The text used as part of a [HexaPDF::Layout::TextBox] class can be styled
4
+ # using [HexaPDF::Layout::Style]. To do this [HexaPDF::Layout::TextFragment]
5
+ # objects have to be created with the needed styling and then added to a text
6
+ # box. In addition the style objects can be used for styling text boxes
7
+ # themselves.
8
+ #
9
+ # This example shows how to do this and shows off the various styling option.
10
+ #
11
+ # Usage:
12
+ # : `ruby text_box_styling.rb`
13
+ #
14
+
15
+ require 'hexapdf'
16
+
17
+ # Wraps the text in a TextFragment using the given style.
18
+ def fragment(text, style)
19
+ HexaPDF::Layout::TextFragment.new(items: style.font.decode_utf8(text),
20
+ style: style)
21
+ end
22
+
23
+ # Draws the text box at the given [x, y] position onto the canvas.
24
+ def draw_box(box, canvas, x, y)
25
+ rest, height = box.fit
26
+ raise "Error" unless rest.empty?
27
+ box.draw(canvas, x, y)
28
+ y - height
29
+ end
30
+
31
+ sample_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit,
32
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
33
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
34
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit
35
+ in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
36
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
37
+ officia deserunt mollit anim id est laborum. ".tr("\n", ' ') * 3
38
+
39
+ doc = HexaPDF::Document.new
40
+
41
+ heading = HexaPDF::Layout::Style.new(font: doc.fonts.load("Helvetica", variant: :bold),
42
+ font_size: 16, align: :center)
43
+ body = HexaPDF::Layout::Style.new(font: doc.fonts.load("Times"),
44
+ font_size: 10, align: :justify,
45
+ text_indent: 20)
46
+ body.line_spacing(:proportional, 1.5)
47
+ standout = HexaPDF::Layout::Style.new(font: doc.fonts.load("Times", variant: :bold),
48
+ font_size: 14)
49
+
50
+ canvas = doc.pages.add.canvas
51
+ y_base = 800
52
+ left = 50
53
+ width = 500
54
+
55
+ box = HexaPDF::Layout::TextBox.new(items: [fragment("This is a header", heading)],
56
+ width: width, style: heading)
57
+ y_base = draw_box(box, canvas, left, y_base)
58
+
59
+ box = HexaPDF::Layout::TextBox.new(items: [fragment(sample_text, body)],
60
+ width: width, style: body)
61
+ y_base = draw_box(box, canvas, left, y_base - 20)
62
+
63
+ items = [
64
+ fragment(sample_text[0, 50], body), fragment(sample_text[50, 15], standout),
65
+ fragment(sample_text[65, 150], body), fragment(sample_text[215, 50], standout),
66
+ fragment(sample_text[265...800], body), fragment(sample_text[800, 40], standout),
67
+ fragment(sample_text[840..-1], body)
68
+ ]
69
+ box = HexaPDF::Layout::TextBox.new(items: items, width: width, style: body)
70
+ draw_box(box, canvas, left, y_base - 20)
71
+
72
+ doc.write("text_box_styling.pdf", optimize: true)
@@ -11,13 +11,12 @@
11
11
  # usual.
12
12
  #
13
13
  # Usage:
14
- # : `ruby truetype.pdf [FONT_FILE]`
14
+ # : `ruby truetype.rb [FONT_FILE]`
15
15
  #
16
16
 
17
17
  require 'hexapdf'
18
18
 
19
19
  doc = HexaPDF::Document.new
20
- doc.config['font.on_missing_glyph'] = ->(_, f) { f.missing_glyph_id }
21
20
  doc.config['font.map'] = {
22
21
  'myfont' => {none: ARGV.shift || File.join(__dir__, '../test/data/fonts/Ubuntu-Title.ttf')},
23
22
  }
@@ -26,7 +25,7 @@ wrapper = doc.fonts.load('myfont')
26
25
  max_gid = wrapper.wrapped_font[:maxp].num_glyphs
27
26
 
28
27
  255.times do |page|
29
- break unless page * 256 < wrapper.wrapped_font[:maxp].num_glyphs
28
+ break unless page * 256 < max_gid
30
29
  canvas = doc.pages.add.canvas
31
30
  canvas.font("Helvetica", size: 10)
32
31
  canvas.text("Font: #{wrapper.wrapped_font.full_name}", at: [50, 825])
@@ -37,7 +36,7 @@ max_gid = wrapper.wrapped_font[:maxp].num_glyphs
37
36
  canvas.show_glyphs((0..15).map do |i|
38
37
  gid = page * 256 + y * 16 + i
39
38
  glyph = wrapper.glyph(gid)
40
- gid > max_gid ? [] : [glyph, -(2000 - glyph.width)]
39
+ gid >= max_gid ? [] : [glyph, -(2000 - glyph.width)]
41
40
  end.flatten!)
42
41
  end
43
42
  end
@@ -87,10 +87,10 @@ module HexaPDF
87
87
  end
88
88
  doc.catalog[:Pages] = page_tree
89
89
 
90
- doc.each(current: false) do |obj|
90
+ doc.each(current: false) do |obj, revision|
91
91
  next unless obj.kind_of?(HexaPDF::Dictionary)
92
92
  if (obj.type == :Pages || obj.type == :Page) && !retained.key?(obj.data)
93
- doc.delete(obj)
93
+ revision.delete(obj)
94
94
  end
95
95
  end
96
96
  end
@@ -31,6 +31,7 @@
31
31
  # is created or manipulated using HexaPDF.
32
32
  #++
33
33
 
34
+ require 'hexapdf/font/invalid_glyph'
34
35
  require 'hexapdf/error'
35
36
 
36
37
  module HexaPDF
@@ -159,11 +160,12 @@ module HexaPDF
159
160
  # font.on_missing_glyph::
160
161
  # Callback hook when an UTF-8 character cannot be mapped to a glyph of a font.
161
162
  #
162
- # The value needs to be an object that responds to \#call(code_or_name, font) where
163
- # +code_or_name+ is the Unicode value or the glyph name for the missing glyph and returns a
164
- # substitute glyph name/ID to be used instead.
163
+ # The value needs to be an object that responds to \#call(character, font_type, font) where
164
+ # +character+ is the Unicode character for the missing glyph and returns a substitute glyph to
165
+ # be used instead.
165
166
  #
166
- # The default implementation raises an error.
167
+ # The default implementation returns an object of class HexaPDF::Font::InvalidGlyph which, when
168
+ # not removed before encoding, will raise an error.
167
169
  #
168
170
  # font.on_missing_unicode_mapping::
169
171
  # Callback hook when a character code point cannot be converted to a Unicode character.
@@ -233,8 +235,8 @@ module HexaPDF
233
235
  DefaultDocumentConfiguration =
234
236
  Configuration.new('document.auto_decrypt' => true,
235
237
  'font.map' => {},
236
- 'font.on_missing_glyph' => proc do |n, f|
237
- raise HexaPDF::Error, "No glyph for '#{n}' in font #{f.font_name} found"
238
+ 'font.on_missing_glyph' => proc do |char, _type, font|
239
+ HexaPDF::Font::InvalidGlyph.new(font, char)
238
240
  end,
239
241
  'font.on_missing_unicode_mapping' => proc do |code_point, font|
240
242
  raise HexaPDF::Error, "No Unicode mapping for code point #{code_point} " \
@@ -1322,10 +1322,13 @@ module HexaPDF
1322
1322
  # canvas.word_spacing(amount) => canvas
1323
1323
  # canvas.word_spacing(amount) { block } => canvas
1324
1324
  #
1325
- # The word spacing determines how much additional space is added when the ASCII space
1326
- # character is encountered in a text. For horizontal writing positive values increase the
1327
- # distance between two words, whereas for vertical writing negative values increase the
1328
- # distance.
1325
+ # If the font's PDF encoding supports this, the word spacing determines how much additional
1326
+ # space is added when the ASCII space character is encountered in a text. For horizontal
1327
+ # writing positive values increase the distance between two words, whereas for vertical
1328
+ # writing negative values increase the distance.
1329
+ #
1330
+ # Note that in HexaPDF only the standard 14 PDF Type1 fonts support this property! When using
1331
+ # any other font, for example a TrueType font, this property has no effect.
1329
1332
  #
1330
1333
  # Returns the current word spacing value (see Content::GraphicsState#word_spacing) when no
1331
1334
  # argument is given. Otherwise sets the word spacing using the +amount+ argument and returns
@@ -1609,7 +1612,7 @@ module HexaPDF
1609
1612
  # See: PDF1.7 s9.2.2
1610
1613
  def font(name = nil, size: nil, **options)
1611
1614
  if name
1612
- @font = context.document.fonts.load(name, options)
1615
+ @font = (name.respond_to?(:dict) ? name : context.document.fonts.load(name, options))
1613
1616
  if size
1614
1617
  font_size(size)
1615
1618
  else
@@ -119,10 +119,11 @@ module HexaPDF
119
119
  def parse_number
120
120
  if (val = @ss.scan(/[+-]?\d++(?!\.)/))
121
121
  val.to_i
122
- else
123
- val = @ss.scan(/[+-]?(?:\d+\.\d*|\.\d+)/)
122
+ elsif (val = @ss.scan(/[+-]?(?:\d+\.\d*|\.\d+)/))
124
123
  val << '0'.freeze if val.getbyte(-1) == 46 # dot '.'
125
124
  Float(val)
125
+ else
126
+ parse_keyword
126
127
  end
127
128
  end
128
129
 
@@ -305,7 +305,7 @@ module HexaPDF
305
305
  attr_reader :operators
306
306
 
307
307
  # The resources dictionary used during processing.
308
- attr_accessor :resources
308
+ attr_reader :resources
309
309
 
310
310
  # The GraphicsState object containing the current graphics state.
311
311
  #
@@ -336,8 +336,19 @@ module HexaPDF
336
336
  def initialize(resources = nil)
337
337
  @operators = Operator::DEFAULT_OPERATORS.dup
338
338
  @graphics_state = GraphicsState.new
339
- @resources = resources
340
339
  @graphics_object = :none
340
+ @original_resources = nil
341
+ self.resources = resources
342
+ end
343
+
344
+ # Sets the resources dictionary used during processing.
345
+ #
346
+ # The first time resources are set, they are also stored as the "original" resources. This is
347
+ # needed because form XObject don't need to have a resources dictionary and can use the page's
348
+ # resources dictionary instead.
349
+ def resources=(res)
350
+ @original_resources = res if @original_resources.nil?
351
+ @resources = res
341
352
  end
342
353
 
343
354
  # Processes the operator with the given operands.
@@ -364,7 +375,7 @@ module HexaPDF
364
375
  graphics_state.save
365
376
 
366
377
  graphics_state.ctm.premultiply(*xobject[:Matrix]) if xobject.key?(:Matrix)
367
- xobject.process_contents(self)
378
+ xobject.process_contents(self, original_resources: @original_resources)
368
379
 
369
380
  graphics_state.restore
370
381
  self.resources = res
@@ -46,6 +46,7 @@ require 'hexapdf/writer'
46
46
  require 'hexapdf/importer'
47
47
  require 'hexapdf/image_loader'
48
48
  require 'hexapdf/font_loader'
49
+ require 'hexapdf/layout'
49
50
 
50
51
  # == HexaPDF API Documentation
51
52
  #
@@ -67,7 +67,8 @@ module HexaPDF
67
67
  if font
68
68
  @loaded_fonts_cache[[name, options]] = font
69
69
  else
70
- raise HexaPDF::Error, "The requested font '#{name}' couldn't be found"
70
+ raise HexaPDF::Error, "The requested font '#{name}' in variant '#{options[:variant]}' " \
71
+ "couldn't be found"
71
72
  end
72
73
  end
73
74
 
@@ -31,6 +31,8 @@
31
31
  # is created or manipulated using HexaPDF.
32
32
  #++
33
33
 
34
+ require 'hexapdf/error'
35
+
34
36
  module HexaPDF
35
37
  class Document
36
38
 
@@ -52,8 +54,29 @@ module HexaPDF
52
54
  @document.catalog.pages
53
55
  end
54
56
 
57
+ # :call-seq:
58
+ # pages.add -> new_page
59
+ # pages.add(media_box) -> new_page
60
+ # pages.add(page) -> page
61
+ #
55
62
  # Adds the page or a new empty page at the end and returns it.
63
+ #
64
+ # If no argument is given, a new page with the default dimensions (see configuration option
65
+ # 'page.default_media_box') is used. If the single argument is an array with four numbers
66
+ # (specifying the media box) or a symbol (referencing a pre-defined media box, see
67
+ # HexaPDF::Type::Page::PAPER_SIZE), the new page will have these dimensions.
56
68
  def add(page = nil)
69
+ case page
70
+ when Array
71
+ page = @document.add(Type: :Page, MediaBox: page)
72
+ when Symbol
73
+ if Type::Page::PAPER_SIZE.key?(page)
74
+ media_box = Type::Page::PAPER_SIZE[page].dup
75
+ page = @document.add(Type: :Page, MediaBox: media_box)
76
+ else
77
+ raise HexaPDF::Error, "Invalid page format specified: #{page}"
78
+ end
79
+ end
57
80
  @document.catalog.pages.add_page(page)
58
81
  end
59
82
 
@@ -0,0 +1,78 @@
1
+ # -*- encoding: utf-8 -*-
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-2017 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
+
34
+ module HexaPDF
35
+ module Font
36
+
37
+ # Represents an invalid glyph, i.e. a Unicode character that has no representation in the used
38
+ # font.
39
+ class InvalidGlyph
40
+
41
+ # The string that could not be represented as a glyph.
42
+ attr_reader :str
43
+
44
+ # Creates a new Glyph object.
45
+ def initialize(font, str)
46
+ @font = font
47
+ @str = str
48
+ end
49
+
50
+ # Returns the appropriate missing glyph id based on the used font.
51
+ def id
52
+ @font.missing_glyph_id
53
+ end
54
+ alias_method :name, :id
55
+
56
+ # Returns 0.
57
+ def x_min
58
+ 0
59
+ end
60
+ alias_method :x_max, :x_min
61
+ alias_method :y_min, :x_min
62
+ alias_method :y_max, :x_min
63
+ alias_method :width, :x_min
64
+
65
+ # Word spacing is never applied for the invalid glyph, so +false+ is returned.
66
+ def apply_word_spacing?
67
+ false
68
+ end
69
+
70
+ #:nodoc:
71
+ def inspect
72
+ "#<#{self.class.name} font=#{@font.full_name.inspect} id=#{id} #{@str.inspect}>"
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+ end
@@ -32,6 +32,7 @@
32
32
  #++
33
33
 
34
34
  require 'hexapdf/font/true_type/table'
35
+ require 'set'
35
36
 
36
37
  module HexaPDF
37
38
  module Font
@@ -45,8 +46,8 @@ module HexaPDF
45
46
  # font.ttf.table_mapping::
46
47
  # The default mapping from table tag as symbol to table class name.
47
48
  #
48
- # font.ttf.cmap.unknown_format::
49
- # Action to take when encountering unknown 'cmap' subtables. Can either be :ignore
49
+ # font.ttf.unknown_format::
50
+ # Action to take when encountering unknown subtables. Can either be :ignore
50
51
  # which ignores them or :raise which raises an error.
51
52
  DEFAULT_CONFIG = {
52
53
  'font.true_type.table_mapping' => {
@@ -60,8 +61,9 @@ module HexaPDF
60
61
  post: 'HexaPDF::Font::TrueType::Table::Post',
61
62
  glyf: 'HexaPDF::Font::TrueType::Table::Glyf',
62
63
  'OS/2': 'HexaPDF::Font::TrueType::Table::OS2',
64
+ kern: 'HexaPDF::Font::TrueType::Table::Kern',
63
65
  },
64
- 'font.true_type.cmap.unknown_format' => :ignore,
66
+ 'font.true_type.unknown_format' => :ignore,
65
67
  }
66
68
 
67
69
 
@@ -93,6 +95,15 @@ module HexaPDF
93
95
  @directory ||= Table::Directory.new(self, io ? Table::Directory::SELF_ENTRY : nil)
94
96
  end
95
97
 
98
+ # Returns a set of features this font supports.
99
+ #
100
+ # Features that may be available are for example :kern or :liga.
101
+ def features
102
+ @features ||= Set.new.tap do |set|
103
+ set << :kern if self[:kern]&.horizontal_kerning_subtable
104
+ end
105
+ end
106
+
96
107
  # Returns the PostScript font name.
97
108
  def font_name
98
109
  self[:name][:postscript_name].preferred_record
@@ -53,6 +53,7 @@ module HexaPDF
53
53
  autoload(:Post, 'hexapdf/font/true_type/table/post')
54
54
  autoload(:Glyf, 'hexapdf/font/true_type/table/glyf')
55
55
  autoload(:OS2, 'hexapdf/font/true_type/table/os2')
56
+ autoload(:Kern, 'hexapdf/font/true_type/table/kern')
56
57
 
57
58
 
58
59
  # The time Epoch used in sfnt-formatted font files.
@@ -64,7 +64,7 @@ module HexaPDF
64
64
  def parse_table #:nodoc:
65
65
  @version, num_tables = read_formatted(4, 'n2')
66
66
  @tables = []
67
- handle_unknown = font.config['font.true_type.cmap.unknown_format']
67
+ handle_unknown = font.config['font.true_type.unknown_format']
68
68
 
69
69
  num_tables.times { @tables << read_formatted(8, 'n2N') }
70
70
  offset_map = {}
@@ -267,6 +267,7 @@ module HexaPDF
267
267
  offset = id_range_offsets[index]
268
268
  if offset != 0
269
269
  glyph_id = glyph_indexes[offset - end_codes.length + (code - start_codes[index])]
270
+ glyph_id ||= 0 # Handle invalid subtable entries
270
271
  glyph_id = (glyph_id + id_deltas[index]) % 65536 if glyph_id != 0
271
272
  else
272
273
  glyph_id = (code + id_deltas[index]) % 65536
@@ -82,6 +82,10 @@ module HexaPDF
82
82
  @raw_data = raw_data
83
83
  @number_of_contours, @x_min, @y_min, @x_max, @y_max = @raw_data.unpack('s>5')
84
84
  @number_of_contours ||= 0
85
+ @x_min ||= 0
86
+ @y_min ||= 0
87
+ @x_max ||= 0
88
+ @y_max ||= 0
85
89
  @components = nil
86
90
  @component_offsets = nil
87
91
  parse_compound_glyph if compound?