fontprocessor 27.1.3
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rbenv-version +1 -0
- data/.rspec +0 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +64 -0
- data/CHANGELOG_FPV14.md +20 -0
- data/Dockerfile +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +156 -0
- data/Guardfile +5 -0
- data/README.md +6 -0
- data/Rakefile +53 -0
- data/Rakefile.base +57 -0
- data/bin/process-file +41 -0
- data/config/development.yml +5 -0
- data/config/production.yml +3 -0
- data/config/staging.yml +3 -0
- data/config/test.yml +5 -0
- data/fontprocessor.gemspec +37 -0
- data/lib/fontprocessor/config.rb +85 -0
- data/lib/fontprocessor/external/batik/batik-ttf2svg.jar +0 -0
- data/lib/fontprocessor/external/batik/lib/batik-svggen.jar +0 -0
- data/lib/fontprocessor/external/batik/lib/batik-util.jar +0 -0
- data/lib/fontprocessor/external/fontforge/subset.py +30 -0
- data/lib/fontprocessor/external/fontforge/utils.py +66 -0
- data/lib/fontprocessor/external_execution.rb +117 -0
- data/lib/fontprocessor/font_file.rb +149 -0
- data/lib/fontprocessor/font_file_naming_strategy.rb +63 -0
- data/lib/fontprocessor/font_format.rb +29 -0
- data/lib/fontprocessor/process_font_job.rb +227 -0
- data/lib/fontprocessor/processed_font_iterator.rb +89 -0
- data/lib/fontprocessor/processor.rb +790 -0
- data/lib/fontprocessor/version.rb +3 -0
- data/lib/fontprocessor.rb +16 -0
- data/scripts/build_and_test.sh +15 -0
- data/scripts/get_production_source_map.rb +53 -0
- data/spec/fixtures/bad_os2_width_class.otf +0 -0
- data/spec/fixtures/extra_language_names.otf +0 -0
- data/spec/fixtures/fixtures.rb +35 -0
- data/spec/fixtures/locked.otf +0 -0
- data/spec/fixtures/op_size.otf +0 -0
- data/spec/fixtures/ots_failure_font.otf +0 -0
- data/spec/fixtures/postscript.otf +0 -0
- data/spec/fixtures/stat_font.otf +0 -0
- data/spec/fixtures/truetype.otf +0 -0
- data/spec/lib/fontprocessor/config_spec.rb +38 -0
- data/spec/lib/fontprocessor/external_execution_spec.rb +33 -0
- data/spec/lib/fontprocessor/font_file_naming_strategy_spec.rb +13 -0
- data/spec/lib/fontprocessor/font_file_spec.rb +110 -0
- data/spec/lib/fontprocessor/process_font_job_spec.rb +317 -0
- data/spec/lib/fontprocessor/processed_font_iterator_spec.rb +128 -0
- data/spec/lib/fontprocessor/processor_spec.rb +466 -0
- data/spec/spec_helper.rb +4 -0
- data/tasks/fonts.rake +30 -0
- data/worker.rb +23 -0
- metadata +312 -0
@@ -0,0 +1,790 @@
|
|
1
|
+
module FontProcessor
|
2
|
+
FontProcessorError = Class.new(StandardError)
|
3
|
+
|
4
|
+
class Processor
|
5
|
+
|
6
|
+
|
7
|
+
#Unicode defs for hyphen-minus, hyphen and soft-hyphen
|
8
|
+
HYPHEN_MINUS = 0x2D
|
9
|
+
HYPHEN = 0x2010
|
10
|
+
SOFT_HYPHEN = 0xAD
|
11
|
+
|
12
|
+
#Unicode defs for space and non-breaking space
|
13
|
+
SPACE = 0x20
|
14
|
+
NON_BREAKING_SPACE = 0xA0
|
15
|
+
|
16
|
+
# PUA glyphs for font loading detection
|
17
|
+
PUA_FROM_0 = 0xe000
|
18
|
+
PUA_TO_0 = 66
|
19
|
+
PUA_FROM_1 = 0xe001
|
20
|
+
PUA_TO_1 = 69
|
21
|
+
PUA_FROM_2 = 0xe002
|
22
|
+
PUA_TO_2 = 83
|
23
|
+
PUA_FROM_3 = 0xe003
|
24
|
+
PUA_TO_3 = 98
|
25
|
+
PUA_FROM_4 = 0xe004
|
26
|
+
PUA_TO_4 = 115
|
27
|
+
PUA_FROM_5 = 0xe005
|
28
|
+
PUA_TO_5 = 119
|
29
|
+
PUA_FROM_6 = 0xe006
|
30
|
+
PUA_TO_6 = 121
|
31
|
+
PUA_PAIRS = [ [PUA_FROM_0, PUA_TO_0], [PUA_FROM_1, PUA_TO_1], [PUA_FROM_2, PUA_TO_2], [PUA_FROM_3, PUA_TO_3],
|
32
|
+
[PUA_FROM_4, PUA_TO_4], [PUA_TO_5, PUA_TO_5], [PUA_FROM_6, PUA_TO_6] ].freeze
|
33
|
+
|
34
|
+
# Name table record ids
|
35
|
+
FAMILY_ID = 1
|
36
|
+
SUBFAMILY_ID = 2
|
37
|
+
UNIQUE_ID = 3
|
38
|
+
FULLNAME_ID = 4
|
39
|
+
POSTSCRIPT_NAME_ID = 6
|
40
|
+
LICENSE_URL_ID = 14
|
41
|
+
|
42
|
+
# DWrite is picky about which weight values are used, if they aren't
|
43
|
+
# correct the font won't render.
|
44
|
+
# http://msdn.microsoft.com/en-us/library/dd368082(VS.85).aspx
|
45
|
+
VALID_US_WEIGHT_CLASSES = [100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
46
|
+
|
47
|
+
MissingSourceFile = Class.new(FontProcessorError)
|
48
|
+
OTSValidationFailure = Class.new(StandardError)
|
49
|
+
WOFFProcessingFailure = Class.new(StandardError)
|
50
|
+
UnknownFontFormat = Class.new(ArgumentError)
|
51
|
+
|
52
|
+
# naming_strategy - The FontFileNamingStrategy to process.
|
53
|
+
# license_url - The URL to embed within processed fonts in
|
54
|
+
# LICENSE_URL_ID name table record.
|
55
|
+
def initialize(naming_strategy, license_url, font_base_id)
|
56
|
+
@naming_strategy = naming_strategy
|
57
|
+
@license_url = license_url
|
58
|
+
@font_base_id = font_base_id
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: A helper method to inspect the metadata of the primary file for
|
62
|
+
# this font.
|
63
|
+
#
|
64
|
+
# Returns a FontFile for the unlocked source of this font.
|
65
|
+
def file_metadata(formats)
|
66
|
+
return @file_metadata if @file_metadata
|
67
|
+
|
68
|
+
begin
|
69
|
+
preprocess(formats)
|
70
|
+
@file_metadata = FontProcessor::FontFile.new(@naming_strategy.unlocked(original_font_format))
|
71
|
+
rescue FontProcessorError => e
|
72
|
+
raise
|
73
|
+
rescue EOFError, Error => e
|
74
|
+
fpe = FontProcessorError.new "#{e.class} : #{e.message}"
|
75
|
+
fpe.set_backtrace(e.backtrace)
|
76
|
+
raise fpe
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: Generates the series of fonts required to display this font on
|
81
|
+
# the web.
|
82
|
+
#
|
83
|
+
# charset_data - A JSON blob containing the complete definition for the charset
|
84
|
+
# including charset_id, features and unicode list.
|
85
|
+
# formats - A JSON blob containing the formats to process
|
86
|
+
# including process_orignal, convert and a derivatives array.
|
87
|
+
# lock - Determines whether the raw fonts should be locked or not.
|
88
|
+
# Defaults to true, only really useful for testing.
|
89
|
+
#
|
90
|
+
# Returns nothing.
|
91
|
+
def generate_char_set(charset_data, formats, lock=true)
|
92
|
+
begin
|
93
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Peverifying format data")
|
94
|
+
preverify_format_data(formats) or raise "Invalid format data #{formats}"
|
95
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Preprocessing")
|
96
|
+
preprocess(formats)
|
97
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "generate_internal_char_set...")
|
98
|
+
generate_internal_char_set(charset_data, formats, lock)
|
99
|
+
rescue FontProcessorError => e
|
100
|
+
raise
|
101
|
+
rescue EOFError, Error => e
|
102
|
+
fpe = FontProcessorError.new "#{e.class} : #{e.message}"
|
103
|
+
fpe.set_backtrace(e.backtrace)
|
104
|
+
raise fpe
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns a FontFormat that represents the original, primary format of this
|
109
|
+
# font. The result is determine by inspecting the font itself.
|
110
|
+
def original_font_format
|
111
|
+
@original_font_format ||= begin
|
112
|
+
source_file = FontProcessor::FontFile.new(@naming_strategy.source)
|
113
|
+
|
114
|
+
outline_format = case
|
115
|
+
when source_file.has_postscript_outlines? then :cff
|
116
|
+
when source_file.has_truetype_outlines? then :ttf
|
117
|
+
else raise UnknownFontFormat, "Does not contain either TrueType or PostScript outlines"
|
118
|
+
end
|
119
|
+
|
120
|
+
FontFormat.new(outline_format, :otf)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# font_formats_to_generate - creates an arra of FontFormats.
|
125
|
+
#
|
126
|
+
# format_data - A JSON blob containing the formats to process
|
127
|
+
# including process_orignal, convert and a derivatives array.
|
128
|
+
#
|
129
|
+
# Returns an Array of FontFormats to be generated from the source font.
|
130
|
+
def font_formats_to_generate(format_data)
|
131
|
+
@font_formats_to_generate ||= begin
|
132
|
+
formats = []
|
133
|
+
formats << FontFormat.new(:cff, :otf) if original_font_format.outline_format == :cff
|
134
|
+
if original_font_format.outline_format == :ttf || format_data['convert'] == true
|
135
|
+
formats << FontFormat.new(:ttf, :otf)
|
136
|
+
end
|
137
|
+
formats
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Performs all of the global font modifications to the original font
|
142
|
+
# creating a set of source fonts. If the font contains only TrueType
|
143
|
+
# outlines it will only have a single source font, if it has PostScript
|
144
|
+
# outlines it will contain two sources and the TrueType outlines will be
|
145
|
+
# derived from the PostScript outlines.
|
146
|
+
#
|
147
|
+
# Note: If the source file previously exists it is assumed to be correct
|
148
|
+
# and will not be reprocessed.
|
149
|
+
#
|
150
|
+
# formats - A JSON blob containing the formats to process
|
151
|
+
# including process_orignal, convert and a derivatives array.
|
152
|
+
#
|
153
|
+
# Returns nothing.
|
154
|
+
# Raises MissingSourceFile if the original font file can not be found.
|
155
|
+
def preprocess(formats)
|
156
|
+
raise MissingSourceFile, "Missing original file '#{@naming_strategy.source}'" unless File.exist?(@naming_strategy.source)
|
157
|
+
|
158
|
+
filename = @naming_strategy.source(original_font_format)
|
159
|
+
return if File.exists? filename
|
160
|
+
|
161
|
+
unlocked_filename = @naming_strategy.unlocked(original_font_format)
|
162
|
+
FileUtils.cp(@naming_strategy.source, unlocked_filename)
|
163
|
+
|
164
|
+
unlock(unlocked_filename)
|
165
|
+
FileUtils.cp(unlocked_filename, filename)
|
166
|
+
|
167
|
+
# we want to avoid using FontForge at all costs. Skytype can fix some minor problems which
|
168
|
+
# will save us from failing OTS and therefore needing FontForge. Make a "subset" of
|
169
|
+
# everything here - which will not actually remove any glyphs.
|
170
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
171
|
+
subset_data = fm.generate_subset_raw(nil, "ALL")
|
172
|
+
fm = Skytype::FontManipulator.new(subset_data)
|
173
|
+
fm.save(filename)
|
174
|
+
|
175
|
+
sanitizer = Ots::Ruby::Sanitizer.new(File.binread(filename), Ots::Ruby::Sanitizer::SOFT)
|
176
|
+
sanitizer.dump_file = File.join(Dir::tmpdir, "tmp_ots_dump.txt")
|
177
|
+
if not sanitizer.valid?
|
178
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
179
|
+
orig_had_gdef = fm.has_table("GDEF")
|
180
|
+
os2_data = fm.get_table("OS/2")
|
181
|
+
os2_table = Skytype::OS2Table.new(os2_data)
|
182
|
+
|
183
|
+
ProcessFontJob.log(@font_base_id, "N/A", "WARNING: Original font failed OTS. Re-processing with FontForge!")
|
184
|
+
tmp_file_name = File.join(Dir::tmpdir, "temp_orignal.otf")
|
185
|
+
FileUtils.cp(filename, tmp_file_name)
|
186
|
+
FileUtils.rm(filename)
|
187
|
+
parameters = {
|
188
|
+
'input_filename' => tmp_file_name,
|
189
|
+
'output_filename' => filename}
|
190
|
+
python_script = File.join(File.dirname(__FILE__), "external", "fontforge", "subset.py")
|
191
|
+
FontProcessor::ExternalJSONProgram.run(python_script, parameters)
|
192
|
+
ProcessFontJob.log(@font_base_id, "N/A", "FontForge reprocessing complete")
|
193
|
+
# check to make sure that FF didn't add a GDEF for us
|
194
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
195
|
+
if fm.has_table("GDEF") && !orig_had_gdef
|
196
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Removing GDEF table added by FontForge")
|
197
|
+
fm.remove_table("GDEF")
|
198
|
+
fm.save(filename)
|
199
|
+
end
|
200
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Applying os2 table to FontForge font")
|
201
|
+
fm.apply_table(os2_table)
|
202
|
+
fm.save(filename)
|
203
|
+
end
|
204
|
+
|
205
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
206
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Inserting PUA glyphs..")
|
207
|
+
insert_pua_glyphs(fm,filename)
|
208
|
+
raise FontProcessorError, "Does not contain either TrueType or PostScript outlines" unless fm.valid?
|
209
|
+
strip(filename)
|
210
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Adding name record..")
|
211
|
+
add_name_record(LICENSE_URL_ID, filename, @license_url)
|
212
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Fixing os2 weight and width")
|
213
|
+
fix_os2_weight_and_width(filename)
|
214
|
+
|
215
|
+
if original_font_format.outline_format == :cff && formats['convert'] == true
|
216
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Converting CFF...")
|
217
|
+
convert(filename, @naming_strategy.source(FontFormat.new(:ttf, :otf)))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def insert_pua_glyphs(fm, filename)
|
222
|
+
PUA_PAIRS.each do | pair |
|
223
|
+
fm.map_unicode_to_unicode(pair[0], pair[1]) unless fm.get_glyph_for_unicode(pair[0]) != 0
|
224
|
+
end
|
225
|
+
fm.save(filename)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Generates all of the files required to display a single character set.
|
229
|
+
#
|
230
|
+
# charset_data - A JSON blob containing the complete definition for the charset
|
231
|
+
# including charset_id, features and unicode list.
|
232
|
+
# format_data - A JSON blob containing the formats to process
|
233
|
+
# including process_orignal, convert and a derivatives array.
|
234
|
+
# lock - Determines whether the raw fonts should be locked or not.
|
235
|
+
# Defaults to true, only really useful for testing.
|
236
|
+
#
|
237
|
+
# Returns nothing.
|
238
|
+
# Raises MissingSourceFile if one of the source files require for
|
239
|
+
# processing is missing.
|
240
|
+
# Raises OTSValidationFailure if the subset font cannot pass OTS.
|
241
|
+
def generate_internal_char_set(charset_data, format_data, lock)
|
242
|
+
font_formats_to_generate(format_data).each do |font_format|
|
243
|
+
raise MissingSourceFile, "Missing source file '#{@naming_strategy.source(font_format)}'" unless File.exist?(@naming_strategy.source(font_format))
|
244
|
+
|
245
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "generate_internal_char_set naming strategy: #{@naming_strategy}")
|
246
|
+
filename = @naming_strategy.char_set(charset_data['charset_id'], font_format)
|
247
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Subsetting...")
|
248
|
+
subset(charset_data,
|
249
|
+
@naming_strategy.source(font_format),
|
250
|
+
@naming_strategy.source(font_format),
|
251
|
+
filename)
|
252
|
+
|
253
|
+
|
254
|
+
# Get a FM instance of the created subset. If it does not contain
|
255
|
+
# soft-hyphen (U+00AD), but does contain hyphen (U+002D or U+2010),
|
256
|
+
# then map soft-hyphen to hyphen.
|
257
|
+
# Same thing is true for non-breaking space and space.
|
258
|
+
# https://git.corp.adobe.com/typekit/fontprocessor/issues/2 -- this is for browsers to not display notdefs
|
259
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
260
|
+
if fm.get_glyph_for_unicode(SOFT_HYPHEN) == 0
|
261
|
+
if fm.get_glyph_for_unicode(HYPHEN_MINUS) != 0
|
262
|
+
fm.map_unicode_to_unicode(SOFT_HYPHEN, HYPHEN_MINUS)
|
263
|
+
elsif fm.get_glyph_for_unicode(HYPHEN) != 0
|
264
|
+
fm.map_unicode_to_unicode(SOFT_HYPHEN, HYPHEN)
|
265
|
+
end
|
266
|
+
fm.save(filename)
|
267
|
+
end
|
268
|
+
|
269
|
+
# always map nbsp to space so that they will render identically
|
270
|
+
if fm.get_glyph_for_unicode(SPACE) != 0
|
271
|
+
fm.map_unicode_to_unicode(NON_BREAKING_SPACE, SPACE)
|
272
|
+
end
|
273
|
+
fm.save(filename)
|
274
|
+
|
275
|
+
# validate subset font
|
276
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Sanitizing...")
|
277
|
+
sanitizer = Ots::Ruby::Sanitizer.new(File.binread(filename), Ots::Ruby::Sanitizer::HARD)
|
278
|
+
sanitizer.dump_file = File.join(Dir::tmpdir, "tmp_ots_dump.txt")
|
279
|
+
raise OTSValidationFailure, "Failed to sanitize subset file '#{Pathname.new(@naming_strategy.source).basename}' with CharSet '#{charset_data['charset_id']}'!" unless sanitizer.valid?
|
280
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Done sanitizing...")
|
281
|
+
|
282
|
+
# Copy the family name to the full name so that IE9 can handle the
|
283
|
+
# files properly
|
284
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "Copying name record...")
|
285
|
+
copy_name_record(filename,
|
286
|
+
FAMILY_ID,
|
287
|
+
FULLNAME_ID)
|
288
|
+
|
289
|
+
# Work around FontSupervisor deploy issue
|
290
|
+
format_data['derivatives'].push "dyna_base" unless format_data['derivatives'].include?("dyna_base")
|
291
|
+
format_data['derivatives'].push "woff2" unless format_data['derivatives'].include?("woff2")
|
292
|
+
|
293
|
+
format_data['derivatives'].each do |derivative|
|
294
|
+
case derivative
|
295
|
+
when "dyna_base"
|
296
|
+
wrap_dyna_base(charset_data['charset_id'], font_format)
|
297
|
+
when "woff"
|
298
|
+
wrap_woff(charset_data['charset_id'], font_format)
|
299
|
+
when "woff2"
|
300
|
+
wrap_woff2(charset_data['charset_id'], font_format)
|
301
|
+
when "inst"
|
302
|
+
wrap_installable(charset_data['charset_id'], font_format)
|
303
|
+
when "swf"
|
304
|
+
wrap_swf(charset_data['charset_id'], font_format)
|
305
|
+
when "svg"
|
306
|
+
ProcessFontJob.log(@font_base_id, charset_data['charset_id'], "SVG, doing nothing for this format...")
|
307
|
+
""
|
308
|
+
else
|
309
|
+
raise "Invalid format found in derivatives request"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
obfuscate_names(filename, Digest::MD5.hexdigest(@naming_strategy.source))
|
313
|
+
|
314
|
+
lock(filename) if lock
|
315
|
+
# remove the master font if this the orignal font should not be processed
|
316
|
+
if font_format.outline_format == original_font_format.outline_format && format_data['process_original'] == false
|
317
|
+
File.delete(filename)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Removes characters contained in the source font but aren't included in
|
323
|
+
# the character set.
|
324
|
+
#
|
325
|
+
# charset_data - A JSON blob containing the complete definition for the charset
|
326
|
+
# including charset_id, features and unicode list.
|
327
|
+
# original - The pristine font file from the foundry. It is required to
|
328
|
+
# undo some of fontforge's 'magic'.
|
329
|
+
# source - The filename of the source font to modify.
|
330
|
+
# output - The filename of the result.
|
331
|
+
#
|
332
|
+
# Returns nothing.
|
333
|
+
def subset(charset_data, original, source, output)
|
334
|
+
fm = Skytype::FontManipulator.new(File.binread(source))
|
335
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
336
|
+
the_subset = parse_charset_to_subset(charset_data)
|
337
|
+
|
338
|
+
subset_data = fm.generate_subset(the_subset)
|
339
|
+
subset_manipulator = Skytype::FontManipulator.new(subset_data)
|
340
|
+
subset_manipulator.save(output)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Removes characters contained in the source font but aren't included in
|
344
|
+
# the character set.
|
345
|
+
#
|
346
|
+
# charset_data - A JSON blob containing the complete definition for the charset
|
347
|
+
# including charset_id, features and unicode list.
|
348
|
+
# original - The pristine font file from the foundry. It is required to
|
349
|
+
# undo some of fontforge's 'magic'.
|
350
|
+
# source - The filename of the source font to modify.
|
351
|
+
# output - The filename of the result.
|
352
|
+
#
|
353
|
+
# Returns nothing.
|
354
|
+
def parse_charset_to_subset(charset_data)
|
355
|
+
the_subset = Skytype::Subset.new
|
356
|
+
uni_vals = charset_data['unicode'] or raise "No unicode set on charset!"
|
357
|
+
# split on comma or newline/carriage return
|
358
|
+
uni_array = uni_vals.split(/[,\n\r]/)
|
359
|
+
uni_array.each do |entry|
|
360
|
+
# strip any whitespace
|
361
|
+
entry.strip!
|
362
|
+
# if this is a range, split the entry
|
363
|
+
if entry.include? ".."
|
364
|
+
range_array = entry.split("..")
|
365
|
+
range_start = map_uni_value_to_int(range_array[0])
|
366
|
+
range_end = map_uni_value_to_int(range_array[1])
|
367
|
+
|
368
|
+
uni_range = Range.new(range_start, range_end)
|
369
|
+
uni_range.each do |val|
|
370
|
+
the_subset.add_unicode(val)
|
371
|
+
end
|
372
|
+
else
|
373
|
+
the_subset.add_unicode(map_uni_value_to_int(entry))
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
the_subset.set_features(charset_data['features'])
|
378
|
+
the_subset
|
379
|
+
end
|
380
|
+
|
381
|
+
# Converts from one type of outlines to another (based on the file
|
382
|
+
# extensions as interpreted by fontforge). Currently any file ending in
|
383
|
+
# .otf will be assumed to contain PostScript outlines and any file ending
|
384
|
+
# with .ttf will be assumed to contain TrueType outlines.
|
385
|
+
#
|
386
|
+
# source - The filename of the source font to modify.
|
387
|
+
# output - The filename of the result.
|
388
|
+
#
|
389
|
+
# Returns nothing.
|
390
|
+
def convert(source, output)
|
391
|
+
fm = Skytype::FontManipulator.new(File.binread(source))
|
392
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
393
|
+
ttf_data = fm.convert_to_ttf
|
394
|
+
raise StandardError, "ERROR: Failed to create TTF data from source #{source.to_s}." if ttf_data.nil?
|
395
|
+
ttf_font = Skytype::FontManipulator.new(ttf_data)
|
396
|
+
raise StandardError, "ERROR: Failed to create TTF FontManipulator from TTF source data." if ttf_font.nil?
|
397
|
+
ttf_font.save(output)
|
398
|
+
end
|
399
|
+
|
400
|
+
# Wraps the given file (as specified with char_set and font_format) in a
|
401
|
+
# WOFF.
|
402
|
+
#
|
403
|
+
# charset_id - A CharsetID which represents which characters to keep in the
|
404
|
+
# processed font.
|
405
|
+
# font_format - The FontFormat of the source file.
|
406
|
+
#
|
407
|
+
# Returns nothing.
|
408
|
+
# Raises OTSValidationFailure if the WOFF wrapped font cannot pass OTS.
|
409
|
+
def wrap_woff(charset_id, font_format)
|
410
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Wrapping WOFF...")
|
411
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Naming strategy: #{@naming_strategy}")
|
412
|
+
input_filename = @naming_strategy.char_set(charset_id, font_format)
|
413
|
+
output_raw_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :woff_raw))
|
414
|
+
output_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :woff))
|
415
|
+
|
416
|
+
fm = Skytype::FontManipulator.new(File.binread(input_filename))
|
417
|
+
fm.save(output_raw_filename)
|
418
|
+
validate_raw_path = Pathname.new(output_raw_filename)
|
419
|
+
if validate_raw_path.exist?
|
420
|
+
sanitizer = Ots::Ruby::Sanitizer.new(File.binread(output_raw_filename), Ots::Ruby::Sanitizer::HARD)
|
421
|
+
sanitizer.dump_file = File.join(Dir::tmpdir, "tmp_ots_woff_raw_dump.txt")
|
422
|
+
raise OTSValidationFailure, "Failed to sanitize pre-WOFF file '#{Pathname.new(output_raw_filename).basename}' with CharSet '#{charset_id}'!" unless sanitizer.valid?
|
423
|
+
end
|
424
|
+
|
425
|
+
input_data = File.binread(input_filename)
|
426
|
+
begin
|
427
|
+
woff_data = Wofferizer::WOFF.sfnt_to_woff(input_data, input_data.size)
|
428
|
+
rescue ExternalProgramError => e
|
429
|
+
raise e unless e.message =~ /:\s*### WOFF warning: checksum mismatch \(corrected\)\s*\z/
|
430
|
+
end
|
431
|
+
|
432
|
+
if woff_data.nil?
|
433
|
+
raise WOFFProcessingFailure, "Failed to wrap WOFF file!"
|
434
|
+
end
|
435
|
+
File.binwrite(output_filename, woff_data)
|
436
|
+
|
437
|
+
#validate WOFF wrapped font - only check this if the wrapping succeeded
|
438
|
+
validation_path = Pathname.new(output_filename)
|
439
|
+
if validation_path.exist?
|
440
|
+
sanitizer = Ots::Ruby::Sanitizer.new(File.binread(output_filename), Ots::Ruby::Sanitizer::HARD)
|
441
|
+
sanitizer.dump_file = File.join(Dir::tmpdir, "tmp_ots_woff_dump.txt")
|
442
|
+
raise OTSValidationFailure, "Failed to sanitize WOFF file '#{Pathname.new(output_filename).basename}' with CharSet '#{charset_id}'!" unless sanitizer.valid?
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Wraps the given file (as specified with char_set and font_format) in a
|
447
|
+
# WOFF2 (using maximum compression).
|
448
|
+
#
|
449
|
+
# charset_id - A CharsetID which represents which characters to keep in the
|
450
|
+
# processed font.
|
451
|
+
# font_format - The FontFormat of the source file.
|
452
|
+
#
|
453
|
+
# Returns nothing.
|
454
|
+
def wrap_woff2(charset_id, font_format)
|
455
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Wrapping WOFF2...")
|
456
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Naming strategy: #{@naming_strategy}")
|
457
|
+
input_filename = @naming_strategy.char_set(charset_id, font_format)
|
458
|
+
output_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :woff2))
|
459
|
+
|
460
|
+
input_data = File.binread(input_filename)
|
461
|
+
woff2_data = Wofferizer::WOFF.sfnt_to_woff2(input_data, input_data.size, 11)
|
462
|
+
|
463
|
+
if woff2_data.nil?
|
464
|
+
raise WOFFProcessingFailure, "Failed to wrap WOFF2 file!"
|
465
|
+
end
|
466
|
+
File.binwrite(output_filename, woff2_data)
|
467
|
+
end
|
468
|
+
|
469
|
+
def wrap_installable(charset_id, font_format)
|
470
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Wrapping Installable...")
|
471
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Naming strategy: #{@naming_strategy}")
|
472
|
+
input_filename = @naming_strategy.char_set(charset_id, font_format)
|
473
|
+
output_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :inst))
|
474
|
+
# read the file and save it to the output dir
|
475
|
+
fm = Skytype::FontManipulator.new(File.binread(input_filename))
|
476
|
+
fm.save(output_filename)
|
477
|
+
#using the saved file, obfuscate the names
|
478
|
+
obfuscate_names(output_filename, Digest::MD5.hexdigest(@naming_strategy.source))
|
479
|
+
# reload the font
|
480
|
+
fm = Skytype::FontManipulator.new(File.binread(output_filename))
|
481
|
+
|
482
|
+
base_name_replacement = Digest::SHA256.hexdigest(@font_base_id)
|
483
|
+
name_data = fm.get_table("name")
|
484
|
+
name_table = Skytype::NameTable.new(name_data)
|
485
|
+
name_table.add_or_replace_string(base_name_replacement, POSTSCRIPT_NAME_ID)
|
486
|
+
name_table.add_or_replace_string(base_name_replacement, FAMILY_ID)
|
487
|
+
name_table.add_or_replace_string(base_name_replacement + " #{name_table.get_string(SUBFAMILY_ID, Skytype::NameTable::EN_LANGUAGE_ID)}", FULLNAME_ID)
|
488
|
+
fm.apply_table(name_table)
|
489
|
+
|
490
|
+
# make sure that the font's CFF NameDict entry matches the PS name of the font
|
491
|
+
fm.set_cff_name(base_name_replacement)
|
492
|
+
|
493
|
+
fm.save(output_filename)
|
494
|
+
end
|
495
|
+
|
496
|
+
def wrap_dyna_base(charset_id, font_format)
|
497
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Wrapping Dynamic Base...")
|
498
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Naming strategy: #{@naming_strategy}")
|
499
|
+
input_filename = @naming_strategy.char_set(charset_id, font_format)
|
500
|
+
output_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :dyna_base))
|
501
|
+
# read the file and save it to the output dir
|
502
|
+
fm = Skytype::FontManipulator.new(File.binread(input_filename))
|
503
|
+
fm.save(output_filename)
|
504
|
+
#using the saved file, obfuscate the names
|
505
|
+
obfuscate_names(output_filename, Digest::MD5.hexdigest(@naming_strategy.source))
|
506
|
+
# reload the font
|
507
|
+
fm = Skytype::FontManipulator.new(File.binread(output_filename))
|
508
|
+
|
509
|
+
base_name_replacement = Digest::SHA256.hexdigest(@font_base_id)
|
510
|
+
name_data = fm.get_table("name")
|
511
|
+
name_table = Skytype::NameTable.new(name_data)
|
512
|
+
name_table.add_or_replace_string(base_name_replacement, POSTSCRIPT_NAME_ID)
|
513
|
+
name_table.add_or_replace_string(base_name_replacement, FAMILY_ID)
|
514
|
+
name_table.add_or_replace_string(base_name_replacement + " #{name_table.get_string(SUBFAMILY_ID, Skytype::NameTable::EN_LANGUAGE_ID)}", FULLNAME_ID)
|
515
|
+
fm.apply_table(name_table)
|
516
|
+
# make sure that the font's CFF NameDict entry matches the PS name of the font
|
517
|
+
fm.set_cff_name(base_name_replacement)
|
518
|
+
|
519
|
+
os2_data = fm.get_table("OS/2")
|
520
|
+
os2_table = Skytype::OS2Table.new(os2_data)
|
521
|
+
os2_table.disable_embedding(true)
|
522
|
+
fm.apply_table(os2_table)
|
523
|
+
|
524
|
+
fm.save(output_filename)
|
525
|
+
end
|
526
|
+
|
527
|
+
# Fake module used for tests since FLEX SDK is hard to install and it's on its way out since
|
528
|
+
# we won't be supporting SWF in the near future in 2018 per Gregor
|
529
|
+
module FakeSwfWrapper
|
530
|
+
def self.wrap(file_name)
|
531
|
+
FileUtils.touch file_name
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# Wraps the given file (as specified with char_set and font_format) in a
|
536
|
+
# SWF.
|
537
|
+
#
|
538
|
+
# charset_id - A CharsetID which represents which characters to keep in the
|
539
|
+
# processed font.
|
540
|
+
# font_format - The FontFormat of the source file.
|
541
|
+
#
|
542
|
+
# Returns nothing.
|
543
|
+
def wrap_swf(charset_id, font_format)
|
544
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Wrapping SWF...")
|
545
|
+
ProcessFontJob.log(@font_base_id, charset_id, "Naming strategy: #{@naming_strategy}")
|
546
|
+
input_filename = @naming_strategy.char_set(charset_id, font_format)
|
547
|
+
output_filename = @naming_strategy.char_set(charset_id, FontFormat.new(font_format.outline_format, :swf))
|
548
|
+
font_name = "typekit_#{@font_base_id}"
|
549
|
+
|
550
|
+
actionscript_filename = File.join(Dir::tmpdir, "#{font_name}.as")
|
551
|
+
actionscript = <<-ACTIONSCRIPT
|
552
|
+
package
|
553
|
+
{
|
554
|
+
import flash.display.Sprite;
|
555
|
+
import flash.text.Font
|
556
|
+
|
557
|
+
public class #{font_name} extends Sprite
|
558
|
+
{
|
559
|
+
public function #{font_name}()
|
560
|
+
{
|
561
|
+
Font.registerFont(fontClass);
|
562
|
+
}
|
563
|
+
[Embed(source='#{input_filename}', fontName='_#{font_name}', embedAsCFF='true', mimeType='application/x-font-truetype')]
|
564
|
+
public static var fontClass:Class;
|
565
|
+
}
|
566
|
+
}
|
567
|
+
ACTIONSCRIPT
|
568
|
+
begin
|
569
|
+
File.open(actionscript_filename, "w") do |f|
|
570
|
+
f.write(actionscript)
|
571
|
+
end
|
572
|
+
|
573
|
+
if ENV['RACK'] == "test"
|
574
|
+
FakeSwfWrapper.wrap(output_filename)
|
575
|
+
else
|
576
|
+
executable = File.join(Config.flex_sdk_path, "bin", "mxmlc")
|
577
|
+
command = "\"#{executable}\" -static-link-runtime-shared-libraries=true -output \"#{output_filename}\" -- #{actionscript_filename}"
|
578
|
+
|
579
|
+
FontProcessor::ExternalStdOutCommand.run(command, :expect_output => :return)
|
580
|
+
end
|
581
|
+
ensure
|
582
|
+
FileUtils.rm(actionscript_filename)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Ensures that both the usWeightClass and usWidthClass conform to
|
587
|
+
# the OTF spec. If the font is already valid nothing is done.
|
588
|
+
#
|
589
|
+
# filename - The filename of the font to fix.
|
590
|
+
#
|
591
|
+
# Returns nothing.
|
592
|
+
def fix_os2_weight_and_width(filename)
|
593
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
594
|
+
os2_data = fm.get_table("OS/2")
|
595
|
+
os2_table = Skytype::OS2Table.new(os2_data)
|
596
|
+
if os2_table.get_width < 1
|
597
|
+
os2_table.set_width(1)
|
598
|
+
elsif os2_table.get_width > 9
|
599
|
+
os2_table.set_width(9)
|
600
|
+
end
|
601
|
+
|
602
|
+
if os2_table.get_weight > 950
|
603
|
+
os2_table.set_weight(950)
|
604
|
+
else
|
605
|
+
index = (VALID_US_WEIGHT_CLASSES + [os2_table.get_weight]).sort.index(os2_table.get_weight)
|
606
|
+
os2_table.set_weight(VALID_US_WEIGHT_CLASSES[index])
|
607
|
+
end
|
608
|
+
fm.apply_table(os2_table)
|
609
|
+
fm.save(filename)
|
610
|
+
end
|
611
|
+
|
612
|
+
# Sets the OS/2 embedding bit to be emdedable, which allows
|
613
|
+
# fontforge to modify it and IE to render it.
|
614
|
+
#
|
615
|
+
# filename - The filename of the font to unlock.
|
616
|
+
#
|
617
|
+
# Returns nothing.
|
618
|
+
def unlock(filename)
|
619
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
620
|
+
os2_data = fm.get_table("OS/2")
|
621
|
+
os2_table = Skytype::OS2Table.new(os2_data)
|
622
|
+
os2_table.disable_embedding(false)
|
623
|
+
|
624
|
+
fm.apply_table(os2_table)
|
625
|
+
fm.save(filename)
|
626
|
+
end
|
627
|
+
|
628
|
+
# Sets the OS/2 embedding bit to deny embedding rights, which prevents
|
629
|
+
# fontforge from modifying it.
|
630
|
+
#
|
631
|
+
# filename - The filename of the font to lock.
|
632
|
+
#
|
633
|
+
# Returns nothing.
|
634
|
+
def lock(filename)
|
635
|
+
|
636
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
637
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
638
|
+
os2_data = fm.get_table("OS/2")
|
639
|
+
os2_table = Skytype::OS2Table.new(os2_data)
|
640
|
+
os2_table.disable_embedding(true)
|
641
|
+
|
642
|
+
fm.apply_table(os2_table)
|
643
|
+
fm.save(filename)
|
644
|
+
end
|
645
|
+
|
646
|
+
# Removes all name records but except ids 0 through 6. Removes all name
|
647
|
+
# records with platform records other than Windows and Mac. Will retain
|
648
|
+
# any required strings referenced from other tables, e.g. STAT table.
|
649
|
+
#
|
650
|
+
# Shortens all remaining fields to 5000 characters, to prevent issues on
|
651
|
+
# Windows platforms.
|
652
|
+
#
|
653
|
+
# filename - The filename of the font to strip.
|
654
|
+
#
|
655
|
+
# Returns nothing.
|
656
|
+
def strip(filename)
|
657
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
658
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
659
|
+
|
660
|
+
stat_strings = fm.referenced_string_IDs
|
661
|
+
name_data = fm.get_table("name")
|
662
|
+
name_table = Skytype::NameTable.new(name_data)
|
663
|
+
name_table.strip_strings(5000, [ Skytype::NameTable::EN_IANA_TAG,
|
664
|
+
Skytype::NameTable::FR_IANA_TAG,
|
665
|
+
Skytype::NameTable::JP_IANA_TAG ], stat_strings)
|
666
|
+
|
667
|
+
fm.apply_table(name_table)
|
668
|
+
fm.save(filename)
|
669
|
+
end
|
670
|
+
|
671
|
+
# This is the protection method that we use for raw fonts. With these
|
672
|
+
# settings you can't install fonts into Windows or OS X.
|
673
|
+
#
|
674
|
+
# filename - The filename of the font to modify.
|
675
|
+
# unique_id - The string to place in the unique id field
|
676
|
+
#
|
677
|
+
# Returns nothing.
|
678
|
+
def obfuscate_names(filename, unique_id)
|
679
|
+
ProcessFontJob.log(@font_base_id, "N/A", "Obfuscating #{filename}")
|
680
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
681
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
682
|
+
|
683
|
+
name_data = fm.get_table("name")
|
684
|
+
name_table = Skytype::NameTable.new(name_data)
|
685
|
+
name_table.add_or_replace_string("", FAMILY_ID)
|
686
|
+
name_table.add_or_replace_string("Regular", SUBFAMILY_ID)
|
687
|
+
name_table.add_or_replace_string(unique_id, UNIQUE_ID)
|
688
|
+
name_table.add_or_replace_string("-", FULLNAME_ID)
|
689
|
+
name_table.add_or_replace_string("-", POSTSCRIPT_NAME_ID)
|
690
|
+
|
691
|
+
fm.apply_table(name_table)
|
692
|
+
fm.save(filename)
|
693
|
+
end
|
694
|
+
|
695
|
+
# Adds a name record for both Macintosh and Windows platforms to the name
|
696
|
+
# table of the given font.
|
697
|
+
#
|
698
|
+
# Note: If the font record already exists, the resulting font will contain
|
699
|
+
# both records and it's behavior is undefined, so it's best to always
|
700
|
+
# remove the other record first.
|
701
|
+
#
|
702
|
+
# id - The id of the name record to add.
|
703
|
+
# filename - The filename of the font to modify.
|
704
|
+
# string - The value of the new name record.
|
705
|
+
#
|
706
|
+
# Returns nothing.
|
707
|
+
def add_name_record(id, filename, string)
|
708
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
709
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
710
|
+
name_data = fm.get_table("name")
|
711
|
+
name_table = Skytype::NameTable.new(name_data)
|
712
|
+
|
713
|
+
name_table.add_or_replace_string(string, id)
|
714
|
+
|
715
|
+
fm.apply_table(name_table)
|
716
|
+
fm.save(filename)
|
717
|
+
end
|
718
|
+
|
719
|
+
# Copies a single name table record within a font.
|
720
|
+
#
|
721
|
+
# filename - The filename of the font to modify.
|
722
|
+
# source_id - The id of the name record to copy.
|
723
|
+
# destination_id - The id of the name record to replace.
|
724
|
+
#
|
725
|
+
# Returns nothing.
|
726
|
+
def copy_name_record(filename, source_id, destination_id)
|
727
|
+
fm = Skytype::FontManipulator.new(File.binread(filename))
|
728
|
+
raise StandardError, "ERROR: Failed to create FontManipulator" if fm.nil?
|
729
|
+
|
730
|
+
name_data = fm.get_table("name")
|
731
|
+
name_table = Skytype::NameTable.new(name_data)
|
732
|
+
|
733
|
+
dest_string = name_table.get_string(source_id, Skytype::NameTable::EN_LANGUAGE_ID)
|
734
|
+
return if dest_string.nil?
|
735
|
+
|
736
|
+
name_table.add_or_replace_string(dest_string, destination_id)
|
737
|
+
fm.apply_table(name_table)
|
738
|
+
fm.save(filename)
|
739
|
+
end
|
740
|
+
|
741
|
+
|
742
|
+
def preverify_format_data(formats)
|
743
|
+
if formats['process_original'] == false && formats['convert'] == false && formats['derivatives'] == []
|
744
|
+
# Nothing is going to be produces, this is not OK
|
745
|
+
return false
|
746
|
+
elsif original_font_format.outline_format == :cff && formats['convert'] == false && formats['derivatives'] == ["svg"]
|
747
|
+
# We cannot produce a SVG unless we have a TTF outline, so if no conversion and only svg, we'll produce nothing.
|
748
|
+
return false
|
749
|
+
end
|
750
|
+
return true
|
751
|
+
end
|
752
|
+
|
753
|
+
# Pubilc: Maps a Unicode value string to an integer
|
754
|
+
#
|
755
|
+
# original_unicode - a numeric string in decimal, Hex or Unicode (U+) format
|
756
|
+
#
|
757
|
+
# Raises InputError if argument contains characters which are not 0-9 or a-f
|
758
|
+
# Raises InputError if argument contains Hex "x" char in an invalid location
|
759
|
+
#
|
760
|
+
# Returns an integer
|
761
|
+
def map_uni_value_to_int(original_unicode)
|
762
|
+
# for sanity, make it all downcase, keeping the orginal for error messages
|
763
|
+
unicode = original_unicode.downcase
|
764
|
+
|
765
|
+
# if in Unicode format, convert to Hex
|
766
|
+
if unicode.start_with? "u+"
|
767
|
+
unicode.sub!("u+", "0x");
|
768
|
+
end
|
769
|
+
|
770
|
+
#validate that the entire string just 0-9, a-f or x
|
771
|
+
if not unicode[/[^x0123456789abcdef]/].nil?
|
772
|
+
raise InputError, "ERROR: Invalid Unicode numeric value passed! unicode string = #{original_unicode}."
|
773
|
+
end
|
774
|
+
|
775
|
+
#validate that if there is an x, that it is the second char
|
776
|
+
if unicode["x"]
|
777
|
+
raise InputError, "ERROR: Invalid Unicode numeric value passed! unicode string = #{original_unicode}." unless unicode[1] == 'x'
|
778
|
+
end
|
779
|
+
|
780
|
+
unicode.strip!
|
781
|
+
|
782
|
+
# if Hex, convert to hex, then to int
|
783
|
+
if unicode[/[abcdefx]/]
|
784
|
+
unicode.hex.to_i
|
785
|
+
else
|
786
|
+
unicode.to_i
|
787
|
+
end
|
788
|
+
end
|
789
|
+
end
|
790
|
+
end
|