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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../../reader'
2
4
 
3
5
  module TTFunk
@@ -13,18 +15,22 @@ module TTFunk
13
15
  WE_HAVE_A_TWO_BY_TWO = 0x0080
14
16
  WE_HAVE_INSTRUCTIONS = 0x0100
15
17
 
16
- attr_reader :raw
18
+ attr_reader :id, :raw
19
+ attr_reader :number_of_contours
17
20
  attr_reader :x_min, :y_min, :x_max, :y_max
18
21
  attr_reader :glyph_ids
19
22
 
20
23
  Component = Struct.new(:flags, :glyph_index, :arg1, :arg2, :transform)
21
24
 
22
- def initialize(raw, x_min, y_min, x_max, y_max)
25
+ def initialize(id, raw)
26
+ @id = id
23
27
  @raw = raw
24
- @x_min = x_min
25
- @y_min = y_min
26
- @x_max = x_max
27
- @y_max = y_max
28
+ io = StringIO.new(raw)
29
+
30
+ @number_of_contours, @x_min, @y_min, @x_max, @y_max =
31
+ io.read(10).unpack('n*').map do |i|
32
+ BinUtils.twos_comp_to_int(i, bit_width: 16)
33
+ end
28
34
 
29
35
  # Because TTFunk only cares about glyphs insofar as they (1) provide
30
36
  # a bounding box for each glyph, and (2) can be rewritten into a
@@ -45,6 +51,7 @@ module TTFunk
45
51
  @glyph_id_offsets << offset + 2
46
52
 
47
53
  break unless flags & MORE_COMPONENTS != 0
54
+
48
55
  offset += 4
49
56
 
50
57
  offset +=
@@ -69,7 +76,7 @@ module TTFunk
69
76
  end
70
77
 
71
78
  def recode(mapping)
72
- result = @raw.dup
79
+ result = raw.dup
73
80
  new_ids = glyph_ids.map { |id| mapping[id] }
74
81
 
75
82
  new_ids.zip(@glyph_id_offsets).each do |new_id, offset|
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTFunk
4
+ class Table
5
+ class Glyf
6
+ class PathBased
7
+ attr_reader :path, :horizontal_metrics
8
+ attr_reader :x_min, :y_min, :x_max, :y_max
9
+ attr_reader :left_side_bearing, :right_side_bearing
10
+
11
+ def initialize(path, horizontal_metrics)
12
+ @path = path
13
+ @horizontal_metrics = horizontal_metrics
14
+
15
+ @x_min = 0
16
+ @y_min = 0
17
+ @x_max = horizontal_metrics.advance_width
18
+ @y_max = 0
19
+
20
+ path.commands.each do |command|
21
+ cmd, x, y = command
22
+ next if cmd == :close
23
+
24
+ @x_min = x if x < @x_min
25
+ @x_max = x if x > @x_max
26
+ @y_min = y if y < @y_min
27
+ @y_max = y if y > @y_max
28
+ end
29
+
30
+ @left_side_bearing = horizontal_metrics.left_side_bearing
31
+ @right_side_bearing =
32
+ horizontal_metrics.advance_width -
33
+ @left_side_bearing -
34
+ (@x_max - @x_min)
35
+ end
36
+
37
+ def number_of_contours
38
+ path.number_of_contours
39
+ end
40
+
41
+ def compound?
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,28 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../../reader'
2
4
 
3
5
  module TTFunk
4
6
  class Table
5
7
  class Glyf
6
8
  class Simple
7
- attr_reader :raw
9
+ attr_reader :id, :raw
8
10
  attr_reader :number_of_contours
9
11
  attr_reader :x_min, :y_min, :x_max, :y_max
12
+ attr_reader :end_points_of_contours
13
+ attr_reader :instruction_length, :instructions
10
14
 
11
- def initialize(raw, number_of_contours, x_min, y_min, x_max, y_max)
15
+ def initialize(id, raw)
16
+ @id = id
12
17
  @raw = raw
