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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class Table
5
+ class Cff < TTFunk::Table
6
+ class CharstringsIndex < TTFunk::Table::Cff::Index
7
+ attr_reader :top_dict
8
+
9
+ def initialize(top_dict, *remaining_args)
10
+ super(*remaining_args)
11
+ @top_dict = top_dict
12
+ end
13
+
14
+ def [](index)
15
+ entry_cache[index] ||= TTFunk::Table::Cff::Charstring.new(
16
+ index, top_dict, font_dict_for(index), super
17
+ )
18
+ end
19
+
20
+ # gets passed a mapping of new => old glyph ids
21
+ def encode(mapping)
22
+ super() do |_entry, index|
23
+ self[mapping[index]].encode if mapping.include?(index)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def font_dict_for(index)
30
+ # only CID-keyed fonts contain an FD selector and font dicts
31
+ if top_dict.is_cid_font?
32
+ fd_index = top_dict.font_dict_selector[index]
33
+ top_dict.font_index[fd_index]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module TTFunk
6
+ class Table
7
+ class Cff < TTFunk::Table
8
+ class Dict < TTFunk::SubTable
9
+ class InvalidOperandError < StandardError; end
10
+ class TooManyOperandsError < StandardError; end
11
+
12
+ # for regular single-byte operators
13
+ OPERATOR_BZERO = (0..21).freeze
14
+ OPERAND_BZERO = [28..30, 32..254].freeze
15
+
16
+ # for operators that are two bytes wide
17
+ WIDE_OPERATOR_BZERO = 12
18
+ WIDE_OPERATOR_ADJUSTMENT = 1200
19
+
20
+ # maximum number of operands allowed per operator
21
+ MAX_OPERANDS = 48
22
+
23
+ # used to validate operands expressed in scientific notation
24
+ VALID_SCI_SIGNIFICAND_RE = /\A-?(\.\d+|\d+|\d+\.\d+)\z/.freeze
25
+ VALID_SCI_EXPONENT_RE = /\A-?\d+\z/.freeze
26
+
27
+ include Enumerable
28
+
29
+ def [](operator)
30
+ @dict[operator]
31
+ end
32
+
33
+ def each(&block)
34
+ @dict.each(&block)
35
+ end
36
+
37
+ alias each_pair each
38
+
39
+ def encode
40
+ map do |(operator, operands)|
41
+ operands.map { |operand| encode_operand(operand) }.join +
42
+ encode_operator(operator)
43
+ end.join
44
+ end
45
+
46
+ private
47
+
48
+ def encode_operator(operator)
49
+ if operator >= WIDE_OPERATOR_ADJUSTMENT
50
+ [
51
+ WIDE_OPERATOR_BZERO,
52
+ operator - WIDE_OPERATOR_ADJUSTMENT
53
+ ].pack('C*')
54
+ else
55
+ [operator].pack('C')
56
+ end
57
+ end
58
+
59
+ def encode_operand(operand)
60
+ case operand
61
+ when Integer
62
+ encode_integer(operand)
63
+ when Float, BigDecimal
64
+ encode_float(operand)
65
+ when SciForm
66
+ encode_sci(operand)
67
+ end
68
+ end
69
+
70
+ def encode_integer(int)
71
+ case int
72
+ when -107..107
73
+ [int + 139].pack('C')
74
+
75
+ when 108..1131
76
+ int -= 108
77
+ [(int >> 8) + 247, int & 0xFF].pack('C*')
78
+
79
+ when -1131..-108
80
+ int = -int - 108
81
+ [(int >> 8) + 251, int & 0xFF].pack('C*')
82
+
83
+ when -32_768..32_767
84
+ [28, (int >> 8) & 0xFF, int & 0xFF].pack('C*')
85
+
86
+ else
87
+ encode_integer32(int)
88
+ end
89
+ end
90
+
91
+ def encode_integer32(int)
92
+ [29, int].pack('CN')
93
+ end
94
+
95
+ def encode_float(float)
96
+ pack_decimal_nibbles(encode_significand(float))
97
+ end
98
+
99
+ def encode_sci(sci)
100
+ sig_bytes = encode_significand(sci.significand)
101
+ exp_bytes = encode_exponent(sci.exponent)
102
+ pack_decimal_nibbles(sig_bytes + exp_bytes)
103
+ end
104
+
105
+ def encode_exponent(exp)
106
+ return [] if exp == 0
107
+
108
+ [exp > 0 ? 0xB : 0xC, *encode_significand(exp.abs)]
109
+ end
110
+
111
+ def encode_significand(sig)
112
+ sig.to_s.each_char.with_object([]) do |char, ret|
113
+ case char
114
+ when '0'..'9'
115
+ ret << char.to_i
116
+ when '.'
117
+ ret << 0xA
118
+ when '-'
119
+ ret << 0xE
120
+ else
121
+ break ret
122
+ end
123
+ end
124
+ end
125
+
126
+ def pack_decimal_nibbles(nibbles)
127
+ bytes = [30]
128
+
129
+ nibbles.each_slice(2).each do |(high_nb, low_nb)|
130
+ # low_nb can be nil if nibbles contains an odd number of elements
131
+ low_nb ||= 0xF
132
+ bytes << (high_nb << 4 | low_nb)
133
+ end
134
+
135
+ bytes << 0xFF if nibbles.size.even?
136
+ bytes.pack('C*')
137
+ end
138
+
139
+ def parse!
140
+ @dict = {}
141
+ operands = []
142
+
143
+ # @length must be set via the constructor
144
+ while io.pos < table_offset + length
145
+ case b_zero = read(1, 'C').first
146
+ when WIDE_OPERATOR_BZERO
147
+ operator = decode_wide_operator
148
+ @dict[operator] = operands
149
+ operands = []
150
+ when OPERATOR_BZERO
151
+ @dict[b_zero] = operands unless operands.empty?
152
+ operands = []
153
+ when *OPERAND_BZERO
154
+ operands << decode_operand(b_zero)
155
+
156
+ if operands.size > MAX_OPERANDS
157
+ raise TooManyOperandsError, 'found one too many operands at '\
158
+ "position #{io.pos} in dict at position #{table_offset}"
159
+ end
160
+ else
161
+ raise "dict byte value #{b_zero} is reserved"
162
+ end
163
+ end
164
+ end
165
+
166
+ def decode_wide_operator
167
+ WIDE_OPERATOR_ADJUSTMENT + read(1, 'C').first
168
+ end
169
+
170
+ def decode_operand(b_zero)
171
+ case b_zero
172
+ when 30
173
+ decode_sci
174
+ else
175
+ decode_integer(b_zero)
176
+ end
177
+ end
178
+
179
+ def decode_sci
180
+ significand = ''.b
181
+ exponent = ''.b
182
+
183
+ loop do
184
+ current = read(1, 'C').first
185
+ break if current == 0xFF
186
+
187
+ high_nibble = current >> 4
188
+ low_nibble = current & 0x0F # 0b00001111
189
+
190
+ [high_nibble, low_nibble].each do |nibble|
191
+ case nibble
192
+ when 0..9
193
+ (exponent.empty? ? significand : exponent) << nibble.to_s
194
+ when 0xA
195
+ significand << '.'
196
+ when 0xB
197
+ # take advantage of Integer#to_i not caring about whitespace
198
+ exponent << ' '
199
+ when 0xC
200
+ exponent << '-'
201
+ when 0xE
202
+ significand << '-'
203
+ end
204
+ end
205
+
206
+ break if low_nibble == 0xF
207
+ end
208
+
209
+ validate_sci!(significand, exponent)
210
+
211
+ SciForm.new(significand.to_f, exponent.to_i)
212
+ end
213
+
214
+ def validate_sci!(significand, exponent)
215
+ unless valid_significand?(significand) && valid_exponent?(exponent)
216
+ raise InvalidOperandError,
217
+ 'invalid scientific notation operand with significand '\
218
+ "'#{significand}' and exponent '#{exponent}' ending at "\
219
+ "position #{io.pos} in dict at position #{table_offset}"
220
+ end
221
+ end
222
+
223
+ def valid_significand?(significand)
224
+ !(significand.strip =~ VALID_SCI_SIGNIFICAND_RE).nil?
225
+ end
226
+
227
+ def valid_exponent?(exponent)
228
+ exponent = exponent.strip
229
+ return true if exponent.empty?
230
+
231
+ !(exponent.strip =~ VALID_SCI_EXPONENT_RE).nil?
232
+ end
233
+
234
+ def decode_integer(b_zero)
235
+ case b_zero
236
+ when 32..246
237
+ # 1 byte
238
+ b_zero - 139
239
+
240
+ when 247..250
241
+ # 2 bytes
242
+ b_one = read(1, 'C').first
243
+ (b_zero - 247) * 256 + b_one + 108
244
+
245
+ when 251..254
246
+ # 2 bytes
247
+ b_one = read(1, 'C').first
248
+ -(b_zero - 251) * 256 - b_one - 108
249
+
250
+ when 28
251
+ # 2 bytes in number (3 total)
252
+ b_one, b_two = read(2, 'C*')
253
+ BinUtils.twos_comp_to_int(b_one << 8 | b_two, bit_width: 16)
254
+
255
+ when 29
256
+ # 4 bytes in number (5 total)
257
+ b_one, b_two, b_three, b_four = read(4, 'C*')
258
+ BinUtils.twos_comp_to_int(
259
+ b_one << 24 | b_two << 16 | b_three << 8 | b_four, bit_width: 32
260
+ )
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class Table
5
+ class Cff < TTFunk::Table
6
+ class Encoding < TTFunk::SubTable
7
+ include Enumerable
8
+
9
+ STANDARD_ENCODING_ID = 0
10
+ EXPERT_ENCODING_ID = 1
11
+
12
+ DEFAULT_ENCODING_ID = STANDARD_ENCODING_ID
13
+
14
+ class << self
15
+ def codes_for_encoding_id(encoding_id)
16
+ case encoding_id
17
+ when STANDARD_ENCODING_ID
18
+ Encodings::STANDARD
19
+ when EXPERT_ENCODING_ID
20
+ Encodings::EXPERT
21
+ end
22
+ end
23
+ end
24
+
25
+ attr_reader :top_dict, :format, :count, :offset_or_id
26
+
27
+ def initialize(top_dict, file, offset_or_id = nil, length = nil)
28
+ @top_dict = top_dict
29
+ @offset_or_id = offset_or_id || DEFAULT_ENCODING_ID
30
+
31
+ if offset
32
+ super(file, offset, length)
33
+ else
34
+ @count = self.class.codes_for_encoding_id(offset_or_id).size
35
+ end
36
+ end
37
+
38
+ def each
39
+ return to_enum(__method__) unless block_given?
40
+
41
+ # +1 adjusts for the implicit .notdef glyph
42
+ (count + 1).times { |i| yield self[i] }
43
+ end
44
+
45
+ def [](glyph_id)
46
+ return 0 if glyph_id == 0
47
+ return code_for(glyph_id) if offset
48
+
49
+ self.class.codes_for_encoding_id(offset_or_id)[glyph_id]
50
+ end
51
+
52
+ def offset
53
+ # Numbers from 0..1 mean encoding IDs instead of offsets. IDs are
54
+ # pre-defined, generic encodings that define the characters present
55
+ # in the font.
56
+ #
57
+ # In the case of an offset, add the CFF table's offset since the
58
+ # charset offset is relative to the start of the CFF table. Otherwise
59
+ # return nil (no offset).
60
+ if offset_or_id > 1
61
+ offset_or_id + top_dict.cff_offset
62
+ end
63
+ end
64
+
65
+ def encode(new_to_old, old_to_new)
66
+ # no offset means no encoding was specified (i.e. we're supposed to
67
+ # use a predefined encoding) so there's nothing to encode
68
+ return '' unless offset
69
+ return encode_supplemental(new_to_old, old_to_new) if supplemental?
70
+
71
+ codes = new_to_old.keys.sort.map do |new_gid|
72
+ code_for(new_to_old[new_gid])
73
+ end
74
+
75
+ ranges = TTFunk::BinUtils.rangify(codes)
76
+
77
+ # calculate whether storing the charset as a series of ranges is
78
+ # more efficient (i.e. takes up less space) vs storing it as an
79
+ # array of SID values
80
+ total_range_size = (2 * ranges.size) +
81
+ (element_width(:range_format) * ranges.size)
82
+
83
+ total_array_size = codes.size * element_width(:array_format)
84
+
85
+ if total_array_size <= total_range_size
86
+ ([format_int(:array_format), codes.size] + codes).pack('C*')
87
+ else
88
+ element_fmt = element_format(:range_format)
89
+ result = [format_int(:range_format), ranges.size].pack('CC')
90
+ ranges.each { |range| result << range.pack(element_fmt) }
91
+ result
92
+ end
93
+ end
94
+
95
+ def supplemental?
96
+ # high-order bit set to 1 indicates supplemental encoding
97
+ @format >> 7 == 1
98
+ end
99
+
100
+ private
101
+
102
+ def encode_supplemental(_new_to_old, old_to_new)
103
+ new_entries = @entries.each_with_object({}) do |(code, old_gid), ret|
104
+ if (new_gid = old_to_new[old_gid])
105
+ ret[code] = new_gid
106
+ end
107
+ end
108
+
109
+ result = [format_int(:supplemental), new_entries.size].pack('CC')
110
+ fmt = element_format(:supplemental)
111
+
112
+ new_entries.each do |code, new_gid|
113
+ result << [code, new_gid].pack(fmt)
114
+ end
115
+
116
+ result
117
+ end
118
+
119
+ def code_for(glyph_id)
120
+ return 0 if glyph_id == 0
121
+
122
+ # rather than validating the glyph as part of one of the predefined
123
+ # encodings, just pass it through
124
+ return glyph_id unless offset
125
+
126
+ case format_sym
127
+ when :array_format
128
+ @entries[glyph_id]
129
+
130
+ when :range_format
131
+ remaining = glyph_id
132
+
133
+ @entries.each do |range|
134
+ if range.size >= remaining
135
+ return (range.first + remaining) - 1
136
+ end
137
+
138
+ remaining -= range.size
139
+ end
140
+
141
+ 0
142
+
143
+ when :supplemental
144
+ @entries[glyph_id]
145
+ end
146
+ end
147
+
148
+ def parse!
149
+ @format, entry_count = read(2, 'C*')
150
+ @length = entry_count * element_width
151
+
152
+ case format_sym
153
+ when :array_format
154
+ @count = entry_count
155
+ @entries = OneBasedArray.new(read(length, 'C*'))
156
+
157
+ when :range_format
158
+ @entries = []
159
+ @count = 0
160
+
161
+ entry_count.times do
162
+ code, num_left = read(element_width, element_format)
163
+ @entries << (code..(code + num_left))
164
+ @count += num_left + 1
165
+ end
166
+
167
+ when :supplemental
168
+ @entries = {}
169
+ @count = entry_count
170
+
171
+ entry_count.times do
172
+ code, glyph = read(element_width, element_format)
173
+ @entries[code] = glyph
174
+ end
175
+ end
176
+ end
177
+
178
+ def element_format(fmt = format_sym)
179
+ case fmt
180
+ when :array_format then 'C'
181
+ when :range_format then 'CC'
182
+ when :supplemental then 'Cn'
183
+ end
184
+ end
185
+
186
+ # @TODO: handle supplemental encoding (necessary?)
187
+ def element_width(fmt = format_sym)
188
+ case fmt
189
+ when :array_format then 1
190
+ when :range_format then 2
191
+ when :supplemental then 3
192
+ else
193
+ raise "'#{fmt}' is an unsupported encoding format"
194
+ end
195
+ end
196
+
197
+ def format_sym
198
+ return :supplemental if supplemental?
199
+
200
+ case @format
201
+ when 0 then :array_format
202
+ when 1 then :range_format
203
+ else
204
+ raise "unsupported charset format '#{fmt}'"
205
+ end
206
+ end
207
+
208
+ def format_int(sym = format_sym)
209
+ case sym
210
+ when :array_format then 0
211
+ when :range_format then 1
212
+ when :supplemental then 129
213
+ else
214
+ raise "unsupported charset format '#{sym}'"
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end