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
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
  require 'digest/sha1'
3
5
 
4
6
  module TTFunk
5
7
  class Table
6
8
  class Name < Table
7
- class String < ::String
9
+ class NameString < ::String
8
10
  attr_reader :platform_id
9
11
  attr_reader :encoding_id
10
12
  attr_reader :language_id
@@ -23,6 +25,7 @@ module TTFunk
23
25
  end
24
26
  end
25
27
 
28
+ attr_reader :entries
26
29
  attr_reader :strings
27
30
 
28
31
  attr_reader :copyright
@@ -44,10 +47,30 @@ module TTFunk
44
47
  attr_reader :compatible_full
45
48
  attr_reader :sample_text
46
49
 
50
+ COPYRIGHT_NAME_ID = 0
51
+ FONT_FAMILY_NAME_ID = 1
52
+ FONT_SUBFAMILY_NAME_ID = 2
53
+ UNIQUE_SUBFAMILY_NAME_ID = 3
54
+ FONT_NAME_NAME_ID = 4
55
+ VERSION_NAME_ID = 5
56
+ POSTSCRIPT_NAME_NAME_ID = 6
57
+ TRADEMARK_NAME_ID = 7
58
+ MANUFACTURER_NAME_ID = 8
59
+ DESIGNER_NAME_ID = 9
60
+ DESCRIPTION_NAME_ID = 10
61
+ VENDOR_URL_NAME_ID = 11
62
+ DESIGNER_URL_NAME_ID = 12
63
+ LICENSE_NAME_ID = 13
64
+ LICENSE_URL_NAME_ID = 14
65
+ PREFERRED_FAMILY_NAME_ID = 16
66
+ PREFERRED_SUBFAMILY_NAME_ID = 17
67
+ COMPATIBLE_FULL_NAME_ID = 18
68
+ SAMPLE_TEXT_NAME_ID = 19
69
+
47
70
  def self.encode(names, key = '')
48
71
  tag = Digest::SHA1.hexdigest(key)[0, 6]
49
72
 