13
- @number_of_contours = number_of_contours
14
- @x_min = x_min
15
- @y_min = y_min
16
- @x_max = x_max
17
- @y_max = y_max
18
-
19
- # Because TTFunk is, at this time, a library for simply pulling
20
- # metrics out of font files, or for writing font subsets, we don't
21
- # really care what the contours are for simple glyphs. We just
22
- # care that we've got an entire glyph's definition. Also, a
23
- # bounding box could be nice to know. Since we've got all that
24
- # at this point, we don't need to worry about parsing the full
25
- # contents of the glyph.
18
+ io = StringIO.new(raw)
19
+
20
+ @number_of_contours, @x_min, @y_min, @x_max, @y_max =
21
+ io.read(10).unpack('n*').map do |i|
22
+ BinUtils.twos_comp_to_int(i, bit_width: 16)
23
+ end
24
+
25
+ @end_points_of_contours = io.read(number_of_contours * 2).unpack('n*')
26
+ @instruction_length = io.read(2).unpack1('n')
27
+ @instructions = io.read(instruction_length).unpack('C*')
26
28
  end
27
29
 
28
30
  def compound?
@@ -32,6 +34,10 @@ module TTFunk
32
34
  def recode(_mapping)
33
35
  raw
34
36
  end
37
+
38
+ def end_point_of_last_contour
39
+ end_points_of_contours.last + 1
40
+ end
35
41
  end
36
42
  end
37
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
@@ -21,11 +23,47 @@ module TTFunk
21
23
  attr_reader :index_to_loc_format
22
24
  attr_reader :glyph_data_format
23
25
 
24
- def self.encode(head, loca)
25
- table = head.raw
26
- table[8, 4] = "\0\0\0\0" # set checksum adjustment to 0 initially
27
- table[-4, 2] = [loca[:type]].pack('n') # set index_to_loc_format
28
- table
26
+ class << self
27
+ # mapping is new -> old glyph ids
28
+ def encode(head, loca, mapping)
29
+ EncodedString.new do |table|
30
+ table <<
31
+ [head.version, head.font_revision].pack('N2') <<
32
+ Placeholder.new(:checksum, length: 4) <<
33
+ [
34
+ head.magic_number,
35
+ head.flags, head.units_per_em,
36
+ head.created, head.modified,
37
+ *min_max_values_for(head, mapping),
38
+ head.mac_style, head.lowest_rec_ppem, head.font_direction_hint,
39
+ loca[:type] || 0, head.glyph_data_format
40
+ ].pack('Nn2q2n*')
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def min_max_values_for(head, mapping)
47
+ x_min = Min.new
48
+ x_max = Max.new
49
+ y_min = Min.new
50
+ y_max = Max.new
51
+
52
+ mapping.each do |_, old_glyph_id|
53
+ glyph = head.file.find_glyph(old_glyph_id)
54
+ next unless glyph
55
+
56
+ x_min << glyph.x_min
57
+ x_max << glyph.x_max
58
+ y_min << glyph.y_min
59
+ y_max << glyph.y_max
60
+ end
61
+
62
+ [
63
+ x_min.value_or(0), y_min.value_or(0),
64
+ x_max.value_or(0), y_max.value_or(0)
65
+ ]
66
+ end
29
67
  end
30
68
 
31
69
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
@@ -13,13 +15,54 @@ module TTFunk
13
15
  attr_reader :x_max_extent
14
16
  attr_reader :carot_slope_rise
15
17
  attr_reader :carot_slope_run
18
+ attr_reader :caret_offset
16
19
  attr_reader :metric_data_format
17
20
  attr_reader :number_of_metrics
18
21
 
19
- def self.encode(hhea, hmtx)
20
- raw = hhea.raw
21
- raw[-2, 2] = [hmtx[:number_of_metrics]].pack('n')
22
- raw
22
+ class << self
23
+ def encode(hhea, hmtx, original, mapping)
24
+ ''.b.tap do |table|
25
+ table << [hhea.version].pack('N')
26
+ table << [
27
+ hhea.ascent, hhea.descent, hhea.line_gap,
28
+ *min_max_values_for(original, mapping),
29
+ hhea.carot_slope_rise, hhea.carot_slope_run, hhea.caret_offset,
30
+ 0, 0, 0, 0, hhea.metric_data_format, hmtx[:number_of_metrics]
31
+ ].pack('n*')
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def min_max_values_for(original, mapping)
38
+ min_lsb = Min.new
39
+ min_rsb = Min.new
40
+ max_aw = Max.new
41
+ max_extent = Max.new
42
+
43
+ mapping.each do |_, old_glyph_id|
44
+ horiz_metrics = original.horizontal_metrics.for(old_glyph_id)
45
+ next unless horiz_metrics
46
+
47
+ min_lsb << horiz_metrics.left_side_bearing
48
+ max_aw << horiz_metrics.advance_width
49
+
50
+ glyph = original.find_glyph(old_glyph_id)
51
+ next unless glyph
52
+
53
+ x_delta = glyph.x_max - glyph.x_min
54
+
55
+ min_rsb << horiz_metrics.advance_width -
56
+ horiz_metrics.left_side_bearing - x_delta
57
+
58
+ max_extent << horiz_metrics.left_side_bearing + x_delta
59
+ end
60
+
61
+ [
62
+ max_aw.value_or(0), min_lsb.value_or(0),
63
+ min_rsb.value_or(0), max_extent.value_or(0)
64
+ ]
65
+ end
23
66
  end
