ttfunk 1.5.1 → 1.6.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 +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