ttfunk 1.5.1 → 1.6.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 +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +60 -0
  5. data/README.md +2 -1
  6. data/lib/ttfunk.rb +45 -0
  7. data/lib/ttfunk/aggregate.rb +15 -0
  8. data/lib/ttfunk/bin_utils.rb +47 -0
  9. data/lib/ttfunk/bit_field.rb +31 -0
  10. data/lib/ttfunk/collection.rb +3 -1
  11. data/lib/ttfunk/directory.rb +6 -0
  12. data/lib/ttfunk/encoded_string.rb +97 -0
  13. data/lib/ttfunk/max.rb +25 -0
  14. data/lib/ttfunk/min.rb +25 -0
  15. data/lib/ttfunk/one_based_array.rb +36 -0
  16. data/lib/ttfunk/otf_encoder.rb +61 -0
  17. data/lib/ttfunk/placeholder.rb +13 -0
  18. data/lib/ttfunk/reader.rb +34 -32
  19. data/lib/ttfunk/resource_file.rb +7 -5
  20. data/lib/ttfunk/sci_form.rb +29 -0
  21. data/lib/ttfunk/sub_table.rb +38 -0
  22. data/lib/ttfunk/subset.rb +2 -0
  23. data/lib/ttfunk/subset/base.rb +61 -120
  24. data/lib/ttfunk/subset/code_page.rb +89 -0
  25. data/lib/ttfunk/subset/mac_roman.rb +5 -42
  26. data/lib/ttfunk/subset/unicode.rb +12 -6
  27. data/lib/ttfunk/subset/unicode_8bit.rb +14 -12
  28. data/lib/ttfunk/subset/windows_1252.rb +5 -47
  29. data/lib/ttfunk/subset_collection.rb +4 -0
  30. data/lib/ttfunk/sum.rb +20 -0
  31. data/lib/ttfunk/table.rb +4 -0
  32. data/lib/ttfunk/table/cff.rb +69 -0
  33. data/lib/ttfunk/table/cff/charset.rb +212 -0
  34. data/lib/ttfunk/table/cff/charsets.rb +14 -0
  35. data/lib/ttfunk/table/cff/charsets/expert.rb +189 -0
  36. data/lib/ttfunk/table/cff/charsets/expert_subset.rb +119 -0
  37. data/lib/ttfunk/table/cff/charsets/iso_adobe.rb +241 -0
  38. data/lib/ttfunk/table/cff/charsets/standard_strings.rb +404 -0
  39. data/lib/ttfunk/table/cff/charstring.rb +487 -0
  40. data/lib/ttfunk/table/cff/charstrings_index.rb +39 -0
  41. data/lib/ttfunk/table/cff/dict.rb +266 -0
  42. data/lib/ttfunk/table/cff/encoding.rb +220 -0
  43. data/lib/ttfunk/table/cff/encodings.rb +12 -0
  44. data/lib/ttfunk/table/cff/encodings/expert.rb +206 -0
  45. data/lib/ttfunk/table/cff/encodings/standard.rb +181 -0
  46. data/lib/ttfunk/table/cff/fd_selector.rb +150 -0
  47. data/lib/ttfunk/table/cff/font_dict.rb +79 -0
  48. data/lib/ttfunk/table/cff/font_index.rb +29 -0
  49. data/lib/ttfunk/table/cff/header.rb +33 -0
  50. data/lib/ttfunk/table/cff/index.rb +125 -0
  51. data/lib/ttfunk/table/cff/one_based_index.rb +31 -0
  52. data/lib/ttfunk/table/cff/path.rb +66 -0
  53. data/lib/ttfunk/table/cff/private_dict.rb +84 -0
  54. data/lib/ttfunk/table/cff/subr_index.rb +19 -0
  55. data/lib/ttfunk/table/cff/top_dict.rb +230 -0
  56. data/lib/ttfunk/table/cff/top_index.rb +16 -0
  57. data/lib/ttfunk/table/cmap.rb +4 -4
  58. data/lib/ttfunk/table/cmap/format00.rb +1 -2
  59. data/lib/ttfunk/table/cmap/format04.rb +11 -3
  60. data/lib/ttfunk/table/cmap/format06.rb +2 -0
  61. data/lib/ttfunk/table/cmap/format10.rb +2 -0
  62. data/lib/ttfunk/table/cmap/format12.rb +2 -0
  63. data/lib/ttfunk/table/cmap/subtable.rb +12 -8
  64. data/lib/ttfunk/table/dsig.rb +50 -0
  65. data/lib/ttfunk/table/glyf.rb +11 -9
  66. data/lib/ttfunk/table/glyf/compound.rb +14 -7
  67. data/lib/ttfunk/table/glyf/path_based.rb +47 -0
  68. data/lib/ttfunk/table/glyf/simple.rb +21 -15
  69. data/lib/ttfunk/table/head.rb +43 -5
  70. data/lib/ttfunk/table/hhea.rb +47 -4
  71. data/lib/ttfunk/table/hmtx.rb +11 -4
  72. data/lib/ttfunk/table/kern.rb +3 -0
  73. data/lib/ttfunk/table/kern/format0.rb +3 -0
  74. data/lib/ttfunk/table/loca.rb +2 -0
  75. data/lib/ttfunk/table/maxp.rb +144 -10
  76. data/lib/ttfunk/table/name.rb +75 -37
  77. data/lib/ttfunk/table/os2.rb +327 -4
  78. data/lib/ttfunk/table/post.rb +8 -1
  79. data/lib/ttfunk/table/post/format10.rb +2 -0
  80. data/lib/ttfunk/table/post/format20.rb +5 -1
  81. data/lib/ttfunk/table/post/format30.rb +2 -0
  82. data/lib/ttfunk/table/post/format40.rb +2 -0
  83. data/lib/ttfunk/table/sbix.rb +2 -0
  84. data/lib/ttfunk/table/simple.rb +2 -0
  85. data/lib/ttfunk/table/vorg.rb +54 -0
  86. data/lib/ttfunk/ttf_encoder.rb +220 -0
  87. metadata +88 -20
  88. metadata.gz.sig +0 -0
  89. data/lib/ttfunk/encoding/mac_roman.rb +0 -100
  90. data/lib/ttfunk/encoding/windows_1252.rb +0 -76
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class OneBasedArray
5
+ include Enumerable
6
+
7
+ def initialize(size = 0)
8
+ @entries = Array.new(size)
9
+ end
10
+
11
+ def [](idx)
12
+ if idx == 0
13
+ raise IndexError,
14
+ "index #{idx} was outside the bounds of the array"
15
+ end
16
+
17
+ entries[idx - 1]
18
+ end
19
+
20
+ def size
21
+ entries.size
22
+ end
23
+
24
+ def to_ary
25
+ entries
26
+ end
27
+
28
+ def each(&block)
29
+ entries.each(&block)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :entries
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class OTFEncoder < TTFEncoder
5
+ OPTIMAL_TABLE_ORDER = [
6
+ 'head', 'hhea', 'maxp', 'OS/2', 'name', 'cmap', 'post', 'CFF '
7
+ ].freeze
8
+
9
+ private
10
+
11
+ # CFF fonts don't maintain a glyf table, all glyph information is stored
12
+ # in the charstrings index. Return an empty hash here to indicate a glyf
13
+ # table should not be encoded.
14
+ def glyf_table
15
+ @glyf_table ||= {}
16
+ end
17
+
18
+ # Since CFF fonts don't maintain a glyf table, they also don't maintain
19
+ # a loca table. Return an empty hash here to indicate a loca table
20
+ # shouldn't be encoded.
21
+ def loca_table
22
+ @loca_table ||= {}
23
+ end
24
+
25
+ def base_table
26
+ @base_table ||= TTFunk::Table::Simple.new(original, 'BASE').raw
27
+ end
28
+
29
+ def cff_table
30
+ @cff_table ||= original.cff.encode(new_to_old_glyph, old_to_new_glyph)
31
+ end
32
+
33
+ def vorg_table
34
+ @vorg_table ||= TTFunk::Table::Vorg.encode(original.vertical_origins)
35
+ end
36
+
37
+ def tables
38
+ @tables ||= super.merge(
39
+ 'BASE' => base_table,
40
+ 'VORG' => vorg_table,
41
+ 'CFF ' => cff_table
42
+ ).reject { |_tag, table| table.nil? }
43
+ end
44
+
45
+ def optimal_table_order
46
+ # DSIG is always last
47
+ OPTIMAL_TABLE_ORDER +
48
+ (tables.keys - ['DSIG'] - OPTIMAL_TABLE_ORDER) +
49
+ ['DSIG']
50
+ end
51
+
52
+ def collect_glyphs(glyph_ids)
53
+ # CFF top indexes are supposed to contain only one font, although they're
54
+ # capable of supporting many (no idea why this is true, maybe for CFF
55
+ # v2??). Anyway it's cool to do top_index[0], don't worry about it.
56
+ glyph_ids.each_with_object({}) do |id, h|
57
+ h[id] = original.cff.top_index[0].charstrings_index[id]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class Placeholder
5
+ attr_accessor :position
6
+ attr_reader :name, :length
7
+
8
+ def initialize(name, length: 1)
9
+ @name = name
10
+ @length = length
11
+ end
12
+ end
13
+ end
@@ -1,45 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TTFunk
2
4
  module Reader