24
67
 
25
68
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
@@ -23,14 +25,19 @@ module TTFunk
23
25
 
24
26
  def for(glyph_id)
25
27
  @metrics[glyph_id] ||
26
- HorizontalMetric.new(
27
- @metrics.last.advance_width,
28
- @left_side_bearings[glyph_id - @metrics.length]
29
- )
28
+ metrics_cache[glyph_id] ||=
29
+ HorizontalMetric.new(
30
+ @metrics.last.advance_width,
31
+ @left_side_bearings[glyph_id - @metrics.length]
32
+ )
30
33
  end
31
34
 
32
35
  private
33
36
 
37
+ def metrics_cache
38
+ @metrics_cache ||= {}
39
+ end
40
+
34
41
  def parse!
35
42
  @metrics = []
36
43
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
@@ -8,6 +10,7 @@ module TTFunk
8
10
 
9
11
  def self.encode(kerning, mapping)
10
12
  return nil unless kerning.exists? && kerning.tables.any?
13
+
11
14
  tables = kerning.tables.map { |table| table.recode(mapping) }.compact
12
15
  return nil if tables.empty?
13
16
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../../reader'
2
4
 
3
5
  module TTFunk
@@ -18,6 +20,7 @@ module TTFunk
18
20
  num_pairs.times do |i|
19
21
  # sanity check, in case there's a bad length somewhere
20
22
  break if i * 3 + 2 > pairs.length
23
+
21
24
  left = pairs[i * 3]
22
25
  right = pairs[i * 3 + 1]
23
26
  value = to_signed(pairs[i * 3 + 2])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
@@ -1,8 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../table'
2
4
 
3
5
  module TTFunk
4
6
  class Table
5
7
  class Maxp < Table
8
+ DEFAULT_MAX_COMPONENT_DEPTH = 1
9
+ MAX_V1_TABLE_LENGTH = 34
10
+
6
11
  attr_reader :version
7
12
  attr_reader :num_glyphs
8
13
  attr_reader :max_points
@@ -19,21 +24,150 @@ module TTFunk
19
24
  attr_reader :max_component_elements
20
25
  attr_reader :max_component_depth
21
26
 
