File: ttf_encoder.rb

package info (click to toggle)
ruby-ttfunk 1.8.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 18,472 kB
  • sloc: ruby: 7,954; makefile: 7
file content (218 lines) | stat: -rw-r--r-- 6,058 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
# frozen_string_literal: true

module TTFunk
  # Encodes a TrueType font subset to its binary representation.
  class TTFEncoder
    # Optimal table order according to TrueType specification.
    OPTIMAL_TABLE_ORDER = [
      'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'LTSH', 'VDMX',
      'hdmx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf',
      'kern', 'name', 'post', 'gasp', 'PCLT',
    ].freeze

    # Original font.
    # @return [TTFunk::File]
    attr_reader :original

    # Subset to encode.
    # @return [TTFunk::Subset]
    attr_reader :subset

    # Encoding options.
    # @return [Hash]
    attr_reader :options

    # @param original [TTFunk::File]
    # @param subset [TTFunk::Subset]
    # @param options [Hash]
    # @option options :kerning [Boolean] whether to encode Kerning (`kern`)
    #   table.
    def initialize(original, subset, options = {})
      @original = original
      @subset = subset
      @options = options
    end

    # Encode the font subset.
    #
    # @return [String]
    def encode
      # https://www.microsoft.com/typography/otspec/otff.htm#offsetTable
      search_range = (2**Math.log2(tables.length).floor) * 16
      entry_selector = Integer(Math.log2(2**Math.log2(tables.length).floor))
      range_shift = (tables.length * 16) - search_range
      range_shift = 0 if range_shift.negative?

      newfont = EncodedString.new

      newfont << [
        original.directory.scaler_type,
        tables.length,
        search_range,
        entry_selector,
        range_shift,
      ].pack('Nn*')

      # Tables are supposed to be listed in ascending order whereas there is a
      # known optimal order for table data.
      tables.keys.sort.each do |tag|
        newfont << [tag, checksum(tables[tag])].pack('A4N')
        newfont << Placeholder.new(tag, length: 4)
        newfont << [tables[tag].length].pack('N')
      end

      optimal_table_order.each do |optimal_tag|
        next unless tables.include?(optimal_tag)

        newfont.resolve_placeholder(optimal_tag, [newfont.length].pack('N'))
        newfont << tables[optimal_tag]
        newfont.align!(4)
      end

      sum = checksum(newfont)
      adjustment = 0xB1B0AFBA - sum
      newfont.resolve_placeholder(:checksum, [adjustment].pack('N'))

      newfont.string
    end

    private

    def optimal_table_order
      OPTIMAL_TABLE_ORDER +
        (tables.keys - ['DSIG'] - OPTIMAL_TABLE_ORDER) +
        ['DSIG']
    end

    # "mandatory" tables. Every font should ("should") have these

    def cmap_table
      @cmap_table ||= subset.new_cmap_table
    end

    def glyf_table
      @glyf_table ||= TTFunk::Table::Glyf.encode(glyphs, new_to_old_glyph, old_to_new_glyph)
    end

    def loca_table
      @loca_table ||= TTFunk::Table::Loca.encode(glyf_table[:offsets])
    end

    def hmtx_table
      @hmtx_table ||= TTFunk::Table::Hmtx.encode(original.horizontal_metrics, new_to_old_glyph)
    end

    def hhea_table
      @hhea_table = TTFunk::Table::Hhea.encode(original.horizontal_header, hmtx_table, original, new_to_old_glyph)
    end

    def maxp_table
      @maxp_table ||= TTFunk::Table::Maxp.encode(original.maximum_profile, old_to_new_glyph)
    end

    def post_table
      @post_table ||= TTFunk::Table::Post.encode(original.postscript, new_to_old_glyph)
    end

    def name_table
      @name_table ||= TTFunk::Table::Name.encode(original.name, glyf_table.fetch(:table, ''))
    end

    def head_table
      @head_table ||= TTFunk::Table::Head.encode(original.header, loca_table, new_to_old_glyph)
    end

    # "optional" tables. Fonts may omit these if they do not need them.
    # Because they apply globally, we can simply copy them over, without
    # modification, if they exist.

    def os2_table
      @os2_table ||= TTFunk::Table::OS2.encode(original.os2, subset)
    end

    def cvt_table
      @cvt_table ||= TTFunk::Table::Simple.new(original, 'cvt ').raw
    end

    def fpgm_table
      @fpgm_table ||= TTFunk::Table::Simple.new(original, 'fpgm').raw
    end

    def prep_table
      @prep_table ||= TTFunk::Table::Simple.new(original, 'prep').raw
    end

    def gasp_table
      @gasp_table ||= TTFunk::Table::Simple.new(original, 'gasp').raw
    end

    def kern_table
      # for PDFs, the kerning info is all included in the PDF as the text is
      # drawn. Thus, the PDF readers do not actually use the kerning info in
      # embedded fonts. If the library is used for something else, the
      # generated subfont may need a kerning table... in that case, you need
      # to opt into it.
      if options[:kerning]
        @kern_table ||= TTFunk::Table::Kern.encode(original.kerning, old_to_new_glyph)
      end
    end

    def vorg_table
      @vorg_table ||= TTFunk::Table::Vorg.encode(original.vertical_origins)
    end

    def dsig_table
      @dsig_table ||= TTFunk::Table::Dsig.encode(original.digital_signature)
    end

    def tables
      @tables ||= {
        'cmap' => cmap_table[:table],
        'glyf' => glyf_table[:table],
        'loca' => loca_table[:table],
        'kern' => kern_table,
        'hmtx' => hmtx_table[:table],
        'hhea' => hhea_table,
        'maxp' => maxp_table,
        'OS/2' => os2_table,
        'post' => post_table,
        'name' => name_table,
        'head' => head_table,
        'prep' => prep_table,
        'fpgm' => fpgm_table,
        'cvt ' => cvt_table,
        'VORG' => vorg_table,
        'DSIG' => dsig_table,
        'gasp' => gasp_table,
      }.compact
    end

    def glyphs
      subset.glyphs
    end

    def new_to_old_glyph
      subset.new_to_old_glyph
    end

    def old_to_new_glyph
      subset.old_to_new_glyph
    end

    def checksum(data)
      align(raw(data), 4).unpack('N*').sum & 0xFFFF_FFFF
    end

    def raw(data)
      data.respond_to?(:unresolved_string) ? data.unresolved_string : data
    end

    def align(data, width)
      if (data.length % width).positive?
        data + ("\0" * (width - (data.length % width)))
      else
        data
      end
    end
  end
end