def main(args): filename, glyphs = args[0], args[1:] if not glyphs: glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal'] from fontTools.ttLib import TTFont font = TTFont(filename) glyphset = font.getGlyphSet() test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
def main(args): filename, glyphs = args[0], args[1:] if not glyphs: glyphs = [ 'e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal' ] from fontTools.ttLib import TTFont font = TTFont(filename) glyphset = font.getGlyphSet() test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
class FontSize(object): def __init__(self, filepath): self.ttfont = TTFont(filepath) self.filepath = filepath self.total_glyphs = 0 self.encoded_glyphs = 0 self.total_bytes = 0 self.per_enc_glyph_bytes = 0.0 self.per_total_glyph_bytes = 0.0 self.tables = {} self._calculate_sizes() def _calculate_sizes(self): self.total_bytes = os.path.getsize(self.filepath) self._calc_per_encoded_glyph_size() self._calc_per_total_glyph_size() self._calc_table_sizes() def _calc_per_encoded_glyph_size(self): self.encoded_glyphs = len(self.ttfont.getBestCmap()) self.per_enc_glyph_bytes = self.total_bytes / self.encoded_glyphs def _calc_per_total_glyph_size(self): self.total_glyphs = len(self.ttfont.getGlyphSet()) self.per_total_glyph_bytes = self.total_bytes / self.total_glyphs def _calc_table_sizes(self): tables = self.get_table_tags() for table in tables: self.tables[table] = self.ttfont.reader.tables[table].length def get_table_tags(self): return [tag for tag in self.ttfont.keys() if tag != "GlyphOrder"]
def initAvailableChars(self): self.chars = [] key = str(self.path.name) filename = 'cache/fonts/' + key + '.json' try: with open(filename, 'r') as f: cache = json.load(f) except: cache = dict() if 'ranges' in cache and str(TextGenerator.ranges) == cache['ranges']: self.chars = cache['chars'] else: font = TTFont(str(self.path)) glyphset = font.getGlyphSet() table = font.getBestCmap() for r in TextGenerator.ranges: #ugly check to know if char is supported by font self.chars += [ chr(x) for x in range(r[0], r[1] + 1) if x in table.keys() and (glyphset[table[x]]._glyph.bytecode != b' \x1d' if hasattr( glyphset[table[x]]._glyph, 'bytecode' ) else glyphset[table[x]]._glyph.numberOfContours > 0) ] cache = {'ranges': str(TextGenerator.ranges), 'chars': self.chars} with open(filename, 'w') as f: json.dump(cache, f) Fonts.total += len(self.chars)
class DetectIntersections(BaseDetectIntersections): def __init__(self, in_font): self.in_font = in_font self.font = TTFont(self.in_font) self._is_cjk = self.is_cjk() basename, ext = os.path.splitext(os.path.basename(in_font)) self.degree = 2 if ext.lower() == ".ttf" else 3 def run(self): for gname in self.font.getGlyphOrder(): intersections = self.detect(gname) if intersections: print "{}:".format(gname), ", ".join(["({}, {})".format(pt[0], pt[1]) for pt in intersections]) def detect(self, name): glyph = self.font.getGlyphSet()[name] pen = ConvPen(self.degree) glyph.draw(pen) return self.detect_intersections(pen.contours) def gid2name(self, gid): return self.font.getGlyphOrder()[gid] def is_cjk(self): if "CFF " not in self.font: return False return hasattr(self.font["CFF "].cff.topDictIndex[0], "ROS")
def main(): parser = argparse.ArgumentParser(description='something something webfonts') parser.add_argument('files', metavar='FILE', nargs='*') args = parser.parse_args() for file in args.files: font = TTFont(file) #for glyphName in font.getGlyphOrder(): # glyphTable = font["glyf"] # glyph = glyphTable.glyphs.get(glyphName) # glyph.expand(glyphTable) glyphs = font.getGlyphSet() for key in glyphs.keys(): value = glyphs[key] print("glyp width: " + key + ", " + str(value.width)) # glyph.recalcBounds(glyphTable) hmtxTable = font['hmtx'] correct_advance = (hmtxTable.metrics['A'][0], 0) for key, value in hmtxTable.metrics.items(): print("advance: " + key + ", " + str(value)) hmtxTable.metrics[key] = correct_advance name = os.path.splitext(file)[0] font.flavor = "woff" font.save(name + ".woff") font.flavor = "woff2" font.save(name + ".woff2")
def main(): start_json = json.load(sys.stdin) for font in start_json: fontInfo = TTFont("../../fonts/KaTeX_" + font + ".ttf") glyf = fontInfo["glyf"] widths = fontInfo.getGlyphSet() unitsPerEm = float(fontInfo["head"].unitsPerEm) # We keep ALL Unicode cmaps, not just fontInfo["cmap"].getcmap(3, 1). # This is playing it extra safe, since it reports inconsistencies. # Platform 0 is Unicode, platform 3 is Windows. For platform 3, # encoding 1 is UCS-2 and encoding 10 is UCS-4. cmap = [ t.cmap for t in fontInfo["cmap"].tables if (t.platformID == 0) or ( t.platformID == 3 and t.platEncID in (1, 10)) ] chars = metrics_to_extract.get(font, {}) chars[u"\u0020"] = None # space chars[u"\u00a0"] = None # nbsp for char, base_char in chars.items(): code = ord(char) names = set(t.get(code) for t in cmap) if not names: sys.stderr.write( "Codepoint {} of font {} maps to no name\n".format( code, font)) continue if len(names) != 1: sys.stderr.write( "Codepoint {} of font {} maps to multiple names: {}\n". format(code, font, ", ".join(sorted(names)))) continue name = names.pop() height = depth = italic = skew = width = 0 glyph = glyf[name] if glyph.numberOfContours: height = glyph.yMax / unitsPerEm depth = -glyph.yMin / unitsPerEm width = widths[name].width / unitsPerEm if base_char: base_char_str = str(ord(base_char)) base_metrics = start_json[font][base_char_str] italic = base_metrics["italic"] skew = base_metrics["skew"] width = base_metrics["width"] start_json[font][str(code)] = { "height": height, "depth": depth, "italic": italic, "skew": skew, "width": width } sys.stdout.write( json.dumps(start_json, separators=(',', ':'), sort_keys=True))
def test_check_whitespace_ink(): """ Whitespace glyphs have ink? """ from fontbakery.profiles.universal import com_google_fonts_check_whitespace_ink as check test_font = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) status, _ = list(check(test_font))[-1] assert status == PASS print ("Test for whitespace character having composites (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "uni1E17" status, _ = list(check(test_font))[-1] assert status == FAIL print ("Test for whitespace character having outlines (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "scedilla" status, _ = list(check(test_font))[-1] assert status == FAIL print ("Test for whitespace character having composites (without ink).") import fontTools.pens.ttGlyphPen pen = fontTools.pens.ttGlyphPen.TTGlyphPen(test_font.getGlyphSet()) pen.addComponent("space", (1, 0, 0, 1, 0, 0)) test_font["glyf"].glyphs["uni200B"] = pen.glyph() status, _ = list(check(test_font))[-1] assert status == FAIL
def fontToUFO(src, dst, fileType=None): from robofab.ufoLib import UFOWriter from robofab.pens.adapterPens import SegmentToPointPen if fileType is None: fileType = guessFileType(src) if fileType is None: raise ValueError, "Can't determine input file type" ufoWriter = UFOWriter(dst) if fileType == "TTF": from fontTools.ttLib import TTFont font = TTFont(src, 0) elif fileType == "Type 1": from fontTools.t1Lib import T1Font font = T1Font(src) else: assert 0, "unknown file type: %r" % fileType inGlyphSet = font.getGlyphSet() outGlyphSet = ufoWriter.getGlyphSet() for glyphName in inGlyphSet.keys(): print "-", glyphName glyph = inGlyphSet[glyphName] def drawPoints(pen): pen = SegmentToPointPen(pen) glyph.draw(pen) outGlyphSet.writeGlyph(glyphName, glyph, drawPoints) outGlyphSet.writeContents() if fileType == "TTF": info = extractTTFFontInfo(font) elif fileType == "Type 1": info = extractT1FontInfo(font) ufoWriter.writeInfo(info)
def generate_pic(glyphname, font: TTFont, size=120, scale=0.1): """ 生成图片 -> 预处理 -> 保存到本地 Args: glyphname: font: file_suffix: size: scale: Returns: """ gs = font.getGlyphSet() pen = ReportLabPen(gs, Path(fillColor=colors.black, strokeWidth=1)) g = gs[glyphname] g.draw(pen) w, h = size, size # Everything is wrapped in a group to allow transformations. g = Group(pen.path) # g.translate(10, 20) g.scale(scale, scale) d = Drawing(w, h) d.add(g) return renderPM.drawToPIL(d)
def img_save(): img_path = PATH.rsplit('.', 1)[0] font = TTFont( PATH) # it would work just as well with fontTools.t1Lib.T1Font glyf = font['glyf'] if not os.path.exists(img_path): os.makedirs(img_path) for glyphName in glyf.keys(): gs = font.getGlyphSet() pen = ReportLabPen(gs, Path(fillColor=colors.black, strokeWidth=1)) g = gs[glyphName] g.draw(pen) # 调整图片大小 w, h = 800, 800 g = Group(pen.path) g.translate(10, 200) g.scale(0.3, 0.3) d = Drawing(w, h) d.add(g) image = renderPM.drawToPIL(d) little_image = image.resize((180, 180)) fromImage = Image.new('RGBA', (360, 360), color=(255, 255, 255)) fromImage.paste(little_image, (0, 0)) # fromImage.show() glyphName = glyphName.replace('uni', '&#x') imageFile = img_path + "/%s.png" % glyphName fromImage.save(imageFile) break
def test_check_whitespace_ink(): """ Whitespace glyphs have ink? """ from fontbakery.profiles.universal import com_google_fonts_check_whitespace_ink as check test_font = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) status, _ = list(check(test_font))[-1] assert status == PASS print("Test for whitespace character having composites (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "uni1E17" status, _ = list(check(test_font))[-1] assert status == FAIL print("Test for whitespace character having outlines (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "scedilla" status, _ = list(check(test_font))[-1] assert status == FAIL print("Test for whitespace character having composites (without ink).") import fontTools.pens.ttGlyphPen pen = fontTools.pens.ttGlyphPen.TTGlyphPen(test_font.getGlyphSet()) pen.addComponent("space", (1, 0, 0, 1, 0, 0)) test_font["glyf"].glyphs["uni200B"] = pen.glyph() status, _ = list(check(test_font))[-1] assert status == FAIL
def ttCount(input, options): ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) reader = ttf.reader hasPrep = 'prep' in reader.tables hasFpgm = 'fpgm' in reader.tables glyf_program_counts = 0 glyf_names = ttf.getGlyphSet().keys() for glyf_name in glyf_names: glyf = ttf['glyf'].glyphs[glyf_name] glyf.expand(ttf['glyf']) if hasattr(ttf['glyf'].glyphs[glyf_name], "program"): prog = ttf['glyf'].glyphs[glyf_name].program if (hasattr(prog, "bytecode") and len(prog.bytecode) > 0) or (hasattr(prog, "assembly") and len(prog.assembly) > 0): glyf_program_counts += 1 #print ("%s %s" % (glyf, hasattr(ttf['glyf'].glyphs[glyf_name], "program"))) hasSomeGlyfCode = glyf_program_counts > 0 globalAnswer = hasPrep or hasFpgm or hasSomeGlyfCode print ("%s: %s, prep = %s, fpgm = %s, glyf = %s [%d/%d]" % (input, yes_or_no(globalAnswer), yes_or_no(hasPrep), yes_or_no(hasFpgm), yes_or_no(hasSomeGlyfCode), glyf_program_counts, len(glyf_names))) ttf.close()
def test_check_049(): """ Whitespace glyphs have ink? """ from fontbakery.specifications.general import com_google_fonts_check_049 as check test_font = TTFont( os.path.join("data", "test", "nunito", "Nunito-Regular.ttf")) status, _ = list(check(test_font))[-1] assert status == PASS print("Test for whitespace character having composites (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "uni1E17" status, _ = list(check(test_font))[-1] assert status == FAIL print("Test for whitespace character having outlines (with ink).") test_font["cmap"].tables[0].cmap[0x0020] = "scedilla" status, _ = list(check(test_font))[-1] assert status == FAIL print("Test for whitespace character having composites (without ink).") import fontTools.pens.ttGlyphPen pen = fontTools.pens.ttGlyphPen.TTGlyphPen(test_font.getGlyphSet()) pen.addComponent("space", (1, 0, 0, 1, 0, 0)) test_font["glyf"].glyphs["uni200B"] = pen.glyph() status, _ = list(check(test_font))[-1] assert status == FAIL
def _init_glyphs_mapping(self): font = TTFont(io.BytesIO(requests.get(SAMPLE_FONT_URL).content)) glyph_set = font.getGlyphSet() glyphs = glyph_set._glyphs.glyphs self.glyphs_mapping = {} for uni, number in SAMPLE_FONT_MAPPING.items(): self.glyphs_mapping[glyphs[uni].data] = number
def test_check_whitespace_ink(): """ Whitespace glyphs have ink? """ check = CheckTester(universal_profile, "com.google.fonts/check/whitespace_ink") test_font = TTFont(TEST_FILE("nunito/Nunito-Regular.ttf")) assert_PASS(check(test_font)) test_font["cmap"].tables[0].cmap[0x0020] = "uni1E17" assert_results_contain( check(test_font), FAIL, 'has-ink', 'for whitespace character having composites (with ink).') test_font["cmap"].tables[0].cmap[0x0020] = "scedilla" assert_results_contain( check(test_font), FAIL, 'has-ink', 'for whitespace character having outlines (with ink).') import fontTools.pens.ttGlyphPen pen = fontTools.pens.ttGlyphPen.TTGlyphPen(test_font.getGlyphSet()) pen.addComponent("space", (1, 0, 0, 1, 0, 0)) test_font["glyf"].glyphs["uni200B"] = pen.glyph() assert_results_contain( check(test_font), FAIL, 'has-ink', # should we give is a separate keyword? This looks wrong. 'for whitespace character having composites (without ink).')
def run(self): font = TTFont(self.in_font) gs = font.getGlyphSet() glyf = font["glyf"] hmtx = font["hmtx"] for gname, (adw, lsb) in hmtx.metrics.items(): # https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/ttLib/tables/_g_l_y_f.py#L189-L192 # __getitem__ internally calls table__g_l_y_f.expand which is essential to obtain xMin g = glyf[gname] # obtain recalculated xMin from glyph's control bounds g.recalcBounds(glyf) hmtx.metrics[gname] = (adw, g.xMin) if self.update_vmtx: from fontTools.pens.boundsPen import BoundsPen vmtx = font["vmtx"] for gname, (adh, tsb) in vmtx.metrics.items(): g = glyf[gname] # obtain yMax g.recalcBounds(glyf) pen = BoundsPen(gs) g.draw(pen, glyf) if pen.bounds is None: continue left, bottom, right, top = pen.bounds vmtx.metrics[gname] = (adh, top + tsb - g.yMax) font.save(self.out_font) return 0
def main(font_path): font = TTFont(font_path, recalcBBoxes=False) # Force yMin and yMax font['head'].yMin = YMIN font['head'].yMax = YMAX # Enable Bold bits for Black styles if "Black" in font_path and "fvar" not in font: if "Italic" in font_path: font["OS/2"].fsSelection |= 32 else: font["OS/2"].fsSelection ^= 64 | 32 font["head"].macStyle |= 1 # turn off round-to-grid flags in certain problem components # https://github.com/google/roboto/issues/153 glyph_set = font.getGlyphSet() ellipsis = glyph_set['ellipsis']._glyph for component in ellipsis.components: component.flags &= ~(1 << 2) font_data.delete_from_cmap( font, [ 0x20E3, # COMBINING ENCLOSING KEYCAP 0x2191, # UPWARDS ARROW 0x2193, # DOWNWARDS ARROW ]) font.save(font_path)
def _colr_v1_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]: glyph_set = ttfont.getGlyphSet() return { g.BaseGlyph: SVG.fromstring( etree.tostring( _colr_v1_glyph_to_svg(ttfont, glyph_set, view_box, g))) for g in ttfont["COLR"].table.BaseGlyphList.BaseGlyphPaintRecord }
def _colr_v0_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]: glyph_set = ttfont.getGlyphSet() return { g: SVG.fromstring( etree.tostring( _colr_v0_glyph_to_svg(ttfont, glyph_set, view_box, g))) for g in ttfont["COLR"].ColorLayers }
def fetchFontSpec(path): font = TTFont(path) cmap = font['cmap'] t = cmap.getcmap(3, 1).cmap s = font.getGlyphSet() unitsPerEm = font['head'].unitsPerEm return {'font': font, 't': t, 's': s, 'upm': unitsPerEm}
def __init__(self, fontName, pointSize): self.fontName = fontName self.pointSize = pointSize fontToolsFontFallback = TTFont(fontFallback) cmapFallback = fontToolsFontFallback['cmap'] fontToolsFont = TTFont(fontName) cmap = fontToolsFont['cmap'] self.t = cmap.getcmap(3, 1).cmap self.s = fontToolsFont.getGlyphSet() self.tFallback = cmapFallback.getcmap(3, 1).cmap self.sFallback = fontToolsFontFallback.getGlyphSet() self.units_per_emFallback = fontToolsFontFallback['head'].unitsPerEm self.units_per_em = fontToolsFont['head'].unitsPerEm
def generate_glyph_image(font): for index, label in enumerate(LABELS): bitmap_output_path = path.join(BITMAP_DIR, str(index) + '_' + label, font['post_script_name'] + ".png") if path.exists(bitmap_output_path): continue # bitmap canvas = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT), "black") draw = ImageDraw.Draw(canvas) ifont = ImageFont.truetype(font['path'], IMAGE_WIDTH - 10) w, h = draw.textsize(label, font=ifont) draw.text(((IMAGE_WIDTH - w) / 2, (IMAGE_HEIGHT - h) / 2), label, font=ifont, fill="white") canvas.save(bitmap_output_path) # vector vector_output_path = path.join(PREPROCESSED_DIR, font['post_script_name'] + '.ttx') if path.exists(vector_output_path): return ttfont = TTFont(font['path']) glyph_set = ttfont.getGlyphSet() cmap = ttfont.getBestCmap() ttfont.saveXML(vector_output_path) ascender = ttfont['OS/2'].sTypoAscender descender = ttfont['OS/2'].sTypoDescender height = ascender - descender for index, label in enumerate(LABELS): glyph_name = cmap[ord(label)] glyph = glyph_set[glyph_name] width = glyph.width pen = RecordingPen() glyph.draw(pen) # [[x, y, isPenDown, isControlPoint, isContourEnd, isGlyphEnd], ...] matrix = [] for command in pen.value: name = command[0] points = command[1] print('name:', name) print('points:', points) # if name == 'closePath': # pass # if name == 'moveTo': # matrix.append((points[0][0], points[0][1], 0, 0, 0, 0)) # elif name == 'qCurveTo': # matrix.append((points[0][0], points[0][1], 1, 1, 0, 0)) # matrix.append((points[1][0], points[1][1], 1, 0, 0, 0)) # elif name == 'lineTo': # matrix.append((points[1][0], points[1][1], 1, 0, 0, 0)) os.exit()
def main(args): if not args: return filename, glyphs = args[0], args[1:] from fontTools.ttLib import TTFont font = TTFont(filename) if not glyphs: glyphs = font.getGlyphOrder() _test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
def get_woff(font_text): base64_behind = re.split('\;base64\,', font_text)[1] font_content = re.split('\)', base64_behind)[0].strip() if font_content: bs_font = base64.b64decode(font_content) with open("new.woff", 'wb') as f: f.write(bs_font) font_ttf = TTFont("new.woff") data = font_ttf.getGlyphSet()._glyphs.glyphs return data
def __init_font_map(self): """ 初始化猫眼的字符集模版,只在倒入模块时有构造方法调用一次 """ font_file = self.__save_font_file(self.url) font = TTFont(font_file) glyph_set = font.getGlyphSet() glyph_dict = glyph_set._glyphs.glyphs for k, v in self.manual_dict.items(): self.font_dict[glyph_dict[k].data] = v
def get_rel(): maoyan_fonts = TTFont('maoyan.woff') font_dict = {} base_num = { "uniF1D0": "4", "uniE13A": "3", "uniE64F": "0", "uniECF2": "1", "uniF382": "2", "uniE1FD": "8", "uniF5E4": "6", "uniF1B0": "9", "uniE71E": "7", "uniE979": "5"} _data = maoyan_fonts.getGlyphSet()._glyphs.glyphs for k, v in base_num.items(): font_dict[_data[k].data] = v return font_dict
def text_path(t, pos, font_path): # Like text, but draws paths of the glyphs x, y = pos f = TTFont(font_path) gs = f.getGlyphSet() save() translate(x, y) for l in t: glyph = gs[l] drawGlyph(glyph) translate(glyph.width) restore()
def _colr_v0_glyph_to_svg(ttfont: ttLib.TTFont, view_box: Rect, glyph_name: str) -> etree.Element: svg_root = _svg_root(view_box) glyph_set = ttfont.getGlyphSet() for glyph_layer in ttfont["COLR"].ColorLayers[glyph_name]: svg_path = etree.SubElement(svg_root, "path") _solid_paint(svg_path, ttfont, glyph_layer.colorID) _draw_svg_path(svg_path, view_box, ttfont, glyph_layer.name, glyph_set) return svg_root
def get_woff(self,html): selector = etree.HTML(html) font_text = selector.xpath('//style[@id="js-nuwa"]/text()')[0] base64_behind = re.split('\;base64\,', font_text)[1] font_content = re.split('\)', base64_behind)[0].strip() if font_content: bs_font = base64.b64decode(font_content) with open("new.woff",'wb') as f: f.write(bs_font) font_ttf = TTFont("new.woff") data = font_ttf.getGlyphSet()._glyphs.glyphs return data
def test_glyphset( self, location, expected ): font = TTFont(self.getpath("I.ttf")) glyphset = font.getGlyphSet(location=location) pen = RecordingPen() glyph = glyphset['I'] glyph.draw(pen) actual = pen.value assert actual == expected, (location, actual, expected)
def convert_to_num(self, series, url): """ 获取unicode的对应的数字 :param series: int :param url: 字符集文件的地址 :return: int,series对应数字 """ font_file = self.__save_font_file(url) font = TTFont(font_file) cmap = font.getBestCmap() num = cmap.get(series) glyph_set = font.getGlyphSet() return self.font_dict[glyph_set._glyphs.glyphs[num].data]
def run(self): font = TTFont(self.in_font) gs = font.getGlyphSet() for gname in font.getGlyphOrder(): g = gs[gname]._glyph g.decompile() print("[{}]".format(gname)) operands = [] for b in g.program: if isinstance(b, int): operands.append(b) else: print(" [{}] << {} >>".format(", ".join(map(lambda v: str(v), operands)), b)) operands = [] print(" -----")
def getTextWidth(text, pointSize): font = TTFont('C:\simsun.ttf') cmap = font['cmap'] t = cmap.getcmap(3, 1).cmap s = font.getGlyphSet() units_per_em = font['head'].unitsPerEm total = 0 for c in text: if ord(c) in t and t[ord(c)] in s: total += s[t[ord(c)]].width else: total += s['.notdef'].width total = total * float(pointSize) / units_per_em return total
class Font(LayoutEngine): def __init__(self, path, glyphClass=None): super(Font, self).__init__() self.path = path self._glyphs = {} if isinstance(path, TTFont): self.source = path else: self.source = TTFont(path) self.loadGlyphSet() self.loadCMAP() self.loadFeatures() self.loadInfo() if glyphClass is None: glyphClass = Glyph self.glyphClass = glyphClass def __del__(self): del self._glyphs self.source.close() del self.source # -------------- # initialization # -------------- def loadCMAP(self): cmap = extractCMAP(self.source) self.setCMAP(cmap) def loadGlyphSet(self): self.glyphSet = self.source.getGlyphSet() # the glyph order will be needed later # to assign the proper glyph index to # glyph objects. order = self.source.getGlyphOrder() self._glyphOrder = {} for index, glyphName in enumerate(order): self._glyphOrder[glyphName] = index def loadInfo(self): self.info = info = Info() head = self.source["head"] hhea = self.source["hhea"] os2 = self.source["OS/2"] info.unitsPerEm = head.unitsPerEm info.ascender = hhea.ascent info.descender = hhea.descent info.xHeight = os2.sxHeight info.capHeight = os2.sCapHeight # names nameIDs = {} for nameRecord in self.source["name"].names: nameID = nameRecord.nameID platformID = nameRecord.platformID platEncID = nameRecord.platEncID langID = nameRecord.langID nameIDs[nameID, platformID, platEncID, langID] = nameRecord.toUnicode() # to retrieve the family and style names, first start # with the preferred name entries and progress to less # specific entries until something is found. familyPriority = [(16, 1, 0, 0), (16, 1, None, None), (16, None, None, None), (1, 1, 0, 0), (1, 1, None, None), (1, None, None, None)] familyName = self._skimNameIDs(nameIDs, familyPriority) stylePriority = [(17, 1, 0, 0), (17, 1, None, None), (17, None, None, None), (2, 1, 0, 0), (2, 1, None, None), (2, None, None, None)] styleName = self._skimNameIDs(nameIDs, stylePriority) if familyName is None or styleName is None: raise CompositorError("Could not extract name data from name table.") self.info.familyName = familyName self.info.styleName = styleName # stylistic set names self.stylisticSetNames = {} if self.gsub: for featureRecord in self.gsub.FeatureList.FeatureRecord: params = featureRecord.Feature.FeatureParams if hasattr(params, "UINameID"): ssNameID = params.UINameID namePriority = [(ssNameID, 1, 0, 0), (ssNameID, 1, None, None), (ssNameID, 3, 1, 1033), (ssNameID, 3, None, None)] ssName = self._skimNameIDs(nameIDs, namePriority) if ssName: self.stylisticSetNames[featureRecord.FeatureTag] = ssName def _skimNameIDs(self, nameIDs, priority): for (nameID, platformID, platEncID, langID) in priority: for (nID, pID, pEID, lID), text in nameIDs.items(): if nID != nameID: continue if pID != platformID and platformID is not None: continue if pEID != platEncID and platEncID is not None: continue if lID != langID and langID is not None: continue return text def loadFeatures(self): gdef = None if "GDEF" in self.source: gdef = self.source["GDEF"] gsub = None if "GSUB" in self.source: gsub = self.source["GSUB"] gpos = None if "GPOS" in self.source: gpos = self.source["GPOS"] self.setFeatureTables(gdef, gsub, gpos) # ------------- # dict behavior # ------------- def keys(self): return self.glyphSet.keys() def __contains__(self, name): return name in self.glyphSet def __getitem__(self, name): if name not in self._glyphs: if name not in self.glyphSet: name = self.fallbackGlyph glyph = self.glyphSet[name] index = self._glyphOrder[name] glyph = self.glyphClass(name, index, glyph, self) self._glyphs[name] = glyph return self._glyphs[name] # ----------------- # string processing # ----------------- def stringToGlyphNames(self, string): glyphNames = [] for c in string: c = unicode(c) v = ord(c) if v in self.cmap: glyphNames.append(self.cmap[v]) elif self.fallbackGlyph is not None: glyphNames.append(self.fallbackGlyph) return glyphNames def stringToGlyphRecords(self, string): return [GlyphRecord(glyphName) for glyphName in self.stringToGlyphNames(string)] def didProcessingGSUB(self, glyphRecords): for glyphRecord in glyphRecords: glyphRecord.advanceWidth += self[glyphRecord.glyphName].width # ------------- # Miscellaneous # ------------- def getGlyphOrder(self): return self.source.getGlyphOrder()
class Font(object): def __init__(self, path, glyphClass=None): self.path = path self.fallbackGlyph = ".notdef" self._glyphs = {} if isinstance(path, TTFont): self.source = path else: self.source = TTFont(path) self.loadGlyphSet() self.loadInfo() self.loadCMAP() self.loadFeatures() if glyphClass is None: glyphClass = Glyph self.glyphClass = glyphClass def __del__(self): del self._glyphs self.source.close() del self.source # -------------- # initialization # -------------- def loadCMAP(self): self.cmap = extractCMAP(self.source) self.reversedCMAP = reverseCMAP(self.cmap) def loadGlyphSet(self): self.glyphSet = self.source.getGlyphSet() # the glyph order will be needed later # to assign the proper glyph index to # glyph objects. order = self.source.getGlyphOrder() self._glyphOrder = {} for index, glyphName in enumerate(order): self._glyphOrder[glyphName] = index def loadInfo(self): self.info = info = Info() head = self.source["head"] hhea = self.source["hhea"] os2 = self.source["OS/2"] info.unitsPerEm = head.unitsPerEm info.ascender = hhea.ascent info.descender = hhea.descent info.xHeight = os2.sxHeight info.capHeight = os2.sCapHeight # names nameIDs = {} for nameRecord in self.source["name"].names: nameID = nameRecord.nameID platformID = nameRecord.platformID platEncID = nameRecord.platEncID langID = nameRecord.langID text = nameRecord.string nameIDs[nameID, platformID, platEncID, langID] = text # to retrive the family and style names, first start # with the preferred name entries and progress to less # specific entries until something is found. familyPriority = [(16, 1, 0, 0), (16, 1, None, None), (16, None, None, None), (1, 1, 0, 0), (1, 1, None, None), (1, None, None, None)] familyName = self._skimNameIDs(nameIDs, familyPriority) stylePriority = [(17, 1, 0, 0), (17, 1, None, None), (17, None, None, None), (2, 1, 0, 0), (2, 1, None, None), (2, None, None, None)] styleName = self._skimNameIDs(nameIDs, stylePriority) if familyName is None or styleName is None: raise CompositorError("Could not extract name data from name table.") self.info.familyName = familyName self.info.styleName = styleName def _skimNameIDs(self, nameIDs, priority): for (nameID, platformID, platEncID, langID) in priority: for (nID, pID, pEID, lID), text in nameIDs.items(): if nID != nameID: continue if pID != platformID and platformID is not None: continue if pEID != platEncID and platEncID is not None: continue if lID != langID and langID is not None: continue # make sure there are no endian issues # XXX right way to do this? text = "".join([i for i in text if i != "\x00"]) return text def loadFeatures(self): self.gsub = None self.gpos = None self.gdef = None if self.source.has_key("GDEF"): self.gdef = GDEF().loadFromFontTools(self.source["GDEF"]) if self.source.has_key("GSUB"): self.gsub = GSUB().loadFromFontTools(self.source["GSUB"], self.reversedCMAP, self.gdef) if self.source.has_key("GPOS"): self.gpos = GPOS().loadFromFontTools(self.source["GPOS"], self.reversedCMAP, self.gdef) # ------------- # dict behavior # ------------- def keys(self): return self.glyphSet.keys() def __contains__(self, name): return self.glyphSet.has_key(name) def __getitem__(self, name): if name not in self._glyphs: glyph = self.glyphSet[name] index = self._glyphOrder[name] glyph = self.glyphClass(name, index, glyph, self) self._glyphs[name] = glyph return self._glyphs[name] # ----------------- # string processing # ----------------- def stringToGlyphNames(self, string): glyphNames = [] for c in string: c = unicode(c) v = ord(c) if v in self.cmap: glyphNames.append(self.cmap[v]) elif self.fallbackGlyph is not None: glyphNames.append(self.fallbackGlyph) return glyphNames def stringToGlyphRecords(self, string): return [GlyphRecord(glyphName) for glyphName in self.stringToGlyphNames(string)] def glyphListToGlyphRecords(self, glyphList): glyphRecords = [] for glyphName in glyphList: if glyphName not in self: if self.fallbackGlyph is None: continue glyphName = self.fallbackGlyph record = GlyphRecord(glyphName) glyphRecords.append(record) return glyphRecords def process(self, stringOrGlyphList, script="latn", langSys=None, rightToLeft=False, case="unchanged", logger=None): if isinstance(stringOrGlyphList, basestring): stringOrGlyphList = self.stringToGlyphNames(stringOrGlyphList) if case != "unchanged": l = langSys if l is not None: l = l.strip() stringOrGlyphList = convertCase(case, stringOrGlyphList, self.cmap, self.reversedCMAP, l, self.fallbackGlyph) glyphRecords = self.glyphListToGlyphRecords(stringOrGlyphList) if rightToLeft: glyphRecords.reverse() if logger: logger.logStart() glyphNames = [r.glyphName for r in glyphRecords] logger.logMainSettings(glyphNames, script, langSys) if self.gsub is not None: if logger: logger.logTableStart(self.gsub) glyphRecords = self.gsub.process(glyphRecords, script=script, langSys=langSys, logger=logger) if logger: logger.logResults(glyphRecords) logger.logTableEnd() advancedRecords = [] for glyphRecord in glyphRecords: glyphRecord.advanceWidth = self[glyphRecord.glyphName].width advancedRecords.append(glyphRecord) glyphRecords = advancedRecords if self.gpos is not None: if logger: logger.logTableStart(self.gpos) glyphRecords = self.gpos.process(glyphRecords, script=script, langSys=langSys, logger=logger) if logger: logger.logResults(glyphRecords) logger.logTableEnd() if logger: logger.logEnd() return glyphRecords # ------------------ # feature management # ------------------ def getScriptList(self): gsub = [] gpos = [] if self.gsub is not None: gsub = self.gsub.getScriptList() if self.gpos is not None: gpos = self.gpos.getScriptList() return sorted(set(gsub + gpos)) def getLanguageList(self): gsub = [] gpos = [] if self.gsub is not None: gsub = self.gsub.getLanguageList() if self.gpos is not None: gpos = self.gpos.getLanguageList() return sorted(set(gsub + gpos)) def getFeatureList(self): gsub = [] gpos = [] if self.gsub is not None: gsub = self.gsub.getFeatureList() if self.gpos is not None: gpos = self.gpos.getFeatureList() return sorted(set(gsub + gpos)) def getFeatureState(self, featureTag): gsubState = None gposState = None if self.gsub is not None: if featureTag in self.gsub: gsubState = self.gsub.getFeatureState(featureTag) if self.gpos is not None: if featureTag in self.gpos: gposState = self.gpos.getFeatureState(featureTag) if gsubState is not None and gposState is not None: if gsubState != gposState: raise CompositorError("Inconsistently applied feature: %s" % featureTag) if gsubState is not None: return gsubState if gposState is not None: return gposState raise CompositorError("Feature %s is is not contained in GSUB or GPOS" % featureTag) def setFeatureState(self, featureTag, state): if self.gsub is not None: if featureTag in self.gsub: self.gsub.setFeatureState(featureTag, state) if self.gpos is not None: if featureTag in self.gpos: self.gpos.setFeatureState(featureTag, state) # ------------- # Miscellaneous # ------------- def getGlyphOrder(self): return self.source.getGlyphOrder()
class ShapeDiffFinder: """Provides methods to report diffs in glyph shapes between OT Fonts.""" def __init__( self, file_a, file_b, stats, ratio_diffs=False, diff_threshold=0): self.path_a = file_a self.font_a = TTFont(self.path_a) self.glyph_set_a = self.font_a.getGlyphSet() self.gdef_a = {} if 'GDEF' in self.font_a: self.gdef_a = self.font_a['GDEF'].table.GlyphClassDef.classDefs self.path_b = file_b self.font_b = TTFont(self.path_b) self.glyph_set_b = self.font_b.getGlyphSet() self.gdef_b = {} if 'GDEF' in self.font_b: self.gdef_b = self.font_b['GDEF'].table.GlyphClassDef.classDefs for stat_type in ( 'compared', 'untested', 'unmatched', 'unicode_mismatch', 'gdef_mark_mismatch', 'zero_width_mismatch', 'input_mismatch'): if stat_type not in stats: stats[stat_type] = [] self.stats = stats self.ratio_diffs = ratio_diffs self.diff_threshold = diff_threshold self.basepath = os.path.basename(file_a) def find_area_diffs(self): """Report differences in glyph areas.""" self.build_names() pen_a = GlyphAreaPen(self.glyph_set_a) pen_b = GlyphAreaPen(self.glyph_set_b) mismatched = {} for name in self.names: self.glyph_set_a[name].draw(pen_a) area_a = pen_a.pop() self.glyph_set_b[name].draw(pen_b) area_b = pen_b.pop() if area_a != area_b: mismatched[name] = (area_a, area_b) stats = self.stats['compared'] calc = self._calc_ratio if self.ratio_diffs else self._calc_diff for name, areas in mismatched.items(): stats.append((calc(areas), name, self.basepath, areas[0], areas[1])) def find_rendered_diffs(self, font_size=128, render_path=None): """Find diffs of glyphs as rendered by harfbuzz.""" hb_input_generator_a = hb_input.HbInputGenerator(self.font_a) hb_input_generator_b = hb_input.HbInputGenerator(self.font_b) if render_path: font_name, _ = os.path.splitext(self.basepath) render_path = os.path.join(render_path, font_name) if not os.path.exists(render_path): os.makedirs(render_path) self.build_names() diffs = [] for name in self.names: class_a = self.gdef_a.get(name, GDEF_UNDEF) class_b = self.gdef_b.get(name, GDEF_UNDEF) if GDEF_MARK in (class_a, class_b) and class_a != class_b: self.stats['gdef_mark_mismatch'].append(( self.basepath, name, GDEF_LABELS[class_a], GDEF_LABELS[class_b])) continue width_a = self.glyph_set_a[name].width width_b = self.glyph_set_b[name].width zwidth_a = width_a == 0 zwidth_b = width_b == 0 if zwidth_a != zwidth_b: self.stats['zero_width_mismatch'].append(( self.basepath, name, width_a, width_b)) continue hb_args_a = hb_input_generator_a.input_from_name(name, pad=zwidth_a) hb_args_b = hb_input_generator_b.input_from_name(name, pad=zwidth_b) if hb_args_a != hb_args_b: self.stats['input_mismatch'].append(( self.basepath, name, hb_args_a, hb_args_b)) continue # ignore unreachable characters if not hb_args_a: self.stats['untested'].append((self.basepath, name)) continue features, text = hb_args_a # ignore null character if unichr(0) in text: continue img_file_a = StringIO.StringIO(subprocess.check_output([ 'hb-view', '--font-size=%d' % font_size, '--features=%s' % ','.join(features), self.path_a, text])) img_file_b = StringIO.StringIO(subprocess.check_output([ 'hb-view', '--font-size=%d' % font_size, '--features=%s' % ','.join(features), self.path_b, text])) img_a = Image.open(img_file_a) img_b = Image.open(img_file_b) width_a, height_a = img_a.size width_b, height_b = img_b.size data_a = img_a.getdata() data_b = img_b.getdata() img_file_a.close() img_file_b.close() width, height = max(width_a, width_b), max(height_a, height_b) offset_ax = (width - width_a) // 2 offset_ay = (height - height_a) // 2 offset_bx = (width - width_b) // 2 offset_by = (height - height_b) // 2 diff = 0 for y in range(height): for x in range(width): ax, ay = x - offset_ax, y - offset_ay bx, by = x - offset_bx, y - offset_by if (ax < 0 or bx < 0 or ax >= width_a or bx >= width_b or ay < 0 or by < 0 or ay >= height_a or by >= height_b): diff += 1 else: diff += abs(data_a[ax + ay * width_a] - data_b[bx + by * width_b]) / 255 if self.ratio_diffs: diff /= (width * height) if render_path and diff > self.diff_threshold: img_cmp = Image.new('RGB', (width, height)) data_cmp = list(img_cmp.getdata()) self._project(data_a, width_a, height_a, data_cmp, width, height, 1) self._project(data_b, width_b, height_b, data_cmp, width, height, 0) for y in range(height): for x in range(width): i = x + y * width r, g, b = data_cmp[i] assert b == 0 data_cmp[i] = r, g, min(r, g) img_cmp.putdata(data_cmp) img_cmp.save(self._rendered_png(render_path, name)) diffs.append((name, diff)) mismatched = {} for name, diff in diffs: if diff > self.diff_threshold: mismatched[name] = diff stats = self.stats['compared'] for name, diff in mismatched.items(): stats.append((diff, name, self.basepath)) def _project( self, src_data, src_width, src_height, dst_data, width, height, channel): """Project a single-channel image onto a channel of an RGB image.""" offset_x = (width - src_width) // 2 offset_y = (height - src_height) // 2 for y in range(src_height): for x in range(src_width): src_i = x + y * src_width dst_i = x + offset_x + (y + offset_y) * width pixel = list(dst_data[dst_i]) pixel[channel] = src_data[src_i] dst_data[dst_i] = tuple(pixel) def find_shape_diffs(self): """Report differences in glyph shapes, using BooleanOperations.""" self.build_names() area_pen = GlyphAreaPen(None) pen = PointToSegmentPen(area_pen) mismatched = {} for name in self.names: glyph_a = Glyph() glyph_b = Glyph() self.glyph_set_a[name].draw( Qu2CuPen(glyph_a.getPen(), self.glyph_set_a)) self.glyph_set_b[name].draw( Qu2CuPen(glyph_b.getPen(), self.glyph_set_b)) booleanOperations.xor(list(glyph_a), list(glyph_b), pen) area = abs(area_pen.pop()) if area: mismatched[name] = (area) stats = self.stats['compared'] for name, area in mismatched.items(): stats.append((area, name, self.basepath)) def find_area_shape_diff_products(self): """Report product of differences in glyph areas and glyph shapes.""" self.find_area_diffs() old_compared = self.stats['compared'] self.stats['compared'] = [] self.find_shape_diffs() new_compared = {n: d for d, n, _ in self.stats['compared']} for i, (diff, name, font, area_a, area_b) in enumerate(old_compared): if font != self.basepath: continue new_diff = diff * new_compared.get(name, 0) old_compared[i] = new_diff, name, font, area_a, area_b self.stats['compared'] = old_compared def build_names(self): """Build a list of glyph names shared between the fonts.""" if hasattr(self, 'names'): return stats = self.stats['unmatched'] names_a = set(self.font_a.getGlyphOrder()) names_b = set(self.font_b.getGlyphOrder()) if names_a != names_b: stats.append((self.basepath, names_a - names_b, names_b - names_a)) self.names = names_a & names_b stats = self.stats['unicode_mismatch'] reverse_cmap_a = hb_input.build_reverse_cmap(self.font_a) reverse_cmap_b = hb_input.build_reverse_cmap(self.font_b) mismatched = {} for name in self.names: unival_a = reverse_cmap_a.get(name) unival_b = reverse_cmap_b.get(name) if unival_a != unival_b: mismatched[name] = (unival_a, unival_b) if mismatched: stats.append((self.basepath, mismatched.items())) self.names -= set(mismatched.keys()) @staticmethod def dump(stats, whitelist, out_lines, include_vals, multiple_fonts): """Return the results of run diffs. Args: stats: List of tuples with diff data which is sorted and printed. whitelist: Names of glyphs to exclude from report. out_lines: Number of diff lines to print. include_vals: Include the values that have been diffed in report. multiple_fonts: Designates whether stats have been accumulated from multiple fonts, if so then font names will be printed as well. """ report = [] compared = sorted( s for s in stats['compared'] if s[1] not in whitelist) compared.reverse() fmt = '%s %s' if include_vals: fmt += ' (%s vs %s)' if multiple_fonts: fmt = '%s ' + fmt report.append('%d differences in glyph shape' % len(compared)) for line in compared[:out_lines]: # print <font> <glyph> <vals>; stats are sorted in reverse priority line = tuple(reversed(line[:3])) + tuple(line[3:]) # ignore font name if just one pair of fonts was compared if not multiple_fonts: line = line[1:] report.append(fmt % line) report.append('') for font, set_a, set_b in stats['unmatched']: report.append("Glyph coverage doesn't match in %s" % font) report.append(' in A but not B: %s' % sorted(set_a)) report.append(' in B but not A: %s' % sorted(set_b)) report.append('') for font, mismatches in stats['unicode_mismatch']: report.append("Glyph unicode values don't match in %s" % font) for name, univals in sorted(mismatches): univals = [(('0x%04X' % v) if v else str(v)) for v in univals] report.append(' %s: %s in A, %s in B' % (name, univals[0], univals[1])) report.append('') ShapeDiffFinder._add_simple_report( report, stats['gdef_mark_mismatch'], '%s: Mark class mismatch for %s (%s vs %s)') ShapeDiffFinder._add_simple_report( report, stats['zero_width_mismatch'], '%s: Zero-width mismatch for %s (%d vs %d)') ShapeDiffFinder._add_simple_report( report, stats['input_mismatch'], '%s: Harfbuzz input mismatch for %s (%s vs %s)') ShapeDiffFinder._add_simple_report( report, stats['untested'], '%s: %s not tested (unreachable?)') return '\n'.join(report) @staticmethod def _add_simple_report(report, stats, fmt): for stat in sorted(stats): report.append(fmt % stat) if stats: report.append('') def _calc_diff(self, vals): """Calculate an area difference.""" a, b = vals return abs(a - b) def _calc_ratio(self, vals): """Calculate an area ratio.""" a, b = vals if not (a or b): return 0 if abs(a) > abs(b): a, b = b, a return 1 - a / b def _rendered_png(self, render_path, glyph_name): glyph_filename = re.sub(r'([A-Z_])', r'\1_', glyph_name) + '.png' return os.path.join(render_path, glyph_filename)
def makeLookup1(): # make a variation of the shell TTX data f = open(shellSourcePath) ttxData = f.read() f.close() ttxData = ttxData.replace("__familyName__", "gsubtest-lookup1") tempShellSourcePath = shellSourcePath + ".temp" f = open(tempShellSourcePath, "wb") f.write(ttxData) f.close() # compile the shell shell = TTFont(sfntVersion="OTTO") shell.importXML(tempShellSourcePath) shell.save(shellTempPath) os.remove(tempShellSourcePath) # load the shell shell = TTFont(shellTempPath) # grab the PASS and FAIL data hmtx = shell["hmtx"] glyphSet = shell.getGlyphSet() failGlyph = glyphSet["F"] failGlyph.decompile() failGlyphProgram = list(failGlyph.program) failGlyphMetrics = hmtx["F"] passGlyph = glyphSet["P"] passGlyph.decompile() passGlyphProgram = list(passGlyph.program) passGlyphMetrics = hmtx["P"] # grab some tables hmtx = shell["hmtx"] cmap = shell["cmap"] # start the glyph order existingGlyphs = [".notdef", "space", "F", "P"] glyphOrder = list(existingGlyphs) # start the CFF cff = shell["CFF "].cff globalSubrs = cff.GlobalSubrs topDict = cff.topDictIndex[0] topDict.charset = existingGlyphs private = topDict.Private charStrings = topDict.CharStrings charStringsIndex = charStrings.charStringsIndex features = sorted(mapping) # build the outline, hmtx and cmap data cp = baseCodepoint for index, tag in enumerate(features): # tag.pass glyphName = "{0!s}.pass".format(tag) glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=passGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings ) hmtx[glyphName] = passGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError, "Unsupported cmap table format: {0:d}".format(table.format) cp += 1 # tag.fail glyphName = "{0!s}.fail".format(tag) glyphOrder.append(glyphName) addGlyphToCFF( glyphName=glyphName, program=failGlyphProgram, private=private, globalSubrs=globalSubrs, charStringsIndex=charStringsIndex, topDict=topDict, charStrings=charStrings ) hmtx[glyphName] = failGlyphMetrics for table in cmap.tables: if table.format == 4: table.cmap[cp] = glyphName else: raise NotImplementedError, "Unsupported cmap table format: {0:d}".format(table.format) # bump this up so that the sequence is the same as the lookup 3 font cp += 3 # set the glyph order shell.setGlyphOrder(glyphOrder) # start the GSUB shell["GSUB"] = newTable("GSUB") gsub = shell["GSUB"].table = GSUB() gsub.Version = 1.0 # make a list of all the features we will make featureCount = len(features) # set up the script list scriptList = gsub.ScriptList = ScriptList() scriptList.ScriptCount = 1 scriptList.ScriptRecord = [] scriptRecord = ScriptRecord() scriptList.ScriptRecord.append(scriptRecord) scriptRecord.ScriptTag = "DFLT" script = scriptRecord.Script = Script() defaultLangSys = script.DefaultLangSys = DefaultLangSys() defaultLangSys.FeatureCount = featureCount defaultLangSys.FeatureIndex = range(defaultLangSys.FeatureCount) defaultLangSys.ReqFeatureIndex = 65535 defaultLangSys.LookupOrder = None script.LangSysCount = 0 script.LangSysRecord = [] # set up the feature list featureList = gsub.FeatureList = FeatureList() featureList.FeatureCount = featureCount featureList.FeatureRecord = [] for index, tag in enumerate(features): # feature record featureRecord = FeatureRecord() featureRecord.FeatureTag = tag feature = featureRecord.Feature = Feature() featureList.FeatureRecord.append(featureRecord) # feature feature.FeatureParams = None feature.LookupCount = 1 feature.LookupListIndex = [index] # write the lookups lookupList = gsub.LookupList = LookupList() lookupList.LookupCount = featureCount lookupList.Lookup = [] for tag in features: # lookup lookup = Lookup() lookup.LookupType = 1 lookup.LookupFlag = 0 lookup.SubTableCount = 1 lookup.SubTable = [] lookupList.Lookup.append(lookup) # subtable subtable = SingleSubst() subtable.Format = 2 subtable.LookupType = 1 subtable.mapping = { "{0!s}.pass".format(tag) : "{0!s}.fail".format(tag), "{0!s}.fail".format(tag) : "{0!s}.pass".format(tag), } lookup.SubTable.append(subtable) path = outputPath % 1 + ".otf" if os.path.exists(path): os.remove(path) shell.save(path) # get rid of the shell if os.path.exists(shellTempPath): os.remove(shellTempPath)
print(" example: reportLabPen.py Arial.TTF R test.png") print(" (The file format will be PNG, regardless of the image file name supplied)") sys.exit(0) from fontTools.ttLib import TTFont from reportlab.lib import colors path = sys.argv[1] glyphName = sys.argv[2] if (len(sys.argv) > 3): imageFile = sys.argv[3] else: imageFile = "%s.png" % glyphName font = TTFont(path) # it would work just as well with fontTools.t1Lib.T1Font gs = font.getGlyphSet() pen = ReportLabPen(gs, Path(fillColor=colors.red, strokeWidth=5)) g = gs[glyphName] g.draw(pen) w, h = g.width, 1000 from reportlab.graphics import renderPM from reportlab.graphics.shapes import Group, Drawing, scale # Everything is wrapped in a group to allow transformations. g = Group(pen.path) g.translate(0, 200) g.scale(0.3, 0.3) d = Drawing(w, h) d.add(g)
class Text(Grob, TransformMixin, ColorMixin): stateAttributes = ('_transform', '_transformmode', '_fillcolor', '_fontfile', '_fontsize', '_align', '_lineheight') FONT_SPECIFIER_NAME_ID = 4 FONT_SPECIFIER_FAMILY_ID = 1 FAMILY_NAMES = { 0: ("ANY",{}), 1: ("SERIF-OLD", { 0: "ANY", 1: "ROUNDED-LEGIBILITY", 2: "GARALDE", 3: "VENETIAN", 4: "VENETIAN-MODIFIED", 5: "DUTCH-MODERN", 6: "DUTCH-TRADITIONAL", 7: "CONTEMPORARY", 8: "CALLIGRAPHIC", 15: "MISCELLANEOUS", }), 2: ("SERIF-TRANSITIONAL", { 0: "ANY", 1: "DIRECT-LINE", 2: "SCRIPT", 15: "MISCELLANEOUS", }), 3: ("SERIF", { 0: "ANY", 1: "ITALIAN", 2: "SCRIPT", 15: "MISCELLANEOUS", }), 4: ("SERIF-CLARENDON",{ 0: "ANY", 1: "CLARENDON", 2: "MODERN", 3: "TRADITIONAL", 4: "NEWSPAPER", 5: "STUB-SERIF", 6: "MONOTYPE", 7: "TYPEWRITER", 15: "MISCELLANEOUS", }), 5: ("SERIF-SLAB",{ 0: 'ANY', 1: 'MONOTONE', 2: 'HUMANIST', 3: 'GEOMETRIC', 4: 'SWISS', 5: 'TYPEWRITER', 15: 'MISCELLANEOUS', }), 7: ("SERIF-FREEFORM",{ 0: 'ANY', 1: 'MODERN', 15: 'MISCELLANEOUS', }), 8: ("SANS",{ 0: 'ANY', 1: 'GOTHIC-NEO-GROTESQUE-IBM', 2: 'HUMANIST', 3: 'ROUND-GEOMETRIC-LOW-X', 4: 'ROUND-GEOMETRIC-HIGH-X', 5: 'GOTHIC-NEO-GROTESQUE', 6: 'GOTHIC-NEO-GROTESQUE-MODIFIED', 9: 'GOTHIC-TYPEWRITER', 10: 'MATRIX', 15: 'MISCELLANEOUS', }), 9: ("ORNAMENTAL",{ 0: 'ANY', 1: 'ENGRAVER', 2: 'BLACK-LETTER', 3: 'DECORATIVE', 4: 'THREE-DIMENSIONAL', 15: 'MISCELLANEOUS', }), 10:("SCRIPT",{ 0: 'ANY', 1: 'UNCIAL', 2: 'BRUSH-JOINED', 3: 'FORMAL-JOINED', 4: 'MONOTONE-JOINED', 5: 'CALLIGRAPHIC', 6: 'BRUSH-UNJOINED', 7: 'FORMAL-UNJOINED', 8: 'MONOTONE-UNJOINED', 15: 'MISCELLANEOUS', }), 12:("SYMBOL",{ 0: 'ANY', 3: 'MIXED-SERIF', 6: 'OLDSTYLE-SERIF', 7: 'NEO-GROTESQUE-SANS', 15: 'MISCELLANEOUS', }), } WEIGHT_NAMES = { 'thin':100, 'extralight':200, 'ultralight':200, 'light':300, 'normal':400, 'regular':400, 'plain':400, 'medium':500, 'semibold':600, 'demibold':600, 'bold':700, 'extrabold':800, 'ultrabold':800, 'black':900, 'heavy':900, } WEIGHT_NUMBERS = {} def __init__(self, context, text, x=0, y=0, path=None, **kwargs): self._ctx = context if context: copy_attrs(self._ctx, self, self.stateAttributes) self.font_handle = TTFont(self._fontfile) self.x = x self.y = y self.text = unicode(text) self.path = path self.resolution = 72 self.width = 0 self.height = 0 self.text_xmin = 0 self.text_ymin = 0 self.text_xmax = 0 self.text_ymax = 0 self.posX = 0 self.posY = 0 self.kerning = 0 self.scale = float(self._fontsize)/float(self.char_height) self.encoding = None self.current_pt = (0, 0) self.t_scale = Scale(self.scale, self.scale) self.offset = 0 self.metrics = {} ## cx, cy should be the center point fx, fy = (-self.scale, self.scale) C = math.cos(math.pi) S = math.sin(math.pi) self.aux_transform = Transform(fx*C, fx*S, -S*fy, C*fy, self.x, self.y) for key, value in self.WEIGHT_NAMES.items(): self.WEIGHT_NUMBERS.setdefault(value,[]).append(key) if self.encoding is None: self.table = self.font_handle['cmap'].tables self.encoding = (self.table[0].platformID, self.table[0].platEncID) self.table = self.font_handle['cmap'].getcmap(*self.encoding) for glyph in self.text: glyfName = self.table.cmap.get(ord(glyph)) self.width += self.glyph_width(glyfName) self.height = self.char_height * self.scale print "metrics: w:%f h:%f" % (self.width, self.height) #print "scale: %f" % (self.scale) ## self.vorg = self.font_handle['VORG'] #print self.table.__dict__ def _get_abs_bounds(self): return (self.x, self.y, self.width + self.x, self.height + self.y) abs_bounds = property(_get_abs_bounds) def _get_bounds(self): return (self.width, self.height) bounds = property(_get_bounds) def _get_center(self): (x1, y1, x2, y2) = self.abs_bounds x = (x1 + x2) / 2 y = (y1 + y2) / 2 return (x, y) center = property(_get_center) ### Drawing methods ### def _get_transform(self): trans = self._transform.copy() if (self._transformmode == CENTER): (x, y, w, h) = self.bounds deltax = x+w/2 deltay = y+h/2 t = Transform() t.translate(-deltax, -deltay) trans.prepend(t) t = Transform() t.translate(deltax, deltay) trans.append(t) return trans get_transform = property(_get_transform) def _get_ascent(self): return self.font_handle['OS/2'].sTypoAscender ascent = property(_get_ascent) def _get_descent(self): return self.font_handle['OS/2'].sTypoDescender descent = property(_get_descent) def _get_line_height(self): return self.char_height + self.font_handle['OS/2'].sTypoLineGap line_height = property(_get_line_height) def _get_char_height(self): if self.descent > 0: descent = -self.descent else: descent = self.descent return self.ascent - descent char_height = property(_get_char_height) def _get_short_name(self): """Get the short name from the font's names table""" name = "" family = "" for record in self.font_handle['name'].names: if record.nameID == FONT_SPECIFIER_NAME_ID and not name: if '\000' in record.string: name = unicode(record.string, 'utf-16-be').encode('utf-8') else: name = record.string elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family: if '\000' in record.string: family = unicode(record.string, 'utf-16-be').encode('utf-8') else: family = record.string if name and family: break return name, family short_name = property(_get_short_name) def _get_family(self): """Get the family (and sub-family) for a font""" HIBYTE = 65280 LOBYTE = 255 familyID = (self.font_handle['OS/2'].sFamilyClass&HIBYTE)>>8 subFamilyID = self.font_handle['OS/2'].sFamilyClass&LOBYTE familyName, subFamilies = self.FAMILY_NAMES.get(familyID, ('RESERVED', None)) if subFamilies: subFamily = subFamilies.get(subFamilyID, 'RESERVED') else: subFamily = 'ANY' return (familyName, subFamily) family = property(_get_family) def _get_weight(self): return self.font_handle['OS/2'].usWeightClass weight = property(_get_weight) def _get_italic(self): return (self.font_handle['OS/2'].fsSelection &1 or self.font_handle['head'].macStyle&2) italic = property(_get_italic) def decomposeQuad(self, points): n = len(points) - 1 assert n > 0 quadSegments = [] for i in range(n - 1): x, y = points[i] nx, ny = points[i+1] impliedPt = (0.5 * (x + nx), 0.5 * (y + ny)) quadSegments.append((points[i], impliedPt)) quadSegments.append((points[-2], points[-1])) return quadSegments def _q_curveTo(self, *points): n = len(points) - 1 if points[-1] is None: x, y = points[-2] ## last off-curve point nx, ny = points[0] ## first off-curve point impliedStartPoint = (0.5 * (x + nx), 0.5 * (y + ny)) impliedStartPoint = self.aux_transform.transformPoint(impliedStartPoint) self.currentPoint = impliedStartPoint self.path.moveto(impliedStartPoint) points = points[:-1] + (impliedStartPoint,) if n > 0: for pt1, pt2 in self.decomposeQuad(points): #self.path.moveto() pt0 = self.aux_transform.transformPoint(pt1) pt1 = self.aux_transform.transformPoint(pt2) self.path.curve3to(pt0[0], pt0[1], pt1[0], pt1[1]) self.current_pt = (pt1[0], pt1[1]) else: (X, Y) = self.aux_transform.transformPoint((points[0][0], points[0][1])) self.path.lineto(X, Y) self.current_pt = (pt2[0], pt2[1]) def glyph_width(self, glyph_name): try: return self.font_handle['hmtx'].metrics[glyph_name][0]*self.scale except KeyError: raise ValueError( """Couldn't find glyph for glyphName %r""" % (glyphName)) def has_glyph(self, glyph_name, encoding=None): """ Check to see if font appears to have explicit glyph for char """ cmap = self.font_handle['cmap'] if encoding is None: table = self.font_handle['cmap'].tables encoding = (table.platformID, table.platEncID) table = cmap.getcmap(*encoding) glyfName = table.cmap.get(ord(char)) if glyfName is None: return False else: return True def kerning(self): kern = 0 if idx+1 < len(self.text): L = self.text[idx] R = self.text[idx+1] left = glyph_set._ttFont.getGlyphName(ord(L)) right = glyph_set._ttFont.getGlyphName(ord(R)) try: kern = glyph_set._ttFont['kern'].kernTables[0].__dict__['kernTable'][(L, R)] except: pass #c_width = #if not self.metrics.has_key(letter): # self.metrics[letter] = {} #self.metrics[letter].update(glyph_set._ttFont.tables['OS/2'].__dict__) #self.metrics[letter].update(glyph_set._ttFont.tables['maxp'].__dict__) #self.metrics[letter].update(glyph_set._ttFont.tables['hhea'].__dict__) #self.metrics[letter].update(glyph_set._ttFont.tables['head'].__dict__) def decomposeOutline(self, glyph, letter, glyph_set, halign='center'): hmtx = glyph_set._ttFont['hmtx'] width_, lsb = hmtx[letter] rsb = width_ - lsb - (glyph.xMax - glyph.xMin) extent = lsb + (glyph.xMax - glyph.xMin) coordinates, end_points, flags = glyph.getCoordinates(self.glyph_table) offsetx_ = 0 idx = self.text.index(letter) if idx+1 < len(self.text): _letter = self.text[idx+1] _glyph = self.glyph_table[_letter] next_width = ((hmtx[_letter][0] + hmtx[_letter][1]) * self.scale)*0.5 _offsetx = (glyph.xMin + next_width) if halign=='center': offsetx = (glyph.xMin + width_) elif halign=='right': offsetx = glyph.xMin + width_ else: offsetx = glyph.xMin offsetx = (offsetx + offsetx_) * self.scale idx = self.text.index(letter) if idx+1 < len(self.text): n_width = hmtx[self.text[idx+1]] self.aux_transform *= Transform(dx=offsetx) if not coordinates.any(): return ## in the rare case that it doesnt have an xMin if hasattr(glyph, "xMin"): #offset = (lsb_*self.scale - glyph.xMin*self.scale) offset = 0 else: offset = 0 start = 0 for end in end_points: end = end + 1 contour = coordinates[start:end].tolist() contour_flags = flags[start:end].tolist() start = end if 1 not in contour_flags: contour.append(None) self._q_curveTo(*contour) else: f_on_curve = contour_flags.index(1) + 1 contour = contour[f_on_curve:] + contour[:f_on_curve] contour_flags = contour_flags[f_on_curve:] + contour_flags[:f_on_curve] (X, Y) = self.aux_transform.transformPoint(contour[-1]) self.path.moveto(X, Y) self.current_pt=(X, Y) while contour: n_on_curve = contour_flags.index(1) + 1 if n_on_curve == 1: (X, Y) = self.aux_transform.transformPoint(contour[0]) self.path.lineto(X, Y) self.current_pt=(X, Y) else: self._q_curveTo(*contour[:n_on_curve]) contour = contour[n_on_curve:] contour_flags = contour_flags[n_on_curve:] self.path.closepath() self.aux_transform *= Transform(dx=(rsb*self.scale)) def sentence(self): for letter in self.text: rendered = self.draw(letter) def draw(self, letter): glyph_set = self.font_handle.getGlyphSet() self.glyph_table = glyph_set._ttFont['glyf'] #self.metrics[letter] = self.font_handle['hmtx'].metrics[letter] glyph = self.glyph_table[letter] self.decomposeOutline(glyph, letter, glyph_set)