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.
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