| #!/usr/bin/env python |
| # |
| # Copyright 2013 Google, Inc. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| # Google Author(s): Behdad Esfahbod, Stuart Gill, Roozbeh Pournader |
| # |
| |
| |
| from __future__ import print_function |
| import sys, struct |
| from png import PNG |
| import os |
| from os import path |
| |
| from nototools import font_data |
| |
| |
| try: |
| unichr # py2 |
| except NameError: |
| unichr = chr # py3 |
| |
| def get_glyph_name_from_gsub (string, font, cmap_dict): |
| ligatures = font['GSUB'].table.LookupList.Lookup[0].SubTable[0].ligatures |
| first_glyph = cmap_dict[ord (string[0])] |
| rest_of_glyphs = [cmap_dict[ord (ch)] for ch in string[1:]] |
| for ligature in ligatures[first_glyph]: |
| if ligature.Component == rest_of_glyphs: |
| return ligature.LigGlyph |
| |
| |
| def div (a, b): |
| return int (round (a / float (b))) |
| |
| class FontMetrics: |
| def __init__ (self, upem, ascent, descent): |
| self.upem = upem |
| self.ascent = ascent |
| self.descent = descent |
| |
| class StrikeMetrics: |
| def __init__ (self, font_metrics, advance, bitmap_width, bitmap_height): |
| self.width = bitmap_width # in pixels |
| self.height = bitmap_height # in pixels |
| self.advance = advance # in font units |
| self.x_ppem = self.y_ppem = div (bitmap_width * font_metrics.upem, advance) |
| |
| class GlyphMap: |
| def __init__ (self, glyph, offset, image_format): |
| self.glyph = glyph |
| self.offset = offset |
| self.image_format = image_format |
| |
| |
| # Based on http://www.microsoft.com/typography/otspec/ebdt.htm |
| class CBDT: |
| |
| def __init__ (self, font_metrics, options = (), stream = None): |
| self.stream = stream if stream != None else bytearray () |
| self.options = options |
| self.font_metrics = font_metrics |
| self.base_offset = 0 |
| self.base_offset = self.tell () |
| |
| def tell (self): |
| return len (self.stream) - self.base_offset |
| def write (self, data): |
| self.stream.extend (data) |
| def data (self): |
| return self.stream |
| |
| def write_header (self): |
| self.write (struct.pack (">L", 0x00030000)) # FIXED version |
| |
| def start_strike (self, strike_metrics): |
| self.strike_metrics = strike_metrics |
| self.glyph_maps = [] |
| |
| def write_glyphs (self, glyphs, glyph_filenames, image_format): |
| |
| write_func = self.image_write_func (image_format) |
| for glyph in glyphs: |
| img_file = glyph_filenames[glyph] |
| # print 'writing data for glyph %s' % path.basename(img_file) |
| offset = self.tell () |
| write_func (PNG (img_file)) |
| self.glyph_maps.append (GlyphMap (glyph, offset, image_format)) |
| |
| def end_strike (self): |
| |
| self.glyph_maps.append (GlyphMap (None, self.tell (), None)) |
| glyph_maps = self.glyph_maps |
| del self.glyph_maps |
| del self.strike_metrics |
| return glyph_maps |
| |
| def write_glyphMetrics (self, width, height, big_metrics): |
| |
| ascent = self.font_metrics.ascent |
| descent = self.font_metrics.descent |
| upem = self.font_metrics.upem |
| y_ppem = self.strike_metrics.y_ppem |
| |
| x_bearing = 0 |
| # center vertically |
| line_height = (ascent + descent) * y_ppem / float (upem) |
| line_ascent = ascent * y_ppem / float (upem) |
| y_bearing = int (round (line_ascent - .5 * (line_height - height))) |
| # fudge y_bearing if calculations are a bit off |
| if y_bearing == 128: |
| y_bearing = 127 |
| advance = width |
| |
| vert_x_bearing = - width / 2 |
| vert_y_bearing = 0 |
| vert_advance = height |
| |
| # print "big glyph metrics h: %d w: %d" % (height, width) |
| # smallGlyphMetrics |
| # Type Name |
| # BYTE height |
| # BYTE width |
| # CHAR horiBearingX |
| # CHAR horiBearingY |
| # BYTE horiAdvance |
| # add for bigGlyphMetrics: |
| # CHAR vertBearingX |
| # CHAR vertBearingY |
| # BYTE vertAdvance |
| try: |
| if big_metrics: |
| self.write (struct.pack ("BBbbBbbB", |
| height, width, |
| x_bearing, y_bearing, |
| advance, |
| vert_x_bearing, vert_y_bearing, |
| vert_advance)) |
| else: |
| self.write (struct.pack ("BBbbB", |
| height, width, |
| x_bearing, y_bearing, |
| advance)) |
| except Exception as e: |
| raise ValueError("%s, h: %d w: %d x: %d y: %d %d a:" % ( |
| e, height, width, x_bearing, y_bearing, advance)) |
| |
| def write_format1 (self, png): |
| |
| import cairo |
| img = cairo.ImageSurface.create_from_png (png.stream ()) |
| if img.get_format () != cairo.FORMAT_ARGB32: |
| raise Exception ("Expected FORMAT_ARGB32, but image has format %d" % img.get_format ()) |
| |
| width = img.get_width () |
| height = img.get_height () |
| stride = img.get_stride () |
| data = img.get_data () |
| |
| self.write_smallGlyphMetrics (width, height) |
| |
| if sys.byteorder == "little" and stride == width * 4: |
| # Sweet. Data is in desired format, ship it! |
| self.write (data) |
| return |
| |
| # Unexpected stride or endianness, do it the slow way |
| offset = 0 |
| for y in range (height): |
| for x in range (width): |
| pixel = data[offset + 4 * x: offset + 4 * (x + 1)] |
| # Convert to little endian |
| pixel = struct.pack ("<I", struct.unpack ("@I", pixel)[0]) |
| self.write (pixel) |
| offset += stride |
| |
| png_allowed_chunks = [b"IHDR", b"PLTE", b"tRNS", b"sRGB", b"IDAT", b"IEND"] |
| |
| def write_format17 (self, png): |
| self.write_format17or18(png, False) |
| |
| def write_format18 (self, png): |
| self.write_format17or18(png, True) |
| |
| def write_format17or18 (self, png, big_metrics): |
| width, height = png.get_size () |
| |
| if 'keep_chunks' not in self.options: |
| png = png.filter_chunks (self.png_allowed_chunks) |
| |
| self.write_glyphMetrics (width, height, big_metrics) |
| |
| png_data = png.data () |
| # ULONG data length |
| self.write (struct.pack(">L", len (png_data))) |
| self.write (png_data) |
| |
| def image_write_func (self, image_format): |
| if image_format == 1: return self.write_format1 |
| if image_format == 17: return self.write_format17 |
| if image_format == 18: return self.write_format18 |
| return None |
| |
| |
| # Based on http://www.microsoft.com/typography/otspec/eblc.htm |
| class CBLC: |
| |
| def __init__ (self, font_metrics, options = (), stream = None): |
| self.stream = stream if stream != None else bytearray () |
| self.streams = [] |
| self.options = options |
| self.font_metrics = font_metrics |
| self.base_offset = 0 |
| self.base_offset = self.tell () |
| |
| def tell (self): |
| return len (self.stream) - self.base_offset |
| def write (self, data): |
| self.stream.extend (data) |
| def data (self): |
| return self.stream |
| def push_stream (self, stream): |
| self.streams.append (self.stream) |
| self.stream = stream |
| def pop_stream (self): |
| stream = self.stream |
| self.stream = self.streams.pop () |
| return stream |
| |
| def write_header (self): |
| self.write (struct.pack (">L", 0x00030000)) # FIXED version |
| |
| def start_strikes (self, num_strikes): |
| self.num_strikes = num_strikes |
| self.write (struct.pack (">L", self.num_strikes)) # ULONG numSizes |
| self.bitmapSizeTables = bytearray () |
| self.otherTables = bytearray () |
| |
| def write_strike (self, strike_metrics, glyph_maps): |
| self.strike_metrics = strike_metrics |
| self.write_bitmapSizeTable (glyph_maps) |
| del self.strike_metrics |
| |
| def end_strikes (self): |
| self.write (self.bitmapSizeTables) |
| self.write (self.otherTables) |
| del self.bitmapSizeTables |
| del self.otherTables |
| |
| def write_sbitLineMetrics_hori (self): |
| |
| ascent = self.font_metrics.ascent |
| descent = self.font_metrics.descent |
| upem = self.font_metrics.upem |
| y_ppem = self.strike_metrics.y_ppem |
| |
| # sbitLineMetrics |
| # Type Name |
| # CHAR ascender |
| # CHAR descender |
| # BYTE widthMax |
| # CHAR caretSlopeNumerator |
| # CHAR caretSlopeDenominator |
| # CHAR caretOffset |
| # CHAR minOriginSB |
| # CHAR minAdvanceSB |
| # CHAR maxBeforeBL |
| # CHAR minAfterBL |
| # CHAR pad1 |
| # CHAR pad2 |
| line_height = div ((ascent + descent) * y_ppem, upem) |
| ascent = div (ascent * y_ppem, upem) |
| descent = - (line_height - ascent) |
| self.write (struct.pack ("bbBbbbbbbbbb", |
| ascent, descent, |
| self.strike_metrics.width, |
| 0, 0, 0, |
| 0, 0, 0, 0, # TODO |
| 0, 0)) |
| |
| def write_sbitLineMetrics_vert (self): |
| self.write_sbitLineMetrics_hori () # XXX |
| |
| def write_indexSubTable1 (self, glyph_maps): |
| |
| image_format = glyph_maps[0].image_format |
| |
| self.write (struct.pack(">H", 1)) # USHORT indexFormat |
| self.write (struct.pack(">H", image_format)) # USHORT imageFormat |
| imageDataOffset = glyph_maps[0].offset |
| self.write (struct.pack(">L", imageDataOffset)) # ULONG imageDataOffset |
| for gmap in glyph_maps[:-1]: |
| self.write (struct.pack(">L", gmap.offset - imageDataOffset)) # ULONG offsetArray |
| assert gmap.image_format == image_format |
| self.write (struct.pack(">L", glyph_maps[-1].offset - imageDataOffset)) |
| |
| def write_bitmapSizeTable (self, glyph_maps): |
| |
| # count number of ranges |
| count = 1 |
| start = glyph_maps[0].glyph |
| last_glyph = start |
| last_image_format = glyph_maps[0].image_format |
| for gmap in glyph_maps[1:-1]: |
| if last_glyph + 1 != gmap.glyph or last_image_format != gmap.image_format: |
| count += 1 |
| last_glyph = gmap.glyph |
| last_image_format = gmap.image_format |
| headersLen = count * 8 |
| |
| headers = bytearray () |
| subtables = bytearray () |
| start = glyph_maps[0].glyph |
| start_id = 0 |
| last_glyph = start |
| last_image_format = glyph_maps[0].image_format |
| last_id = 0 |
| for gmap in glyph_maps[1:-1]: |
| if last_glyph + 1 != gmap.glyph or last_image_format != gmap.image_format: |
| headers.extend (struct.pack(">HHL", start, last_glyph, headersLen + len (subtables))) |
| self.push_stream (subtables) |
| self.write_indexSubTable1 (glyph_maps[start_id:last_id+2]) |
| self.pop_stream () |
| |
| start = gmap.glyph |
| start_id = last_id + 1 |
| last_glyph = gmap.glyph |
| last_image_format = gmap.image_format |
| last_id += 1 |
| headers.extend (struct.pack(">HHL", start, last_glyph, headersLen + len (subtables))) |
| self.push_stream (subtables) |
| self.write_indexSubTable1 (glyph_maps[start_id:last_id+2]) |
| self.pop_stream () |
| |
| indexTablesSize = len (headers) + len (subtables) |
| numberOfIndexSubTables = count |
| bitmapSizeTableSize = 48 * self.num_strikes |
| |
| indexSubTableArrayOffset = 8 + bitmapSizeTableSize + len (self.otherTables) |
| |
| self.push_stream (self.bitmapSizeTables) |
| # bitmapSizeTable |
| # Type Name Description |
| # ULONG indexSubTableArrayOffset offset to index subtable from beginning of CBLC. |
| self.write (struct.pack(">L", indexSubTableArrayOffset)) |
| # ULONG indexTablesSize number of bytes in corresponding index subtables and array |
| self.write (struct.pack(">L", indexTablesSize)) |
| # ULONG numberOfIndexSubTables an index subtable for each range or format change |
| self.write (struct.pack(">L", numberOfIndexSubTables)) |
| # ULONG colorRef not used; set to 0. |
| self.write (struct.pack(">L", 0)) |
| # sbitLineMetrics hori line metrics for text rendered horizontally |
| self.write_sbitLineMetrics_hori () |
| self.write_sbitLineMetrics_vert () |
| # sbitLineMetrics vert line metrics for text rendered vertically |
| # USHORT startGlyphIndex lowest glyph index for this size |
| self.write (struct.pack(">H", glyph_maps[0].glyph)) |
| # USHORT endGlyphIndex highest glyph index for this size |
| self.write (struct.pack(">H", glyph_maps[-2].glyph)) |
| # BYTE ppemX horizontal pixels per Em |
| self.write (struct.pack(">B", self.strike_metrics.x_ppem)) |
| # BYTE ppemY vertical pixels per Em |
| self.write (struct.pack(">B", self.strike_metrics.y_ppem)) |
| # BYTE bitDepth the Microsoft rasterizer v.1.7 or greater supports the |
| # following bitDepth values, as described below: 1, 2, 4, and 8. |
| self.write (struct.pack(">B", 32)) |
| # CHAR flags vertical or horizontal (see bitmapFlags) |
| self.write (struct.pack(">b", 0x01)) |
| self.pop_stream () |
| |
| self.push_stream (self.otherTables) |
| self.write (headers) |
| self.write (subtables) |
| self.pop_stream () |
| |
| |
| def main (argv): |
| import glob |
| from fontTools import ttx, ttLib |
| |
| options = [] |
| |
| option_map = { |
| "-V": "verbose", |
| "-O": "keep_outlines", |
| "-U": "uncompressed", |
| "-S": "small_glyph_metrics", |
| "-C": "keep_chunks", |
| } |
| |
| for key, value in option_map.items (): |
| if key in argv: |
| options.append (value) |
| argv.remove (key) |
| |
| if len (argv) < 4: |
| print(""" |
| Usage: |
| |
| emoji_builder.py [-V] [-O] [-U] [-S] [-A] font.ttf out-font.ttf strike-prefix... |
| |
| This will search for files that have strike-prefix followed |
| by a hex number, and end in ".png". For example, if strike-prefix |
| is "icons/uni", then files with names like "icons/uni1f4A9.png" will |
| be loaded. All images for the same strike should have the same size |
| for best results. |
| |
| If multiple strike-prefix parameters are provided, multiple |
| strikes will be embedded, in the order provided. |
| |
| The script then embeds color bitmaps in the font, for characters |
| that the font already supports, and writes the new font out. |
| |
| If -V is given, verbose mode is enabled. |
| |
| If -U is given, uncompressed images are stored (imageFormat=1). |
| |
| If -S is given, PNG images are stored with small glyph metrics (imageFormat=17). |
| |
| By default, PNG images are stored with big glyph metrics (imageFormat=18). |
| |
| If -O is given, the outline tables ('glyf', 'CFF ') and |
| related tables are NOT dropped from the font. |
| By default they are dropped. |
| |
| If -C is given, unused chunks (color profile, etc) are NOT |
| dropped from the PNG images when embedding. |
| By default they are dropped. |
| """, file=sys.stderr) |
| sys.exit (1) |
| |
| font_file = argv[1] |
| out_file = argv[2] |
| img_prefixes = argv[3:] |
| del argv |
| |
| def add_font_table (font, tag, data): |
| tab = ttLib.tables.DefaultTable.DefaultTable (tag) |
| tab.data = data |
| font[tag] = tab |
| |
| def drop_outline_tables (font): |
| for tag in ['cvt ', 'fpgm', 'glyf', 'loca', 'prep', 'CFF ', 'VORG']: |
| try: |
| del font[tag] |
| except KeyError: |
| pass |
| |
| |
| print() |
| |
| font = ttx.TTFont (font_file) |
| print("Loaded font '%s'." % font_file) |
| |
| font_metrics = FontMetrics (font['head'].unitsPerEm, |
| font['hhea'].ascent, |
| -font['hhea'].descent) |
| print("Font metrics: upem=%d ascent=%d descent=%d." % \ |
| (font_metrics.upem, font_metrics.ascent, font_metrics.descent)) |
| glyph_metrics = font['hmtx'].metrics |
| unicode_cmap = font['cmap'].getcmap (3, 10) |
| if not unicode_cmap: |
| unicode_cmap = font['cmap'].getcmap (3, 1) |
| if not unicode_cmap: |
| raise Exception ("Failed to find a Unicode cmap.") |
| |
| image_format = 1 if 'uncompressed' in options else (17 |
| if 'small_glyph_metrics' in options else 18) |
| |
| ebdt = CBDT (font_metrics, options) |
| ebdt.write_header () |
| eblc = CBLC (font_metrics, options) |
| eblc.write_header () |
| eblc.start_strikes (len (img_prefixes)) |
| |
| def is_vs(cp): |
| return cp >= 0xfe00 and cp <= 0xfe0f |
| |
| for img_prefix in img_prefixes: |
| print() |
| |
| img_files = {} |
| glb = "%s*.png" % img_prefix |
| print("Looking for images matching '%s'." % glb) |
| for img_file in glob.glob (glb): |
| codes = img_file[len (img_prefix):-4] |
| if "_" in codes: |
| pieces = codes.split ("_") |
| cps = [int(code, 16) for code in pieces] |
| uchars = "".join (unichr(cp) for cp in cps if not is_vs(cp)) |
| else: |
| cp = int(codes, 16) |
| if is_vs(cp): |
| print("ignoring unexpected vs input %04x" % cp) |
| continue |
| uchars = unichr(cp) |
| img_files[uchars] = img_file |
| if not img_files: |
| raise Exception ("No image files found in '%s'." % glb) |
| print("Found images for %d characters in '%s'." % (len (img_files), glb)) |
| |
| glyph_imgs = {} |
| advance = width = height = 0 |
| for uchars, img_file in img_files.items (): |
| if len (uchars) == 1: |
| try: |
| glyph_name = unicode_cmap.cmap[ord (uchars)] |
| except: |
| print("no cmap entry for %x" % ord(uchars)) |
| raise ValueError("%x" % ord(uchars)) |
| else: |
| glyph_name = get_glyph_name_from_gsub (uchars, font, unicode_cmap.cmap) |
| glyph_id = font.getGlyphID (glyph_name) |
| glyph_imgs[glyph_id] = img_file |
| if "verbose" in options: |
| uchars_name = ",".join (["%04X" % ord (char) for char in uchars]) |
| # print "Matched U+%s: id=%d name=%s image=%s" % ( |
| # uchars_name, glyph_id, glyph_name, img_file) |
| |
| advance += glyph_metrics[glyph_name][0] |
| w, h = PNG (img_file).get_size () |
| width += w |
| height += h |
| |
| glyphs = sorted (glyph_imgs.keys ()) |
| if not glyphs: |
| raise Exception ("No common characters found between font and '%s'." % glb) |
| print("Embedding images for %d glyphs for this strike." % len (glyphs)) |
| |
| advance, width, height = (div (x, len (glyphs)) for x in (advance, width, height)) |
| strike_metrics = StrikeMetrics (font_metrics, advance, width, height) |
| print("Strike ppem set to %d." % (strike_metrics.y_ppem)) |
| |
| ebdt.start_strike (strike_metrics) |
| ebdt.write_glyphs (glyphs, glyph_imgs, image_format) |
| glyph_maps = ebdt.end_strike () |
| |
| eblc.write_strike (strike_metrics, glyph_maps) |
| |
| print() |
| |
| ebdt = ebdt.data () |
| add_font_table (font, 'CBDT', ebdt) |
| print("CBDT table synthesized: %d bytes." % len (ebdt)) |
| eblc.end_strikes () |
| eblc = eblc.data () |
| add_font_table (font, 'CBLC', eblc) |
| print("CBLC table synthesized: %d bytes." % len (eblc)) |
| |
| print() |
| |
| if 'keep_outlines' not in options: |
| drop_outline_tables (font) |
| print("Dropped outline ('glyf', 'CFF ') and related tables.") |
| |
| # hack removal of cmap pua entry for unknown flag glyph. If we try to |
| # remove it earlier, getGlyphID dies. Need to restructure all of this |
| # code. |
| font_data.delete_from_cmap(font, [0xfe82b]) |
| font.save (out_file) |
| print("Output font '%s' generated." % out_file) |
| |
| |
| if __name__ == '__main__': |
| main (sys.argv) |