hexapdf 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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?