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
@@ -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