File: maxp.rb

package info (click to toggle)
ruby-ttfunk 1.8.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 18,472 kB
  • sloc: ruby: 7,954; makefile: 7
file content (227 lines) | stat: -rw-r--r-- 6,905 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# frozen_string_literal: true

require_relative '../table'

module TTFunk
  class Table
    # Maximum Profile (`maxp`) table
    class Maxp < Table
      # Default maximum levels of recursion.
      DEFAULT_MAX_COMPONENT_DEPTH = 1

      # Size of full table version 1.
      MAX_V1_TABLE_LENGTH = 32

      # Table version.
      # @return [Integer]
      attr_reader :version

      # The number of glyphs in the font.
      # @return [Integer]
      attr_reader :num_glyphs

      # Maximum points in a non-composite glyph.
      # @return [Integer]
      attr_reader :max_points

      # Maximum contours in a non-composite glyph.
      # @return [Integer]
      attr_reader :max_contours

      # Maximum points in a composite glyph.
      # @return [Integer]
      attr_reader :max_component_points

      # Maximum contours in a composite glyph.
      # @return [Integer]
      attr_reader :max_component_contours

      # Maximum zones.
      # * 1 if instructions do not use the twilight zone (Z0)
      # * 2 if instructions do use Z0
      # @return [Integer]
      attr_reader :max_zones

      # Maximum points used in Z0.
      # @return [Integer]
      attr_reader :max_twilight_points

      # Number of Storage Area locations.
      # @return [Integer]
      attr_reader :max_storage

      # Number of FDEFs.
      # @return [Integer]
      attr_reader :max_function_defs

      # Number of IDEFs.
      # @return [Integer]
      attr_reader :max_instruction_defs

      # Maximum stack depth across Font Program, CVT Program and all glyph
      # instructions.
      # @return [Integer]
      attr_reader :max_stack_elements

      # Maximum byte count for glyph instructions.
      # @return [Integer]
      attr_reader :max_size_of_instructions

      # Maximum number of components referenced at "top level" for any composite
      # glyph.
      # @return [Integer]
      attr_reader :max_component_elements

      # Maximum levels of recursion.
      # @return [Integer]
      attr_reader :max_component_depth

      class << self
        # Encode table.
        #
        # @param maxp [TTFunk::Table::Maxp]
        # @param new2old_glyph [Hash{Integer => Integer}] keys are new glyph IDs, values
        #   are old glyph IDs.
        # @return [String]
        def encode(maxp, new2old_glyph)
          ''.b.tap do |table|
            num_glyphs = new2old_glyph.length
            table << [maxp.version, num_glyphs].pack('Nn')

            if maxp.version == 0x10000
              stats = stats_for(maxp, glyphs_from_ids(maxp, new2old_glyph.values))

              table << [
                stats[:max_points],
                stats[:max_contours],
                stats[:max_component_points],
                stats[:max_component_contours],
                # these all come from the fpgm and cvt tables, which
                # we don't support at the moment
                maxp.max_zones,
                maxp.max_twilight_points,
                maxp.max_storage,
                maxp.max_function_defs,
                maxp.max_instruction_defs,
                maxp.max_stack_elements,
                stats[:max_size_of_instructions],
                stats[:max_component_elements],
                stats[:max_component_depth],
              ].pack('n*')
            end
          end
        end

        private

        def glyphs_from_ids(maxp, glyph_ids)
          glyph_ids.each_with_object([]) do |glyph_id, ret|
            if (glyph = maxp.file.glyph_outlines.for(glyph_id))
              ret << glyph
            end
          end
        end

        def stats_for(maxp, glyphs)
          stats_for_simple(maxp, glyphs)
            .merge(stats_for_compound(maxp, glyphs))
            .transform_values { |agg| agg.value_or(0) }
        end

        def stats_for_simple(_maxp, glyphs)
          max_component_elements = Max.new
          max_points = Max.new
          max_contours = Max.new
          max_size_of_instructions = Max.new

          glyphs.each do |glyph|
            if glyph.compound?
              max_component_elements << glyph.glyph_ids.size
            else
              max_points << glyph.end_point_of_last_contour
              max_contours << glyph.number_of_contours
              max_size_of_instructions << glyph.instruction_length
            end
          end

          {
            max_component_elements: max_component_elements,
            max_points: max_points,
            max_contours: max_contours,
            max_size_of_instructions: max_size_of_instructions,
          }
        end

        def stats_for_compound(maxp, glyphs)
          max_component_points = Max.new
          max_component_depth = Max.new
          max_component_contours = Max.new

          glyphs.each do |glyph|
            next unless glyph.compound?

            stats = totals_for_compound(maxp, [glyph], 0)
            max_component_points << stats[:total_points]
            max_component_depth << stats[:max_depth]
            max_component_contours << stats[:total_contours]
          end

          {
            max_component_points: max_component_points,
            max_component_depth: max_component_depth,
            max_component_contours: max_component_contours,
          }
        end

        def totals_for_compound(maxp, glyphs, depth)
          total_points = Sum.new
          total_contours = Sum.new
          max_depth = Max.new(depth)

          glyphs.each do |glyph|
            if glyph.compound?
              stats = totals_for_compound(maxp, glyphs_from_ids(maxp, glyph.glyph_ids), depth + 1)

              total_points << stats[:total_points]
              total_contours << stats[:total_contours]
              max_depth << stats[:max_depth]
            else
              stats = stats_for_simple(maxp, [glyph])
              total_points << stats[:max_points]
              total_contours << stats[:max_contours]
            end
          end

          {
            total_points: total_points,
            total_contours: total_contours,
            max_depth: max_depth,
          }
        end
      end

      private

      def parse!
        @version, @num_glyphs = read(6, 'Nn')

        if @version == 0x10000
          @max_points, @max_contours, @max_component_points,
            @max_component_contours, @max_zones, @max_twilight_points,
            @max_storage, @max_function_defs, @max_instruction_defs,
            @max_stack_elements, @max_size_of_instructions,
            @max_component_elements = read(24, 'n*')

          # a number of fonts omit these last two bytes for some reason,
          # so we have to supply a default here to prevent nils
          @max_component_depth =
            if length == MAX_V1_TABLE_LENGTH
              read(2, 'n').first
            else
              DEFAULT_MAX_COMPONENT_DEPTH
            end
        end
      end
    end
  end
end