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