3
5
  private
4
6
 
5
- def io
6
- @file.contents
7
- end
7
+ def io
8
+ @file.contents
9
+ end
8
10
 
9
- def read(bytes, format)
10
- io.read(bytes).unpack(format)
11
- end
11
+ def read(bytes, format)
12
+ io.read(bytes).unpack(format)
13
+ end
12
14
 
13
- def read_signed(count)
14
- read(count * 2, 'n*').map { |i| to_signed(i) }
15
- end
15
+ def read_signed(count)
16
+ read(count * 2, 'n*').map { |i| to_signed(i) }
17
+ end
16
18
 
17
- def to_signed(n)
18
- n >= 0x8000 ? -((n ^ 0xFFFF) + 1) : n
19
- end
19
+ def to_signed(number)
20
+ number >= 0x8000 ? -((number ^ 0xFFFF) + 1) : number
21
+ end
20
22
 
21
- def parse_from(position)
22
- saved = io.pos
23
- io.pos = position
24
- result = yield position
25
- io.pos = saved
26
- result
27
- end
23
+ def parse_from(position)
24
+ saved = io.pos
25
+ io.pos = position
26
+ result = yield position
27
+ io.pos = saved
28
+ result
29
+ end
28
30
 
29
- # For debugging purposes
30
- def hexdump(string)
31
- bytes = string.unpack('C*')
32
- bytes.each_with_index do |c, i|
33
- printf('%02X', c)
34
- if (i + 1) % 16 == 0
35
- puts
36
- elsif (i + 1) % 8 == 0
37
- print ' '
38
- else
39
- print ' '
40
- end
31
+ # For debugging purposes
32
+ def hexdump(string)
33
+ bytes = string.unpack('C*')
34
+ bytes.each_with_index do |c, i|
35
+ printf('%02X', c)
36
+ if (i + 1) % 16 == 0
37
+ puts
38
+ elsif (i + 1) % 8 == 0
39
+ print ' '
40
+ else
41
+ print ' '
41
42
  end