50
- postscript_name = Name::String.new(
73
+ postscript_name = NameString.new(
51
74
  "#{tag}+#{names.postscript_name}", 1, 0, 0
52
75
  )
53
76
 
@@ -56,23 +79,31 @@ module TTFunk
56
79
  str_count = strings.inject(0) { |sum, (_, list)| sum + list.length }
57
80
 
58
81
  table = [0, str_count, 6 + 12 * str_count].pack('n*')
59
- strtable = ''
82
+ strtable = +''
60
83
 
84
+ items = []
61
85
  strings.each do |id, list|
62
86
  list.each do |string|
63
- table << [
64
- string.platform_id, string.encoding_id, string.language_id, id,
65
- string.length, strtable.length
66
- ].pack('n*')
67
- strtable << string
87
+ items << [id, string]
68
88
  end
69
89
  end
90
+ items = items.sort_by do |id, string|
91
+ [string.platform_id, string.encoding_id, string.language_id, id]
92
+ end
93
+ items.each do |id, string|
94
+ table << [
95
+ string.platform_id, string.encoding_id, string.language_id, id,
96
+ string.length, strtable.length
97
+ ].pack('n*')
98
+ strtable << string
99
+ end
70
100
 
71
101
  table << strtable
72
102
  end
73
103
 
74
104
  def postscript_name
75
105
  return @postscript_name if @postscript_name
106
+
76
107
  font_family.first || 'unnamed'
77
108
  end
78
109
 
@@ -81,53 +112,60 @@ module TTFunk
81
112
  def parse!
82
113
  count, string_offset = read(6, 'x2n*')
83
114
 
84
- entries = []
115
+ @entries = []
85
116
  count.times do
86
117
  platform, encoding, language, id, length, start_offset =
87
118
  read(12, 'n*')
88
- entries << {
119
+ @entries << {
89
120
  platform_id: platform,
90
121
  encoding_id: encoding,
91
122
  language_id: language,
92
123
  name_id: id,
93
124
  length: length,
94
- offset: offset + string_offset + start_offset
125
+ offset: offset + string_offset + start_offset,
126
+ text: nil
95
127
  }
96
128
  end
97
129
 
98
130
  @strings = Hash.new { |h, k| h[k] = [] }
99
131
 
100
132
  count.times do |i|
101
- io.pos = entries[i][:offset]
102
- text = io.read(entries[i][:length])
103
- @strings[entries[i][:name_id]] << Name::String.new(
104
- text,
105
- entries[i][:platform_id],
106
- entries[i][:encoding_id],
107
- entries[i][:language_id]
133
+ io.pos = @entries[i][:offset]
134
+ @entries[i][:text] = io.read(@entries[i][:length])
135
+ @strings[@entries[i][:name_id]] << NameString.new(
136
+ @entries[i][:text] || '',
137
+ @entries[i][:platform_id],
138
+ @entries[i][:encoding_id],
139
+ @entries[i][:language_id]
108
140
  )
109
141
  end
110
142
 
111
- @copyright = @strings[0]
112
- @font_family = @strings[1]
113
- @font_subfamily = @strings[2]
114
- @unique_subfamily = @strings[3]
115
- @font_name = @strings[4]
116
- @version = @strings[5]
117
143
  # should only be ONE postscript name
118
- @postscript_name = @strings[6].first.strip_extended
119
- @trademark = @strings[7]
120
- @manufacturer = @strings[8]
121
- @designer = @strings[9]
122
- @description = @strings[10]
123
- @vendor_url = @strings[11]
124
- @designer_url = @strings[12]
125
- @license = @strings[13]
126
- @license_url = @strings[14]
127
- @preferred_family = @strings[16]
128
- @preferred_subfamily = @strings[17]
129
- @compatible_full = @strings[18]
130
- @sample_text = @strings[19]
144
+
145
+ @copyright = @strings[COPYRIGHT_NAME_ID]
146
+ @font_family = @strings[FONT_FAMILY_NAME_ID]
147
+ @font_subfamily = @strings[FONT_SUBFAMILY_NAME_ID]
148
+ @unique_subfamily = @strings[UNIQUE_SUBFAMILY_NAME_ID]
149
+ @font_name = @strings[FONT_NAME_NAME_ID]
150
+ @version = @strings[VERSION_NAME_ID]
151
+
152
+ unless @strings[POSTSCRIPT_NAME_NAME_ID].empty?
153
+ @postscript_name = @strings[POSTSCRIPT_NAME_NAME_ID]
154
+ .first.strip_extended
155
+ end
156
+
157
+ @trademark = @strings[TRADEMARK_NAME_ID]
158
+ @manufacturer = @strings[MANUFACTURER_NAME_ID]
159
+ @designer = @strings[DESIGNER_NAME_ID]
160
+ @description = @strings[DESCRIPTION_NAME_ID]
161
+ @vendor_url = @strings[VENDOR_URL_NAME_ID]
162
+ @designer_url = @strings[DESIGNER_URL_NAME_ID]
163
+ @license = @strings[LICENSE_NAME_ID]
164
+ @license_url = @strings[LICENSE_URL_NAME_ID]
165
+ @preferred_family = @strings[PREFERRED_FAMILY_NAME_ID]
166
+ @preferred_subfamily = @strings[PREFERRED_SUBFAMILY_NAME_ID]
167
+ @compatible_full = @strings[COMPATIBLE_FULL_NAME_ID]
168
+ @sample_text = @strings[SAMPLE_TEXT_NAME_ID]
131
169
  end
132
170
  end
133
171
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
4
+ require 'set'
2
5
 
3
6
  module TTFunk
4
7
  class Table
@@ -40,16 +43,327 @@ module TTFunk
40
43
  attr_reader :break_char
41
44
  attr_reader :max_context
42
45
 
46
+ CODE_PAGE_BITS = {
47
+ 1252 => 0, 1250 => 1, 1251 => 2, 1253 => 3, 1254 => 4,
48
+ 1255 => 5, 1256 => 6, 1257 => 7, 1258 => 8, 874 => 16,
49
+ 932 => 17, 936 => 18, 949 => 19, 950 => 20, 1361 => 21,
50
+ 10_000 => 29, 869 => 48, 866 => 49, 865 => 50, 864 => 51,
51
+ 863 => 52, 862 => 53, 861 => 54, 860 => 55, 857 => 56,
52
+ 855 => 57, 852 => 58, 775 => 59, 737 => 60, 708 => 61,
53
+ 850 => 62, 437 => 63
54
+ }.freeze
55
+
56
+ UNICODE_BLOCKS = {
57
+ (0x0000..0x007F) => 0, (0x0080..0x00FF) => 1,
58
+ (0x0100..0x017F) => 2, (0x0180..0x024F) => 3,
59
+ (0x0250..0x02AF) => 4, (0x1D00..0x1D7F) => 4,
60
+ (0x1D80..0x1DBF) => 4, (0x02B0..0x02FF) => 5,
61
+ (0xA700..0xA71F) => 5, (0x0300..0x036F) => 6,
62
+ (0x1DC0..0x1DFF) => 6, (0x0370..0x03FF) => 7,
63
+ (0x2C80..0x2CFF) => 8, (0x0400..0x04FF) => 9,
64
+ (0x0500..0x052F) => 9, (0x2DE0..0x2DFF) => 9,
65
+ (0xA640..0xA69F) => 9, (0x0530..0x058F) => 10,
66
+ (0x0590..0x05FF) => 11, (0xA500..0xA63F) => 12,
67
+ (0x0600..0x06FF) => 13, (0x0750..0x077F) => 13,
68
+ (0x07C0..0x07FF) => 14, (0x0900..0x097F) => 15,
69
+ (0x0980..0x09FF) => 16, (0x0A00..0x0A7F) => 17,
70
+ (0x0A80..0x0AFF) => 18, (0x0B00..0x0B7F) => 19,
71
+ (0x0B80..0x0BFF) => 20, (0x0C00..0x0C7F) => 21,
72
+ (0x0C80..0x0CFF) => 22, (0x0D00..0x0D7F) => 23,
73
+ (0x0E00..0x0E7F) => 24, (0x0E80..0x0EFF) => 25,
74
+ (0x10A0..0x10FF) => 26, (0x2D00..0x2D2F) => 26,
75
+ (0x1B00..0x1B7F) => 27, (0x1100..0x11FF) => 28,
76
+ (0x1E00..0x1EFF) => 29, (0x2C60..0x2C7F) => 29,
77
+ (0xA720..0xA7FF) => 29, (0x1F00..0x1FFF) => 30,
78
+ (0x2000..0x206F) => 31, (0x2E00..0x2E7F) => 31,
79
+ (0x2070..0x209F) => 32, (0x20A0..0x20CF) => 33,
80
+ (0x20D0..0x20FF) => 34, (0x2100..0x214F) => 35,
81
+ (0x2150..0x218F) => 36, (0x2190..0x21FF) => 37,
82
+ (0x27F0..0x27FF) => 37, (0x2900..0x297F) => 37,
83
+ (0x2B00..0x2BFF) => 37, (0x2200..0x22FF) => 38,
84
+ (0x2A00..0x2AFF) => 38, (0x27C0..0x27EF) => 38,
85
+ (0x2980..0x29FF) => 38, (0x2300..0x23FF) => 39,
86
+ (0x2400..0x243F) => 40, (0x2440..0x245F) => 41,
87
+ (0x2460..0x24FF) => 42, (0x2500..0x257F) => 43,
88
+ (0x2580..0x259F) => 44, (0x25A0..0x25FF) => 45,
89
+ (0x2600..0x26FF) => 46, (0x2700..0x27BF) => 47,
90
+ (0x3000..0x303F) => 48, (0x3040..0x309F) => 49,
91
+ (0x30A0..0x30FF) => 50, (0x31F0..0x31FF) => 50,
92
+ (0x3100..0x312F) => 51, (0x31A0..0x31BF) => 51,
93
+ (0x3130..0x318F) => 52, (0xA840..0xA87F) => 53,
94
+ (0x3200..0x32FF) => 54, (0x3300..0x33FF) => 55,
95
+ (0xAC00..0xD7AF) => 56, (0xD800..0xDFFF) => 57,
96
+ (0x10900..0x1091F) => 58, (0x4E00..0x9FFF) => 59,
97
+ (0x2E80..0x2EFF) => 59, (0x2F00..0x2FDF) => 59,
98
+ (0x2FF0..0x2FFF) => 59, (0x3400..0x4DBF) => 59,
99
+ (0x20000..0x2A6DF) => 59, (0x3190..0x319F) => 59,
100
+ (0xE000..0xF8FF) => 60, (0x31C0..0x31EF) => 61,
101
+ (0xF900..0xFAFF) => 61, (0x2F800..0x2FA1F) => 61,
102
+ (0xFB00..0xFB4F) => 62, (0xFB50..0xFDFF) => 63,
103
+ (0xFE20..0xFE2F) => 64, (0xFE10..0xFE1F) => 65,
104
+ (0xFE30..0xFE4F) => 65, (0xFE50..0xFE6F) => 66,
105
+ (0xFE70..0xFEFF) => 67, (0xFF00..0xFFEF) => 68,
106
+ (0xFFF0..0xFFFF) => 69, (0x0F00..0x0FFF) => 70,
107
+ (0x0700..0x074F) => 71, (0x0780..0x07BF) => 72,
108
+ (0x0D80..0x0DFF) => 73, (0x1000..0x109F) => 74,
109
+ (0x1200..0x137F) => 75, (0x1380..0x139F) => 75,
110
+ (0x2D80..0x2DDF) => 75, (0x13A0..0x13FF) => 76,
111
+ (0x1400..0x167F) => 77, (0x1680..0x169F) => 78,
112
+ (0x16A0..0x16FF) => 79, (0x1780..0x17FF) => 80,
113
+ (0x19E0..0x19FF) => 80, (0x1800..0x18AF) => 81,
114
+ (0x2800..0x28FF) => 82, (0xA000..0xA48F) => 83,
115
+ (0xA490..0xA4CF) => 83, (0x1700..0x171F) => 84,
116
+ (0x1720..0x173F) => 84, (0x1740..0x175F) => 84,
117
+ (0x1760..0x177F) => 84, (0x10300..0x1032F) => 85,
118
+ (0x10330..0x1034F) => 86, (0x10400..0x1044F) => 87,
119
+ (0x1D000..0x1D0FF) => 88, (0x1D100..0x1D1FF) => 88,
120
+ (0x1D200..0x1D24F) => 88, (0x1D400..0x1D7FF) => 89,
121
+ (0xF0000..0xFFFFD) => 90, (0x100000..0x10FFFD) => 90,
122
+ (0xFE00..0xFE0F) => 91, (0xE0100..0xE01EF) => 91,
123
+ (0xE0000..0xE007F) => 92, (0x1900..0x194F) => 93,
124
+ (0x1950..0x197F) => 94, (0x1980..0x19DF) => 95,
125
+ (0x1A00..0x1A1F) => 96, (0x2C00..0x2C5F) => 97,
126
+ (0x2D30..0x2D7F) => 98, (0x4DC0..0x4DFF) => 99,
127
+ (0xA800..0xA82F) => 100, (0x10000..0x1007F) => 101,
128
+ (0x10080..0x100FF) => 101, (0x10100..0x1013F) => 101,
129
+ (0x10140..0x1018F) => 102, (0x10380..0x1039F) => 103,
130
+ (0x103A0..0x103DF) => 104, (0x10450..0x1047F) => 105,
131
+ (0x10480..0x104AF) => 106, (0x10800..0x1083F) => 107,
132
+ (0x10A00..0x10A5F) => 108, (0x1D300..0x1D35F) => 109,
133
+ (0x12000..0x123FF) => 110, (0x12400..0x1247F) => 110,
134
+ (0x1D360..0x1D37F) => 111, (0x1B80..0x1BBF) => 112,
135
+ (0x1C00..0x1C4F) => 113, (0x1C50..0x1C7F) => 114,
136
+ (0xA880..0xA8DF) => 115, (0xA900..0xA92F) => 116,
137
+ (0xA930..0xA95F) => 117, (0xAA00..0xAA5F) => 118,
138
+ (0x10190..0x101CF) => 119, (0x101D0..0x101FF) => 120,
139
+ (0x102A0..0x102DF) => 121, (0x10280..0x1029F) => 121,
140
+ (0x10920..0x1093F) => 121, (0x1F030..0x1F09F) => 122,
141
+ (0x1F000..0x1F02F) => 122
142
+ }.freeze
143
+
144
+ UNICODE_MAX = 0xFFFF
145
+ UNICODE_RANGES = UNICODE_BLOCKS.keys.freeze
146
+ LOWERCASE_START = 'a'.ord
147
+ LOWERCASE_END = 'z'.ord
148
+ LOWERCASE_COUNT = (LOWERCASE_END - LOWERCASE_START) + 1
149
+ CODEPOINT_SPACE = 32
150
+ SPACE_GLYPH_MISSING_ERROR = "Space glyph (0x#{CODEPOINT_SPACE.to_s(16)})"\
151
+ ' must be included in the font'
152
+
153
+ # Used to calculate the xAvgCharWidth field.
154
+ # From https://docs.microsoft.com/en-us/typography/opentype/spec/os2:
155
+ #
156
+ # "When first defined, the specification was biased toward Basic Latin
157
+ # characters, and it was thought that the xAvgCharWidth value could be
158
+ # used to estimate the average length of lines of text. A formula for
159
+ # calculating xAvgCharWidth was provided using frequency-of-use
160
+ # weighting factors for lowercase letters a - z."
161
+ #
162
+ # The array below contains 26 weight values which correspond to the
163
+ # 26 letters in the Latin alphabet. Each weight is the relative
164
+ # frequency of that letter in the English language.
165
+ WEIGHT_SPACE = 166
166
+ WEIGHT_LOWERCASE = [
167
+ 64, 14, 27, 35, 100, 20, 14, 42, 63, 3, 6, 35, 20,
168
+ 56, 56, 17, 4, 49, 56, 71, 31, 10, 18, 3, 18, 2
169
+ ].freeze
170
+
43
171
  def tag
44
172
  'OS/2'
45
173
  end
46
174
 
175
+ class << self
176
+ def encode(os2, subset)
177
+ ''.b.tap do |result|
178
+ result << [
179
+ os2.version, avg_char_width_for(os2, subset), os2.weight_class,
180
+ os2.width_class, os2.type, os2.y_subscript_x_size,
181
+ os2.y_subscript_y_size, os2.y_subscript_x_offset,
182
+ os2.y_subscript_y_offset, os2.y_superscript_x_size,
183
+ os2.y_superscript_y_size, os2.y_superscript_x_offset,
184
+ os2.y_superscript_y_offset, os2.y_strikeout_size,
185
+ os2.y_strikeout_position, os2.family_class
186
+ ].pack('n*')
187
+
188
+ result << os2.panose
189
+
190
+ new_char_range = unicode_blocks_for(os2, os2.char_range, subset)
191
+ result << BinUtils
192
+ .slice_int(
193
+ new_char_range.value,
194
+ bit_width: 32,
195
+ slice_count: 4
196
+ )
197
+ .pack('N*')
198
+
199
+ result << os2.vendor_id
200
+
201
+ new_cmap_table = subset.new_cmap_table[:charmap]
202
+ code_points = new_cmap_table
203
+ .keys
204
+ .select { |k| new_cmap_table[k][:new] > 0 }
205
+ .sort
206
+
207
+ # "This value depends on which character sets the font supports.
208
+ # This field cannot represent supplementary character values
209
+ # (codepoints greater than 0xFFFF). Fonts that support
210
+ # supplementary characters should set the value in this field
211
+ # to 0xFFFF."
212
+ first_char_index = [code_points.first || 0, UNICODE_MAX].min
213
+ last_char_index = [code_points.last || 0, UNICODE_MAX].min
214
+
215
+ result << [
216
+ os2.selection, first_char_index, last_char_index
217
+ ].pack('n*')
218
+
219
+ if os2.version > 0
220
+ result << [
221
+ os2.ascent, os2.descent, os2.line_gap,
222
+ os2.win_ascent, os2.win_descent
223
+ ].pack('n*')
224
+
225
+ result << BinUtils
226
+ .slice_int(
227
+ code_pages_for(subset).value,
228
+ bit_width: 32,
229
+ slice_count: 2
230
+ )
231
+ .pack('N*')
232
+
233
+ if os2.version > 1
234
+ result << [
235
+ os2.x_height, os2.cap_height, os2.default_char,
236
+ os2.break_char, os2.max_context
237
+ ].pack('n*')
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ private
244
+
245
+ def code_pages_for(subset)
246
+ field = BitField.new(0)
247
+ return field if subset.unicode?
248
+
249
+ field.on(CODE_PAGE_BITS[subset.code_page])
250
+ field
251
+ end
252
+
253
+ def unicode_blocks_for(os2, original_field, subset)
254
+ field = BitField.new(0)
255
+ return field unless subset.unicode?
256
+
257
+ subset_code_points = Set.new(subset.new_cmap_table[:charmap].keys)
258
+ original_code_point_groups = group_original_code_points_by_bit(os2)
259
+
260
+ original_code_point_groups.each do |bit, code_points|
261
+ next if original_field.off?(bit)
262
+
263
+ if code_points.any? { |cp| subset_code_points.include?(cp) }
264
+ field.on(bit)
265
+ end
266
+ end
267
+
268
+ field
269
+ end
270
+
271
+ def group_original_code_points_by_bit(os2)
272
+ Hash.new { |h, k| h[k] = [] }.tap do |result|
273
+ os2.file.cmap.unicode.first.code_map.each_key do |code_point|
274
+ # find corresponding bit
275
+ range = UNICODE_RANGES.find { |r| r.cover?(code_point) }
276
+
277
+ if (bit = UNICODE_BLOCKS[range])
278
+ result[bit] << code_point
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ def avg_char_width_for(os2, subset)
285
+ if subset.microsoft_symbol?
286
+ avg_ms_symbol_char_width_for(os2, subset)
287
+ else
288
+ avg_weighted_char_width_for(os2, subset)
289
+ end
290
+ end
291
+
292
+ def avg_ms_symbol_char_width_for(os2, subset)
293
+ total_width = 0
294
+ num_glyphs = 0
295
+
296
+ # use new -> old glyph mapping in order to include compound glyphs
297
+ # in the calculation
298
+ subset.new_to_old_glyph.each do |_, old_gid|
299
+ if (metric = os2.file.horizontal_metrics.for(old_gid))
300
+ total_width += metric.advance_width
301
+ num_glyphs += 1 if metric.advance_width > 0
302
+ end
303
+ end
304
+
305
+ return 0 if num_glyphs == 0
306
+
307
+ total_width / num_glyphs # this should be a whole number
308
+ end
309
+
310
+ def avg_weighted_char_width_for(os2, subset)
311
+ # make sure the subset includes the space char
312
+ unless subset.to_unicode_map[CODEPOINT_SPACE]
313
+ raise SPACE_GLYPH_MISSING_ERROR
314
+ end
315
+
316
+ space_gid = os2.file.cmap.unicode.first[CODEPOINT_SPACE]
317
+ space_hm = os2.file.horizontal_metrics.for(space_gid)
318
+ return 0 unless space_hm
319
+
320
+ total_weight = space_hm.advance_width * WEIGHT_SPACE
321
+ num_lowercase = 0
322
+
323
+ # calculate the weighted sum of all the lowercase widths in
324
+ # the subset
325
+ LOWERCASE_START.upto(LOWERCASE_END) do |lowercase_cp|
326
+ # make sure the subset includes the character
327
+ next unless subset.to_unicode_map[lowercase_cp]
328
+
329
+ lowercase_gid = os2.file.cmap.unicode.first[lowercase_cp]
330
+ lowercase_hm = os2.file.horizontal_metrics.for(lowercase_gid)
331
+
332
+ num_lowercase += 1
333
+ total_weight += lowercase_hm.advance_width *
334
+ WEIGHT_LOWERCASE[lowercase_cp - 'a'.ord]
335
+ end
336
+
337
+ # return if all lowercase characters are present in the subset
338
+ return total_weight / 1000 if num_lowercase == LOWERCASE_COUNT
339
+
340
+ # If not all lowercase characters are present in the subset, take
341
+ # the average width of all the subsetted characters. This differs
342
+ # from avg_ms_char_width_for in that it includes zero-width glyphs
343
+ # in the calculation.
344
+ total_width = 0
345
+ num_glyphs = subset.new_to_old_glyph.size
346
+
347
+ # use new -> old glyph mapping in order to include compound glyphs
348
+ # in the calculation
349
+ subset.new_to_old_glyph.each do |_, old_gid|
350
+ if (metric = os2.file.horizontal_metrics.for(old_gid))
351
+ total_width += metric.advance_width
352
+ end
353
+ end
354
+
355
+ return 0 if num_glyphs == 0
356
+
357
+ total_width / num_glyphs # this should be a whole number
358
+ end
359
+ end
360
+
47
361
  private
48
362
 
49
363
  def parse!
50
364
  @version = read(2, 'n').first
51
365
 
52
- @ave_char_width = read_signed(1)
366
+ @ave_char_width = read_signed(1).first
53
367
  @weight_class, @width_class = read(4, 'nn')
54
368
  @type, @y_subscript_x_size, @y_subscript_y_size, @y_subscript_x_offset,
55
369
  @y_subscript_y_offset, @y_superscript_x_size, @y_superscript_y_size,
@@ -57,19 +371,28 @@ module TTFunk
57
371
  @y_strikeout_position, @family_class = read_signed(12)
58
372
  @panose = io.read(10)
59
373
 
60
- @char_range = io.read(16)
61
- @vendor_id = io.read(4)
374
+ @char_range = BitField.new(
375
+ BinUtils.stitch_int(read(16, 'N*'), bit_width: 32)
376
+ )
62
377
 
378
+ @vendor_id = io.read(4)
63
379
  @selection, @first_char_index, @last_char_index = read(6, 'n*')
64
380
 
65
381
  if @version > 0
66
382
  @ascent, @descent, @line_gap = read_signed(3)
67
383
  @win_ascent, @win_descent = read(4, 'nn')
68
- @code_page_range = io.read(8)
384
+ @code_page_range = BitField.new(
385
+ BinUtils.stitch_int(read(8, 'N*'), bit_width: 32)
386
+ )
69
387
 
70
388
  if @version > 1
71
389
  @x_height, @cap_height = read_signed(2)
72
390
  @default_char, @break_char, @max_context = read(6, 'nnn')
391
+
392
+ # Set this to zero until GSUB/GPOS support has been implemented.
393
+ # This value is calculated via those tables, and should be set to
394
+ # zero if the data is not available.
395
+ @max_context = 0
73
396
  end
74
397
  end
75
398
  end