fontprocessor 27.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rbenv-version +1 -0
  4. data/.rspec +0 -0
  5. data/.travis.yml +17 -0
  6. data/CHANGELOG.md +64 -0
  7. data/CHANGELOG_FPV14.md +20 -0
  8. data/Dockerfile +3 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +156 -0
  11. data/Guardfile +5 -0
  12. data/README.md +6 -0
  13. data/Rakefile +53 -0
  14. data/Rakefile.base +57 -0
  15. data/bin/process-file +41 -0
  16. data/config/development.yml +5 -0
  17. data/config/production.yml +3 -0
  18. data/config/staging.yml +3 -0
  19. data/config/test.yml +5 -0
  20. data/fontprocessor.gemspec +37 -0
  21. data/lib/fontprocessor/config.rb +85 -0
  22. data/lib/fontprocessor/external/batik/batik-ttf2svg.jar +0 -0
  23. data/lib/fontprocessor/external/batik/lib/batik-svggen.jar +0 -0
  24. data/lib/fontprocessor/external/batik/lib/batik-util.jar +0 -0
  25. data/lib/fontprocessor/external/fontforge/subset.py +30 -0
  26. data/lib/fontprocessor/external/fontforge/utils.py +66 -0
  27. data/lib/fontprocessor/external_execution.rb +117 -0
  28. data/lib/fontprocessor/font_file.rb +149 -0
  29. data/lib/fontprocessor/font_file_naming_strategy.rb +63 -0
  30. data/lib/fontprocessor/font_format.rb +29 -0
  31. data/lib/fontprocessor/process_font_job.rb +227 -0
  32. data/lib/fontprocessor/processed_font_iterator.rb +89 -0
  33. data/lib/fontprocessor/processor.rb +790 -0
  34. data/lib/fontprocessor/version.rb +3 -0
  35. data/lib/fontprocessor.rb +16 -0
  36. data/scripts/build_and_test.sh +15 -0
  37. data/scripts/get_production_source_map.rb +53 -0
  38. data/spec/fixtures/bad_os2_width_class.otf +0 -0
  39. data/spec/fixtures/extra_language_names.otf +0 -0
  40. data/spec/fixtures/fixtures.rb +35 -0
  41. data/spec/fixtures/locked.otf +0 -0
  42. data/spec/fixtures/op_size.otf +0 -0
  43. data/spec/fixtures/ots_failure_font.otf +0 -0
  44. data/spec/fixtures/postscript.otf +0 -0
  45. data/spec/fixtures/stat_font.otf +0 -0
  46. data/spec/fixtures/truetype.otf +0 -0
  47. data/spec/lib/fontprocessor/config_spec.rb +38 -0
  48. data/spec/lib/fontprocessor/external_execution_spec.rb +33 -0
  49. data/spec/lib/fontprocessor/font_file_naming_strategy_spec.rb +13 -0
  50. data/spec/lib/fontprocessor/font_file_spec.rb +110 -0
  51. data/spec/lib/fontprocessor/process_font_job_spec.rb +317 -0
  52. data/spec/lib/fontprocessor/processed_font_iterator_spec.rb +128 -0
  53. data/spec/lib/fontprocessor/processor_spec.rb +466 -0
  54. data/spec/spec_helper.rb +4 -0
  55. data/tasks/fonts.rake +30 -0
  56. data/worker.rb +23 -0
  57. 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