42
- puts unless bytes.length % 16 == 0
43
43
  end
44
+ puts unless bytes.length % 16 == 0
45
+ end
44
46
  end
45
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TTFunk
2
4
  class ResourceFile
3
5
  attr_reader :map
@@ -23,7 +25,7 @@ module TTFunk
23
25
  name_list_offset += map_offset
24
26
 
25
27
  @io.pos = type_list_offset
26
- max_index = @io.read(2).unpack('n').first
28
+ max_index = @io.read(2).unpack1('n')
27
29
  0.upto(max_index) do
28
30
  type, max_type_index, ref_list_offset = @io.read(8).unpack('A4nn')
29
31
  @map[type] = { list: [], named: {} }
@@ -32,8 +34,8 @@ module TTFunk
32
34
  0.upto(max_type_index) do
33
35
  id, name_ofs, attr = @io.read(5).unpack('nnC')
34
36
  data_ofs = @io.read(3)
35
- data_ofs = data_offset + [0, data_ofs].pack('CA*').unpack('N').first
36
- handle = @io.read(4).unpack('N').first
37
+ data_ofs = data_offset + [0, data_ofs].pack('CA*').unpack1('N')
38
+ handle = @io.read(4).unpack1('N')
37
39
 
38
40
  entry = {
39
41
  id: id,
@@ -44,7 +46,7 @@ module TTFunk
44
46
 
45
47
  if name_list_offset + name_ofs < map_offset + map_length
46
48
  parse_from(name_ofs + name_list_offset) do
47
- len = @io.read(1).unpack('C').first
49
+ len = @io.read(1).unpack1('C')
48
50
  entry[:name] = @io.read(len)
49
51
  end
50
52
  end
@@ -61,7 +63,7 @@ module TTFunk
61
63
  collection = index.is_a?(Integer) ? :list : :named
62
64
  if @map[type][collection][index]
63
65
  parse_from(@map[type][collection][index][:offset]) do
64
- length = @io.read(4).unpack('N').first
66
+ length = @io.read(4).unpack1('N')
65
67
  return @io.read(length)
66
68
  end
67
69
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class SciForm
5
+ attr_reader :significand, :exponent
6
+ alias eql? ==
7
+
8
+ def initialize(significand, exponent = 0)
9
+ @significand = significand
10
+ @exponent = exponent
11
+ end
12
+
13
+ def to_f
14
+ significand * 10**exponent
15
+ end
16
+
17
+ def ==(other)
18
+ case other
19
+ when Float
20
+ other == to_f
21
+ when self.class
22
+ other.significand == significand &&
23
+ other.exponent == exponent
24
+ else
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './reader'
4
+
5
+ module TTFunk
6
+ class SubTable
7
+ class EOTError < StandardError
8
+ end
9
+
10
+ include Reader
11
+
12
+ attr_reader :file, :table_offset, :length
13
+
14
+ def initialize(file, offset, length = nil)
15
+ @file = file
16
+ @table_offset = offset
17
+ @length = length
18
+ parse_from(@table_offset) { parse! }
19
+ end
20
+
21
+ # end of table
22
+ def eot?
23
+ # if length isn't set yet there's no way to know if we're at the end of
24
+ # the table or not
25
+ return false unless length
26
+
27
+ io.pos > table_offset + length
28
+ end
29
+
30
+ def read(*args)
31
+ if eot?
32
+ raise EOTError, 'attempted to read past the end of the table'
33
+ end
34
+
35
+ super
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'subset/unicode'
2
4
  require_relative 'subset/unicode_8bit'
3
5
  require_relative 'subset/mac_roman'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table/cmap'
2
4
  require_relative '../table/glyf'
3
5
  require_relative '../table/head'
@@ -13,6 +15,9 @@ require_relative '../table/simple'
13
15
  module TTFunk
14
16
  module Subset
15
17
  class Base
18
+ MICROSOFT_PLATFORM_ID = 3
19
+ MS_SYMBOL_ENCODING_ID = 0
20
+
16
21
  attr_reader :original
17
22
 
18
23
  def initialize(original)
@@ -23,146 +28,82 @@ module TTFunk
23
28
  false
24
29
  end
25
30
 
31
+ def microsoft_symbol?
32
+ new_cmap_table[:platform_id] == MICROSOFT_PLATFORM_ID &&
33
+ new_cmap_table[:encoding_id] == MS_SYMBOL_ENCODING_ID
34
+ end
35
+
26
36
  def to_unicode_map
27
37
  {}
28
38
  end
29
39
 
30
40
  def encode(options = {})
31
- cmap_table = new_cmap_table(options)
32
- glyphs = collect_glyphs(original_glyph_ids)
41
+ encoder_klass.new(original, self, options).encode
42
+ end
33
43
 
34
- old2new_glyph = cmap_table[:charmap]
35
- .each_with_object(0 => 0) do |(_, ids), map|
36
- map[ids[:old]] = ids[:new]
37
- end
38
- next_glyph_id = cmap_table[:max_glyph_id]
44
+ def encoder_klass
45
+ original.cff.exists? ? OTFEncoder : TTFEncoder
46
+ end
39
47
 
40
- glyphs.keys.each do |old_id|
41
- unless old2new_glyph.key?(old_id)
42
- old2new_glyph[old_id] = next_glyph_id
43
- next_glyph_id += 1
44
- end
45
- end
48
+ def unicode_cmap
49
+ @unicode_cmap ||= @original.cmap.unicode.first
50
+ end
46
51
 
47
- new2old_glyph = old2new_glyph.invert
48
-
49
- # "mandatory" tables. Every font should ("should") have these, including
50
- # the cmap table (encoded above).
51
- glyf_table = TTFunk::Table::Glyf.encode(
52
- glyphs, new2old_glyph, old2new_glyph
53
- )
54
- loca_table = TTFunk::Table::Loca.encode(glyf_table[:offsets])
55
- hmtx_table = TTFunk::Table::Hmtx.encode(
56
- original.horizontal_metrics, new2old_glyph
57
- )
58
- hhea_table = TTFunk::Table::Hhea.encode(
59
- original.horizontal_header, hmtx_table
60
- )
61
- maxp_table = TTFunk::Table::Maxp.encode(
62
- original.maximum_profile, old2new_glyph
63
- )
64
- post_table = TTFunk::Table::Post.encode(
65
- original.postscript, new2old_glyph
66
- )
67
- name_table = TTFunk::Table::Name.encode(
68
- original.name, glyf_table[:table]
69
- )
70
- head_table = TTFunk::Table::Head.encode(
71
- original.header, loca_table
72
- )
73
-
74
- # "optional" tables. Fonts may omit these if they do not need them.
75
- # Because they apply globally, we can simply copy them over, without
76
- # modification, if they exist.
77
- os2_table = original.os2.raw
78
- cvt_table = TTFunk::Table::Simple.new(original, 'cvt ').raw
79
- fpgm_table = TTFunk::Table::Simple.new(original, 'fpgm').raw
80
- prep_table = TTFunk::Table::Simple.new(original, 'prep').raw
81
-
82
- # for PDF's, the kerning info is all included in the PDF as the text is
83
- # drawn. Thus, the PDF readers do not actually use the kerning info in
84
- # embedded fonts. If the library is used for something else, the
85
- # generated subfont may need a kerning table... in that case, you need
86
- # to opt into it.
87
- if options[:kerning]
88
- kern_table =
89
- TTFunk::Table::Kern.encode(original.kerning, old2new_glyph)
90
- end
52
+ def glyphs
53
+ @glyphs ||= collect_glyphs(original_glyph_ids)
54
+ end
91
55
 
92
- tables = { 'cmap' => cmap_table[:table],
93
- 'glyf' => glyf_table[:table],
94
- 'loca' => loca_table[:table],
95
- 'kern' => kern_table,
96
- 'hmtx' => hmtx_table[:table],
97
- 'hhea' => hhea_table,
98
- 'maxp' => maxp_table,
99
- 'OS/2' => os2_table,
100
- 'post' => post_table,
101
- 'name' => name_table,
102
- 'head' => head_table,
103
- 'prep' => prep_table,
104
- 'fpgm' => fpgm_table,
105
- 'cvt ' => cvt_table }
106
-
107
- tables.delete_if { |_tag, table| table.nil? }
108
-
109
- search_range = (Math.log(tables.length) / Math.log(2)).to_i * 16
110
- entry_selector = (Math.log(search_range) / Math.log(2)).to_i
111
- range_shift = tables.length * 16 - search_range
112
-
113
- newfont = [
114
- original.directory.scaler_type,
115
- tables.length,
116
- search_range,
117
- entry_selector,
118
- range_shift
119
- ].pack('Nn*')
120
-
121
- directory_size = tables.length * 16
122
- offset = newfont.length + directory_size
123
-
124
- table_data = ''
125
- head_offset = nil
126
- tables.each do |tag, data|
127
- newfont << [tag, checksum(data), offset, data.length].pack('A4N*')
128
- table_data << data
129
- head_offset = offset if tag == 'head'
130
- offset += data.length
131
- while offset % 4 != 0
132
- offset += 1
133
- table_data << "\0"
134
- end
56
+ def collect_glyphs(glyph_ids)
57
+ collected = glyph_ids.each_with_object({}) do |id, h|
58
+ h[id] = glyph_for(id)
135
59
  end
136
60
 
137
- newfont << table_data
138
- sum = checksum(newfont)
139
- adjustment = 0xB1B0AFBA - sum
140
- newfont[head_offset + 8, 4] = [adjustment].pack('N')
61
+ additional_ids = collected.values
62
+ .select { |g| g && g.compound? }
63
+ .map(&:glyph_ids)
64
+ .flatten
65
+
66
+ collected.update(collect_glyphs(additional_ids)) if additional_ids.any?
141
67
 
142
- newfont
68
+ collected
143
69
  end
144
70
 
145
- private
71
+ def old_to_new_glyph
72
+ @old_to_new_glyph ||= begin
73
+ charmap = new_cmap_table[:charmap]
74
+ old_to_new = charmap.each_with_object(0 => 0) do |(_, ids), map|
75
+ map[ids[:old]] = ids[:new]
76
+ end
146
77
 
147
- def unicode_cmap
148
- @unicode_cmap ||= @original.cmap.unicode.first
149
- end
78
+ next_glyph_id = new_cmap_table[:max_glyph_id]
150
79
 
151
- def checksum(data)
152
- data += "\0" * (4 - data.length % 4) unless data.length % 4 == 0
153
- data.unpack('N*').reduce(0, :+) & 0xFFFF_FFFF
154
- end
80
+ glyphs.keys.each do |old_id|
81
+ unless old_to_new.key?(old_id)
82
+ old_to_new[old_id] = next_glyph_id
83
+ next_glyph_id += 1
84
+ end
85
+ end
155
86
 
156
- def collect_glyphs(glyph_ids)
157
- glyphs = glyph_ids.each_with_object({}) do |id, h|
158
- h[id] = original.glyph_outlines.for(id)
87
+ old_to_new
159
88
  end
160
- additional_ids = glyphs.values.select { |g| g && g.compound? }
161
- .map(&:glyph_ids).flatten
89
+ end
162
90
 
163
- glyphs.update(collect_glyphs(additional_ids)) if additional_ids.any?
91
+ def new_to_old_glyph
92
+ @new_to_old_glyph ||= old_to_new_glyph.invert
93
+ end
164
94
 
165
- glyphs
95
+ private
96
+
97
+ def glyph_for(glyph_id)
98
+ if original.cff.exists?
99
+ original
100
+ .cff
101
+ .top_index[0]
102
+ .charstrings_index[glyph_id]
103
+ .glyph
104
+ else
105
+ original.glyph_outlines.for(glyph_id)
106
+ end
166
107
  end
167
108
  end
168
109
  end