22
- def self.encode(maxp, mapping)
23
- num_glyphs = mapping.length
24
- raw = maxp.raw
25
- raw[4, 2] = [num_glyphs].pack('n')
26
- raw
27
+ class << self
28
+ def encode(maxp, new2old_glyph)
29
+ ''.b.tap do |table|
30
+ num_glyphs = new2old_glyph.length
31
+ table << [maxp.version, num_glyphs].pack('Nn')
32
+
33
+ if maxp.version == 0x10000
34
+ stats = stats_for(
35
+ maxp, glyphs_from_ids(maxp, new2old_glyph.values)
36
+ )
37
+
38
+ table << [
39
+ stats[:max_points],
40
+ stats[:max_contours],
41
+ stats[:max_component_points],
42
+ stats[:max_component_contours],
43
+ # these all come from the fpgm and cvt tables, which
44
+ # we don't support at the moment
45
+ maxp.max_zones,
46
+ maxp.max_twilight_points,
47
+ maxp.max_storage,
48
+ maxp.max_function_defs,
49
+ maxp.max_instruction_defs,
50
+ maxp.max_stack_elements,
51
+ stats[:max_size_of_instructions],
52
+ stats[:max_component_elements],
53
+ stats[:max_component_depth]
54
+ ].pack('n*')
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def glyphs_from_ids(maxp, glyph_ids)
62
+ glyph_ids.each_with_object([]) do |glyph_id, ret|
63
+ if (glyph = maxp.file.glyph_outlines.for(glyph_id))
64
+ ret << glyph
65
+ end
66
+ end
67
+ end
68
+
69
+ def stats_for(maxp, glyphs)
70
+ stats_for_simple(maxp, glyphs)
71
+ .merge(stats_for_compound(maxp, glyphs))
72
+ .each_with_object({}) do |(name, agg), ret|
73
+ ret[name] = agg.value_or(0)
74
+ end
75
+ end
76
+
77
+ def stats_for_simple(_maxp, glyphs)
78
+ max_component_elements = Max.new
79
+ max_points = Max.new
80
+ max_contours = Max.new
81
+ max_size_of_instructions = Max.new
82
+
83
+ glyphs.each do |glyph|
84
+ if glyph.compound?
85
+ max_component_elements << glyph.glyph_ids.size
86
+ else
87
+ max_points << glyph.end_point_of_last_contour
88
+ max_contours << glyph.number_of_contours
89
+ max_size_of_instructions << glyph.instruction_length
90
+ end
91
+ end
92
+
93
+ {
94
+ max_component_elements: max_component_elements,
95
+ max_points: max_points,
96
+ max_contours: max_contours,
97
+ max_size_of_instructions: max_size_of_instructions
98
+ }
99
+ end
100
+
101
+ def stats_for_compound(maxp, glyphs)
102
+ max_component_points = Max.new
103
+ max_component_depth = Max.new
104
+ max_component_contours = Max.new
105
+
106
+ glyphs.each do |glyph|
107
+ next unless glyph.compound?
108
+
109
+ stats = totals_for_compound(maxp, [glyph], 0)
110
+ max_component_points << stats[:total_points]
111
+ max_component_depth << stats[:max_depth]
112
+ max_component_contours << stats[:total_contours]
113
+ end
114
+
115
+ {
116
+ max_component_points: max_component_points,
117
+ max_component_depth: max_component_depth,
118
+ max_component_contours: max_component_contours
119
+ }
120
+ end
121
+
122
+ def totals_for_compound(maxp, glyphs, depth)
123
+ total_points = Sum.new
124
+ total_contours = Sum.new
125
+ max_depth = Max.new(depth)
126
+
127
+ glyphs.each do |glyph|
128
+ if glyph.compound?
129
+ stats = totals_for_compound(
130
+ maxp, glyphs_from_ids(maxp, glyph.glyph_ids), depth + 1
131
+ )
132
+
133
+ total_points << stats[:total_points]
134
+ total_contours << stats[:total_contours]
135
+ max_depth << stats[:max_depth]
136
+ else
137
+ stats = stats_for_simple(maxp, [glyph])
138
+ total_points << stats[:max_points]
139
+ total_contours << stats[:max_contours]
140
+ end
141
+ end
142
+
143
+ {
144
+ total_points: total_points,
145
+ total_contours: total_contours,
146
+ max_depth: max_depth
147
+ }
148
+ end
27
149
  end
28
150
 
29
151
  private
30
152
 
31
153
  def parse!
32
- @version, @num_glyphs, @max_points, @max_contours,
33
- @max_component_points, @max_component_contours, @max_zones,
34
- @max_twilight_points, @max_storage, @max_function_defs,
35
- @max_instruction_defs, @max_stack_elements, @max_size_of_instructions,
36
- @max_component_elements, @max_component_depth = read(length, 'Nn*')
154
+ @version, @num_glyphs = read(6, 'Nn')
155
+
156
+ if @version == 0x10000
157
+ @max_points, @max_contours, @max_component_points,
158
+ @max_component_contours, @max_zones, @max_twilight_points,
159
+ @max_storage, @max_function_defs, @max_instruction_defs,
160
+ @max_stack_elements, @max_size_of_instructions,
161
+ @max_component_elements = read(26, 'Nn*')
162
+
163
+ # a number of fonts omit these last two bytes for some reason,
164
+ # so we have to supply a default here to prevent nils
165
+ @max_component_depth = if length == MAX_V1_TABLE_LENGTH
166
+ read(2, 'n').first
167
+ else
168
+ DEFAULT_MAX_COMPONENT_DEPTH
169
+ end
170
+ end
37
171
  end
38
172
  end
39
173
  end