Example #1
0
def empty_svg_font():
    glyph_order = [".notdef"] + list(ascii_letters)

    pen = TTGlyphPen(glyphSet=None)
    pen.moveTo((0, 0))
    pen.lineTo((0, 500))
    pen.lineTo((500, 500))
    pen.lineTo((500, 0))
    pen.closePath()
    glyph = pen.glyph()
    glyphs = {g: glyph for g in glyph_order}

    fb = FontBuilder(unitsPerEm=1024, isTTF=True)
    fb.setupGlyphOrder(glyph_order)
    fb.setupCharacterMap({ord(c): c for c in ascii_letters})
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
    fb.setupHorizontalHeader()
    fb.setupOS2()
    fb.setupPost()
    fb.setupNameTable({"familyName": "TestSVG", "styleName": "Regular"})

    svg_table = newTable("SVG ")
    svg_table.docList = []
    fb.font["SVG "] = svg_table

    return fb.font
Example #2
0
def build_font(srcs, metadata, filename):
    ascent = 880
    descent = 120
    upem = ascent + descent
    scale = upem / 360.0
    transform = Transform(scale, 0, 0, -scale, 0, ascent)
    glyphs = collect_glyphs(srcs, transform=transform)

    builder = FontBuilder(1000, isTTF=False)
    builder.setupGlyphOrder([glyph.name for glyph in glyphs])
    builder.setupCharacterMap({0: ".notdef"})
    psname = metadata["psName"]
    builder.setupCFF(psname, {"FullName": psname},
                     {glyph.name: glyph.charstring
                      for glyph in glyphs}, {})
    builder.setupHorizontalMetrics(
        {glyph.name: glyph.get_hmetrics()
         for glyph in glyphs})
    builder.setupHorizontalHeader(ascent=ascent, descent=-descent)
    builder.setupNameTable({})
    builder.setupOS2()
    builder.setupPost()
    builder.setupVerticalMetrics(
        {glyph.name: glyph.get_vmetrics(ascent=ascent)
         for glyph in glyphs})
    builder.setupVerticalOrigins({}, ascent)
    builder.setupVerticalHeader(ascent=ascent, descent=-descent)
    builder.save(filename)
Example #3
0
def featureVarsTestFont():
    fb = FontBuilder(unitsPerEm=100)
    fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"])
    fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"})
    fb.setupNameTable({
        "familyName": "TestFeatureVars",
        "styleName": "Regular"
    })
    fb.setupPost()
    fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[])
    fb.addOpenTypeFeatures("""\
        feature dlig {
            sub f f by f_f;
        } dlig;
    """)
    fb.addFeatureVariations([([{
        "wght": (0.20886, 1.0)
    }], {
        "dollar": "dollar.rvrn"
    })],
                            featureTag="rvrn")
    buf = io.BytesIO()
    fb.save(buf)
    buf.seek(0)

    return TTFont(buf)
Example #4
0
def minimalTTF():
    fb = FontBuilder(1024, isTTF=True)
    fb.updateHead(unitsPerEm=1000, created=0, modified=0)
    fb.setupGlyphOrder([
        ".notdef", ".null", "A", "Aacute", "V", "acutecomb", "gravecomb",
        "A.alt"
    ])
    fb.setupCharacterMap({
        65: "A",
        192: "Aacute",
        86: "V",
        769: "acutecomb",
        768: "gravecomb"
    })
    advanceWidths = {
        ".notdef": 600,
        "A": 600,
        "Aacute": 600,
        "V": 600,
        ".null": 600,
        "acutecomb": 0,
        "gravecomb": 0,
        "A.alt": 600
    }
    familyName = "HelloTestFont"
    styleName = "TotallyNormal"
    nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
                       styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
    nameStrings['psName'] = familyName + "-" + styleName
    glyphs = {
        ".notdef": test_glyph(),
        ".null": test_glyph(),
        "A": test_glyph(),
        "Aacute": test_glyph(),
        "V": test_glyph(),
        "acutecomb": test_glyph(),
        "gravecomb": test_glyph(),
        "A.alt": test_glyph()
    }
    fb.setupGlyf(glyphs)
    metrics = {}
    glyphTable = fb.font["glyf"]
    for gn, advanceWidth in advanceWidths.items():
        metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader()
    fb.setupNameTable(nameStrings)
    fb.setupOS2()
    fb.setupPost()
    return fb
Example #5
0
def _make_fontfile_with_OS2(*, version, **kwargs):
	upem = 1000
	glyphOrder = [".notdef", "a"]
	cmap = {0x61: "a"}
	glyphs = {gn: Glyph() for gn in glyphOrder}
	hmtx = {gn: (500, 0) for gn in glyphOrder}
	names = {"familyName": "TestOS2", "styleName": "Regular"}

	fb = FontBuilder(unitsPerEm=upem)
	fb.setupGlyphOrder(glyphOrder)
	fb.setupCharacterMap(cmap)
	fb.setupGlyf(glyphs)
	fb.setupHorizontalMetrics(hmtx)
	fb.setupHorizontalHeader()
	fb.setupNameTable(names)
	fb.setupOS2(version=version, **kwargs)

	return _compile(fb.font)
Example #6
0
def test_subset_feature_variations():
    fb = FontBuilder(unitsPerEm=100)
    fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"])
    fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"})
    fb.setupNameTable({
        "familyName": "TestFeatureVars",
        "styleName": "Regular"
    })
    fb.setupPost()
    fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[])
    fb.addOpenTypeFeatures("""\
        feature dlig {
            sub f f by f_f;
        } dlig;
    """)
    fb.addFeatureVariations([([{
        "wght": (0.20886, 1.0)
    }], {
        "dollar": "dollar.rvrn"
    })],
                            featureTag="rvrn")
    buf = io.BytesIO()
    fb.save(buf)
    buf.seek(0)

    font = TTFont(buf)

    options = subset.Options()
    subsetter = subset.Subsetter(options)
    subsetter.populate(unicodes=[ord("f"), ord("$")])
    subsetter.subset(font)

    featureTags = {
        r.FeatureTag
        for r in font["GSUB"].table.FeatureList.FeatureRecord
    }
    # 'dlig' is discretionary so it is dropped by default
    assert "dlig" not in featureTags
    assert "f_f" not in font.getGlyphOrder()
    # 'rvrn' is required so it is kept by default
    assert "rvrn" in featureTags
    assert "dollar.rvrn" in font.getGlyphOrder()
Example #7
0
def test_unicodeVariationSequences(tmpdir):
    familyName = "UVSTestFont"
    styleName = "Regular"
    nameStrings = dict(familyName=familyName, styleName=styleName)
    nameStrings['psName'] = familyName + "-" + styleName
    glyphOrder = [".notdef", "space", "zero", "zero.slash"]
    cmap = {ord(" "): "space", ord("0"): "zero"}
    uvs = [
        (0x0030, 0xFE00, "zero.slash"),
        (0x0030, 0xFE01, None),  # not an official sequence, just testing
    ]
    metrics = {gn: (600, 0) for gn in glyphOrder}
    pen = TTGlyphPen(None)
    glyph = pen.glyph()  # empty placeholder
    glyphs = {gn: glyph for gn in glyphOrder}

    fb = FontBuilder(1024, isTTF=True)
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(cmap, uvs)
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader(ascent=824, descent=200)
    fb.setupNameTable(nameStrings)
    fb.setupOS2()
    fb.setupPost()

    outPath = os.path.join(str(tmpdir), "test_uvs.ttf")
    fb.save(outPath)
    _verifyOutput(outPath, tables=["cmap"])

    uvs = [
        (0x0030, 0xFE00, "zero.slash"),
        (
            0x0030, 0xFE01, "zero"
        ),  # should result in the exact same subtable data, due to cmap[0x0030] == "zero"
    ]
    fb.setupCharacterMap(cmap, uvs)
    fb.save(outPath)
    _verifyOutput(outPath, tables=["cmap"])
def test_unicodeVariationSequences(tmpdir):
    familyName = "UVSTestFont"
    styleName = "Regular"
    nameStrings = dict(familyName=familyName, styleName=styleName)
    nameStrings['psName'] = familyName + "-" + styleName
    glyphOrder = [".notdef", "space", "zero", "zero.slash"]
    cmap = {ord(" "): "space", ord("0"): "zero"}
    uvs = [
        (0x0030, 0xFE00, "zero.slash"),
        (0x0030, 0xFE01, None),  # not an official sequence, just testing
    ]
    metrics = {gn: (600, 0) for gn in glyphOrder}
    pen = TTGlyphPen(None)
    glyph = pen.glyph()  # empty placeholder
    glyphs = {gn: glyph for gn in glyphOrder}

    fb = FontBuilder(1024, isTTF=True)
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(cmap, uvs)
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader(ascent=824, descent=200)
    fb.setupNameTable(nameStrings)
    fb.setupOS2()
    fb.setupPost()

    outPath = os.path.join(str(tmpdir), "test_uvs.ttf")
    fb.save(outPath)
    _verifyOutput(outPath, tables=["cmap"])

    uvs = [
        (0x0030, 0xFE00, "zero.slash"),
        (0x0030, 0xFE01, "zero"),  # should result in the exact same subtable data, due to cmap[0x0030] == "zero"
    ]
    fb.setupCharacterMap(cmap, uvs)
    fb.save(outPath)
    _verifyOutput(outPath, tables=["cmap"])
Example #9
0
class TTFBuilder:
    @classmethod
    def from_bdf_path(cls, path: Path):
        with path.open() as f:
            bdf = BdfFont.from_bdf(f)
        return cls(bdf)

    def __init__(self, bdf: BdfFont):
        self.bdf = bdf
        upm = 1000
        self.fb = FontBuilder(unitsPerEm=upm)
        self.ppp = upm / int(self.bdf.meta("SIZE")[0])
        w, h, startx, starty = self.bdf.meta("FONTBOUNDINGBOX")
        self.ascent = int(self.bdf.meta("FONT_ASCENT")[0])
        self.descent = int(self.bdf.meta("FONT_DESCENT")[0])
        self.w = int(w)
        self.h = int(h)
        self.startx = int(startx)
        self.starty = int(starty)

    def build(self, output_path: str):
        glyph_names = {
            k: glyph.meta("STARTCHAR")[0]
            for k, glyph in self.bdf.glyphs.items() if k >= 0
        }
        ascent = round(self.ascent * self.ppp)
        descent = round(self.descent * self.ppp)

        self.fb.setupGlyphOrder(list(glyph_names.values()))
        self.fb.setupCharacterMap(glyph_names)
        advance_widths = {
            name: int(self.bdf.glyphs[k].meta("SWIDTH")[0])
            for k, name in glyph_names.items()
        }
        family_name = "CozetteVector"
        style_name = "Regular"
        version = get_version()

        # scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=IWS-Chapter08
        namestrings = {
            "familyName": {
                "en": family_name
            },
            "styleName": {
                "en": style_name
            },
            "uniqueFontIdentifier": f"fontBuilder: {family_name}.{style_name}",
            "fullName": family_name,
            "psName": f"{family_name}-{style_name}",
            "version": f"Version {version}",
            "copyright": "(c) 2020 Slavfox",
            "manufacturer": "Slavfox",
            "designer": "Slavfox",
            "description": "Programming bitmap font optimized for coziness",
            "vendorURL": "https://github.com/slavfox",
            "designerURL": "https://github.com/slavfox",
            "licenseDescription": LICENSE_TEXT,
            "licenseInfoURL": "https://opensource.org/licenses/MIT",
            "sampleText": "A wizard’s job is to vex chumps quickly in fog.",
        }
        self.fb.setupGlyf({
            name: self.bdf.glyphs[k].draw(self.ppp)
            for k, name in glyph_names.items()
        })
        metrics = {
            name: (w, self.fb.font["glyf"][name].xMin)
            for name, w in advance_widths.items()
        }
        self.fb.setupHorizontalMetrics(metrics)
        self.fb.setupHorizontalHeader(ascent=ascent, descent=-descent)
        self.fb.setupNameTable(namestrings)
        self.fb.setupOS2(
            sTypoAscender=ascent,
            usWinAscent=ascent,
            usWinDescent=descent,
            panose={
                **_panoseDefaults,
                "bFamilyType": 2,  # Text and display
                "bProportion": 9  # Monospace
            })
        self.fb.setupPost(isFixedPitch=1)
        self.fb.save(output_path)
Example #10
0
def test_build_var(tmpdir):
    outPath = os.path.join(str(tmpdir), "test_var.ttf")

    fb = FontBuilder(1024, isTTF=True)
    fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
    fb.setupCharacterMap({65: "A", 97: "a"})

    advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}

    familyName = "HelloTestFont"
    styleName = "TotallyNormal"
    nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
                       styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
    nameStrings['psName'] = familyName + "-" + styleName

    pen = TTGlyphPen(None)
    pen.moveTo((100, 0))
    pen.lineTo((100, 400))
    pen.lineTo((500, 400))
    pen.lineTo((500, 000))
    pen.closePath()

    glyph = pen.glyph()

    pen = TTGlyphPen(None)
    emptyGlyph = pen.glyph()

    glyphs = {
        ".notdef": emptyGlyph,
        "A": glyph,
        "a": glyph,
        ".null": emptyGlyph
    }
    fb.setupGlyf(glyphs)
    metrics = {}
    glyphTable = fb.font["glyf"]
    for gn, advanceWidth in advanceWidths.items():
        metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
    fb.setupHorizontalMetrics(metrics)

    fb.setupHorizontalHeader(ascent=824, descent=200)
    fb.setupNameTable(nameStrings)

    axes = [
        ('LEFT', 0, 0, 100, "Left"),
        ('RGHT', 0, 0, 100, "Right"),
        ('UPPP', 0, 0, 100, "Up"),
        ('DOWN', 0, 0, 100, "Down"),
    ]
    instances = [
        dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0),
             stylename="TotallyNormal"),
        dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0),
             stylename="Right Up"),
    ]
    fb.setupFvar(axes, instances)
    variations = {}
    # Four (x, y) pairs and four phantom points:
    leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None]
    rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None]
    upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None]
    downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None]
    variations['a'] = [
        TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas),
        TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas),
        TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas),
        TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas),
    ]
    fb.setupGvar(variations)

    fb.setupOS2()
    fb.setupPost()
    fb.setupDummyDSIG()

    fb.save(outPath)

    _verifyOutput(outPath)
Example #11
0
def build(instance, opts, glyphOrder):
    font = instance.parent
    master = font.masters[0]

    advanceWidths = {}
    characterMap = {}
    charStrings = {}
    colorLayers = {}
    for name in glyphOrder:
        glyph = font.glyphs[name]
        if not glyph.export:
            continue
        for layer in glyph.layers:
            if "colorPalette" in layer.attributes:
                index = layer.attributes["colorPalette"]
                if name not in colorLayers:
                    colorLayers[name] = []
                if layer.layerId == layer.associatedMasterId:  # master layer
                    colorLayers[name].append((name, int(index)))
                else:
                    assert False, "can’t handle non-master color layers"

        if glyph.unicode:
            characterMap[int(glyph.unicode, 16)] = name

        layer = getLayer(glyph, instance)
        charStrings[name] = draw(layer, instance).getCharString()
        advanceWidths[name] = layer.width

    # XXX
    glyphOrder.pop(glyphOrder.index(".notdef"))
    glyphOrder.pop(glyphOrder.index("space"))
    glyphOrder.insert(0, ".notdef")
    glyphOrder.insert(1, "space")

    version = float(opts.version)

    vendor = get_property(font, "vendorID")
    names = {
        "copyright": font.copyright,
        "familyName": instance.familyName,
        "styleName": instance.name,
        "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}",
        "fullName": instance.fullName,
        "version": f"Version {version:.03f}",
        "psName": instance.fontName,
        "manufacturer": font.manufacturer,
        "designer": font.designer,
        "vendorURL": font.manufacturerURL,
        "designerURL": font.designerURL,
        "licenseDescription": get_property(font, "licenses"),
        "licenseInfoURL": get_property(font, "licenseURL"),
        "sampleText": get_property(font, "sampleTexts"),
    }

    fb = FontBuilder(font.upm, isTTF=False)
    date = font.date.replace(tzinfo=datetime.timezone.utc)
    stat = opts.glyphs.stat()
    fb.updateHead(
        fontRevision=version,
        created=int(date.timestamp()) - mac_epoch_diff,
        modified=int(stat.st_mtime) - mac_epoch_diff,
    )
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(characterMap)
    fb.setupNameTable(names, mac=False)
    fb.setupHorizontalHeader(
        ascent=master.ascender,
        descent=master.descender,
        lineGap=master.customParameters["hheaLineGap"],
    )

    if opts.debug:
        fb.setupCFF(names["psName"], {}, charStrings, {})
        fb.font["CFF "].compile(fb.font)
    else:
        fb.setupCFF2(charStrings)

    metrics = {}
    for name, width in advanceWidths.items():
        bounds = charStrings[name].calcBounds(None) or [0]
        metrics[name] = (width, bounds[0])
    fb.setupHorizontalMetrics(metrics)

    fb.setupPost(
        underlinePosition=master.customParameters["underlinePosition"],
        underlineThickness=master.customParameters["underlineThickness"],
    )

    # Compile to get font bbox
    fb.font["head"].compile(fb.font)

    codePages = [
        CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]
    ]
    fb.setupOS2(
        version=4,
        sTypoAscender=master.ascender,
        sTypoDescender=master.descender,
        sTypoLineGap=master.customParameters["typoLineGap"],
        usWinAscent=fb.font["head"].yMax,
        usWinDescent=-fb.font["head"].yMin,
        sxHeight=master.xHeight,
        sCapHeight=master.capHeight,
        achVendID=vendor,
        fsType=calcBits(font.customParameters["fsType"], 0, 16),
        fsSelection=calcFsSelection(instance),
        ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0,
                                 32),
        ulCodePageRange1=calcBits(codePages, 0, 32),
    )

    fea = makeFeatures(instance, master, opts, glyphOrder)
    fb.addOpenTypeFeatures(fea)

    palettes = master.customParameters["Color Palettes"]
    palettes = [[tuple(v / 255 for v in c) for c in p] for p in palettes]
    fb.setupCPAL(palettes)
    fb.setupCOLR(colorLayers)

    instance.font = fb.font
    if opts.debug:
        fb.font.save(f"{instance.fontName}.otf")

    return fb.font
Example #12
0
def build(instance, opts):
    font = instance.parent
    source = font.masters[0]

    fea, marks = makeFeatures(instance, source)

    glyphOrder = []
    advanceWidths = {}
    characterMap = {}
    charStrings = {}

    source.blueValues = []
    source.otherBlues = []

    for zone in sorted(source.alignmentZones):
        pos = zone.position
        size = zone.size
        vals = sorted((pos, pos + size))
        if pos == 0 or size >= 0:
            source.blueValues.extend(vals)
        else:
            source.otherBlues.extend(vals)

    fontinfo = f"""
        FontName {instance.fontName}
        OrigEmSqUnits {font.upm}
        DominantV {source.verticalStems}
        DominantH {source.horizontalStems}
        BaselineOvershoot {source.blueValues[0]}
        BaselineYCoord {source.blueValues[1]}
        LcHeight {source.blueValues[2]}
        LcOvershoot {source.blueValues[3] - source.blueValues[2]}
        CapHeight {source.blueValues[4]}
        CapOvershoot {source.blueValues[5] - source.blueValues[4]}
        AscenderHeight {source.blueValues[6]}
        AscenderOvershoot {source.blueValues[7] - source.blueValues[6]}
        Baseline5 {source.otherBlues[1]}
        Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]}

        FlexOK true
        BlueFuzz 1
    """

    layerSet = {g.name: g.layers[source.id] for g in font.glyphs}
    for glyph in font.glyphs:
        if not glyph.export:
            continue
        name = glyph.name

        glyphOrder.append(name)
        for code in glyph.unicodes:
            characterMap[int(code, 16)] = name

        layer = glyph.layers[source.id]
        width = 0 if name in marks else layer.width

        # Draw glyph and remove overlaps.
        path = Path()
        layer.draw(DecomposePathPen(path, layerSet=layerSet))
        path.simplify(fix_winding=True, keep_starting_points=True)

        # Autohint.
        pen = BezPen(None, True)
        path.draw(pen)
        bez = "\n".join(["% " + name, "sc", *pen.bez, "ed", ""])
        hinted = hint_bez_glyph(fontinfo, bez)
        program = [width] + convertBezToT2(hinted)

        # Build CharString.
        charStrings[name] = T2CharString(program=program)
        advanceWidths[name] = width

    # Make sure .notdef is glyph index 0.
    glyphOrder.pop(glyphOrder.index(".notdef"))
    glyphOrder.insert(0, ".notdef")

    version = float(opts.version)
    vendor = font.customParameters["vendorID"]
    names = {
        "copyright": font.copyright,
        "familyName": instance.familyName,
        "styleName": instance.name,
        "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}",
        "fullName": instance.fullName,
        "version": f"Version {version:.03f}",
        "psName": instance.fontName,
        "manufacturer": font.manufacturer,
        "designer": font.designer,
        "description": font.customParameters["description"],
        "vendorURL": font.manufacturerURL,
        "designerURL": font.designerURL,
        "licenseDescription": font.customParameters["license"],
        "licenseInfoURL": font.customParameters["licenseURL"],
        "sampleText": font.customParameters["sampleText"],
    }

    date = int(font.date.timestamp()) - epoch_diff
    fb = FontBuilder(font.upm, isTTF=False)
    fb.updateHead(fontRevision=version, created=date, modified=date)
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(characterMap)
    fb.setupNameTable(names, mac=False)
    fb.setupHorizontalHeader(
        ascent=source.ascender,
        descent=source.descender,
        lineGap=source.customParameters["typoLineGap"],
    )

    privateDict = {
        "BlueValues": source.blueValues,
        "OtherBlues": source.otherBlues,
        "StemSnapH": source.horizontalStems,
        "StemSnapV": source.verticalStems,
        "StdHW": source.horizontalStems[0],
        "StdVW": source.verticalStems[0],
    }

    fontInfo = {
        "FullName": names["fullName"],
        "Notice": names["copyright"].replace("©", "\(c\)"),
        "version": f"{version:07.03f}",
        "Weight": instance.name,
    }
    fb.setupCFF(names["psName"], fontInfo, charStrings, privateDict)

    metrics = {}
    for i, (name, width) in enumerate(advanceWidths.items()):
        bounds = charStrings[name].calcBounds(None) or [0]
        metrics[name] = (width, bounds[0])
    fb.setupHorizontalMetrics(metrics)

    codePages = [
        CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]
    ]
    fb.setupOS2(
        version=4,
        sTypoAscender=source.ascender,
        sTypoDescender=source.descender,
        sTypoLineGap=source.customParameters["typoLineGap"],
        usWinAscent=source.ascender,
        usWinDescent=-source.descender,
        sxHeight=source.xHeight,
        sCapHeight=source.capHeight,
        achVendID=vendor,
        fsType=calcBits(font.customParameters["fsType"], 0, 16),
        fsSelection=calcFsSelection(instance),
        ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0,
                                 32),
        ulCodePageRange1=calcBits(codePages, 0, 32),
    )

    ut = int(source.customParameters["underlineThickness"])
    up = int(source.customParameters["underlinePosition"])
    fb.setupPost(underlineThickness=ut, underlinePosition=up + ut // 2)

    meta = newTable("meta")
    meta.data = {"dlng": "Arab", "slng": "Arab"}
    fb.font["meta"] = meta

    fb.addOpenTypeFeatures(fea)

    return fb.font
Example #13
0
def _save_ttfont(bbf):
    fb = FontBuilder(bbf.info.unitsPerEm, isTTF=True)
    fb.setupGlyphOrder(bbf.lib.glyphOrder)
    bbf._build_maps()
    fb.setupCharacterMap(bbf._unicodemap)
    glyf = {}
    metrics = {}
    for i in [1, 2]:
        for g in bbf:
            if i == 1 and g.components:
                continue
            if i == 2 and not g.components:
                continue
            pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000)
            try:
                g.draw(pen)
                glyf[g.name] = pen.pen.glyph()
            except Exception as e:
                pen = Cu2QuPen(TTGlyphPen(glyf), bbf.info.unitsPerEm / 1000)
                glyf[g.name] = pen.pen.glyph()
                pass
            bounds = g.bounds
            xMin = 0
            if bounds:
                xMin = bounds[0]
            metrics[g.name] = (g.width, xMin)
    versionMajor = bbf.info.versionMajor or 1
    versionMinor = bbf.info.versionMinor or 0
    fb.updateHead(
        fontRevision=versionMajor + versionMinor / 10**len(str(versionMinor)),
        created=_ufo_date_to_opentime(bbf.info.openTypeHeadCreated),
        lowestRecPPEM=bbf.info.openTypeHeadLowestRecPPEM
        or fb.font["head"].lowestRecPPEM,
    )
    fb.setupGlyf(glyf)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader(
        ascent=int(bbf.info.openTypeHheaAscender or bbf.info.ascender or 0),
        descent=int(bbf.info.openTypeHheaDescender or bbf.info.descender or 0),
    )
    os2 = {
        "sTypoAscender":
        bbf.info.openTypeOS2TypoAscender or bbf.info.ascender or 0,
        "sTypoDescender":
        bbf.info.openTypeOS2TypoDescender or bbf.info.descender or 0,
        "sxHeight":
        bbf.info.xHeight,
        "sCapHeight":
        bbf.info.capHeight,
    }
    for k in [
            "usWidthClass",
            "usWeightClass",
            "sTypoLineGap",
            "usWinAscent",
            "usWinDescent",
            "ySubscriptXSize",
            "ySubscriptYSize",
            "ySubscriptXOffset",
            "ySubscriptYOffset",
            "ySuperscriptXSize",
            "ySuperscriptYSize",
            "ySuperscriptXOffset",
            "ySuperscriptYOffset",
            "yStrikeoutSize",
            "yStrikeoutPosition",
    ]:
        infokey = k
        while not infokey[0].isupper():
            infokey = infokey[1:]
        infokey = "openTypeOS2" + infokey
        if hasattr(bbf.info, infokey):
            if getattr(bbf.info, infokey):
                os2[k] = getattr(bbf.info, infokey)
    fb.setupOS2(**os2)
    # Name tables
    nameStrings = dict(
        familyName=dict(en=bbf.info.familyName),
        styleName=dict(en=bbf.info.styleName),
        fullName=f"{bbf.info.familyName} {bbf.info.styleName}",
        psName=bbf.info.postscriptFontName,
        version="Version " +
        str(bbf.info.versionMajor +
            bbf.info.versionMinor / 10**len(str(bbf.info.versionMinor))),
    )
    if not nameStrings["psName"]:
        del nameStrings["psName"]
    fb.setupNameTable(nameStrings)
    # Kerning
    # Features

    fb.setupPost(
        underlinePosition=(bbf.info.postscriptUnderlinePosition or 0),
        underlineThickness=(bbf.info.postscriptUnderlineThickness or 0),
    )
    return fb.font
Example #14
0
def build(instance, opts):
    font = instance.parent
    master = font.masters[0]

    glyphOrder = []
    advanceWidths = {}
    characterMap = {}
    charStrings = {}
    colorLayers = {}
    for glyph in font.glyphs:
        if not glyph.export:
            continue
        name = glyph.name
        for layer in glyph.layers:
            if layer.name.startswith("Color "):
                _, index = layer.name.split(" ")
                if name not in colorLayers:
                    colorLayers[name] = []
                colorLayers[name].append((name, int(index)))

        glyphOrder.append(name)
        if glyph.unicode:
            characterMap[int(glyph.unicode, 16)] = name

        layer = getLayer(glyph, instance)
        charStrings[name] = draw(layer, instance).getCharString()
        advanceWidths[name] = layer.width

    # XXX
    glyphOrder.pop(glyphOrder.index(".notdef"))
    glyphOrder.pop(glyphOrder.index("space"))
    glyphOrder.insert(0, ".notdef")
    glyphOrder.insert(1, "space")

    version = float(opts.version)

    vendor = font.customParameters["vendorID"]
    names = {
        "copyright": font.copyright,
        "familyName": instance.familyName,
        "styleName": instance.name,
        "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}",
        "fullName": instance.fullName,
        "version": f"Version {version:.03f}",
        "psName": instance.fontName,
        "manufacturer": font.manufacturer,
        "designer": font.designer,
        "vendorURL": font.manufacturerURL,
        "designerURL": font.designerURL,
        "licenseDescription": font.customParameters["license"],
        "licenseInfoURL": font.customParameters["licenseURL"],
        "sampleText": font.customParameters["sampleText"],
    }

    fb = FontBuilder(font.upm, isTTF=False)
    fb.updateHead(fontRevision=version)
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(characterMap)
    fb.setupNameTable(names, mac=False)
    fb.setupHorizontalHeader(ascent=master.ascender,
                             descent=master.descender,
                             lineGap=master.customParameters['hheaLineGap'])

    if opts.debug:
        fb.setupCFF(names["psName"], {}, charStrings, {})
    else:
        fb.setupCFF2(charStrings)

    metrics = {}
    for name, width in advanceWidths.items():
        bounds = charStrings[name].calcBounds(None) or [0]
        metrics[name] = (width, bounds[0])
    fb.setupHorizontalMetrics(metrics)

    fb.setupPost()

    fea = makeFeatures(instance, master, opts)
    fb.addOpenTypeFeatures(fea)

    palettes = master.customParameters["Color Palettes"]
    palettes = [[tuple(int(v) / 255 for v in c.split(",")) for c in p]
                for p in palettes]
    fb.setupCPAL(palettes)
    fb.setupCOLR(colorLayers)

    instance.font = fb.font
    axes = [
        instance.weightValue,
        instance.widthValue,
        instance.customValue,
        instance.customValue1,
        instance.customValue2,
        instance.customValue3,
    ]
    instance.axes = {}
    for i, axis in enumerate(font.customParameters["Axes"]):
        instance.axes[axis["Tag"]] = axes[i]

    if opts.debug:
        fb.font.save(f"{instance.fontName}.otf")
        fb.font.saveXML(f"{instance.fontName}.ttx")

    return fb.font
Example #15
0
def test_subset_single_pos_format():
    fb = FontBuilder(unitsPerEm=1000)
    fb.setupGlyphOrder([".notdef", "a", "b", "c"])
    fb.setupCharacterMap({ord("a"): "a", ord("b"): "b", ord("c"): "c"})
    fb.setupNameTable({
        "familyName": "TestSingePosFormat",
        "styleName": "Regular"
    })
    fb.setupPost()
    fb.addOpenTypeFeatures("""
        feature kern {
            pos a -50;
            pos b -40;
            pos c -50;
        } kern;
    """)

    buf = io.BytesIO()
    fb.save(buf)
    buf.seek(0)

    font = TTFont(buf)

    # The input font has a SinglePos Format 2 subtable where each glyph has
    # different ValueRecords
    assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [
        '<Lookup>',
        '  <LookupType value="1"/>',
        '  <LookupFlag value="0"/>',
        '  <!-- SubTableCount=1 -->',
        '  <SinglePos index="0" Format="2">',
        '    <Coverage Format="1">',
        '      <Glyph value="a"/>',
        '      <Glyph value="b"/>',
        '      <Glyph value="c"/>',
        '    </Coverage>',
        '    <ValueFormat value="4"/>',
        '    <!-- ValueCount=3 -->',
        '    <Value index="0" XAdvance="-50"/>',
        '    <Value index="1" XAdvance="-40"/>',
        '    <Value index="2" XAdvance="-50"/>',
        '  </SinglePos>',
        '</Lookup>',
    ]

    options = subset.Options()
    subsetter = subset.Subsetter(options)
    subsetter.populate(unicodes=[ord("a"), ord("c")])
    subsetter.subset(font)

    # All the subsetted glyphs from the original SinglePos Format2 subtable
    # now have the same ValueRecord, so we use a more compact Format 1 subtable.
    assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [
        '<Lookup>',
        '  <LookupType value="1"/>',
        '  <LookupFlag value="0"/>',
        '  <!-- SubTableCount=1 -->',
        '  <SinglePos index="0" Format="1">',
        '    <Coverage Format="1">',
        '      <Glyph value="a"/>',
        '      <Glyph value="c"/>',
        '    </Coverage>',
        '    <ValueFormat value="4"/>',
        '    <Value XAdvance="-50"/>',
        '  </SinglePos>',
        '</Lookup>',
    ]
Example #16
0
def colrv1_path(tmp_path):
    base_glyph_names = ["uni%04X" % i for i in range(0xE000, 0xE000 + 10)]
    layer_glyph_names = ["glyph%05d" % i for i in range(10, 20)]
    glyph_order = [".notdef"] + base_glyph_names + layer_glyph_names

    pen = TTGlyphPen(glyphSet=None)
    pen.moveTo((0, 0))
    pen.lineTo((0, 500))
    pen.lineTo((500, 500))
    pen.lineTo((500, 0))
    pen.closePath()
    glyph = pen.glyph()
    glyphs = {g: glyph for g in glyph_order}

    fb = FontBuilder(unitsPerEm=1024, isTTF=True)
    fb.setupGlyphOrder(glyph_order)
    fb.setupCharacterMap(
        {int(name[3:], 16): name
         for name in base_glyph_names})
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
    fb.setupHorizontalHeader()
    fb.setupOS2()
    fb.setupPost()
    fb.setupNameTable({"familyName": "TestCOLRv1", "styleName": "Regular"})

    fb.setupCOLR(
        {
            "uniE000": (
                ot.PaintFormat.PaintColrLayers,
                [
                    {
                        "Format": ot.PaintFormat.PaintGlyph,
                        "Paint": (ot.PaintFormat.PaintSolid, 0),
                        "Glyph": "glyph00010",
                    },
                    {
                        "Format": ot.PaintFormat.PaintGlyph,
                        "Paint": (ot.PaintFormat.PaintSolid, (2, 0.3)),
                        "Glyph": "glyph00011",
                    },
                ],
            ),
            "uniE001": (
                ot.PaintFormat.PaintColrLayers,
                [
                    {
                        "Format": ot.PaintFormat.PaintTransform,
                        "Paint": {
                            "Format": ot.PaintFormat.PaintGlyph,
                            "Paint": {
                                "Format": ot.PaintFormat.PaintRadialGradient,
                                "x0": 250,
                                "y0": 250,
                                "r0": 250,
                                "x1": 200,
                                "y1": 200,
                                "r1": 0,
                                "ColorLine": {
                                    "ColorStop": [(0.0, 1), (1.0, 2)],
                                    "Extend": "repeat",
                                },
                            },
                            "Glyph": "glyph00012",
                        },
                        "Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0),
                    },
                    {
                        "Format": ot.PaintFormat.PaintGlyph,
                        "Paint": (ot.PaintFormat.PaintSolid, (1, 0.5)),
                        "Glyph": "glyph00013",
                    },
                ],
            ),
            "uniE002": (
                ot.PaintFormat.PaintColrLayers,
                [
                    {
                        "Format": ot.PaintFormat.PaintGlyph,
                        "Paint": {
                            "Format": ot.PaintFormat.PaintLinearGradient,
                            "x0": 0,
                            "y0": 0,
                            "x1": 500,
                            "y1": 500,
                            "x2": -500,
                            "y2": 500,
                            "ColorLine": {
                                "ColorStop": [(0.0, 1), (1.0, 2)]
                            },
                        },
                        "Glyph": "glyph00014",
                    },
                    {
                        "Format": ot.PaintFormat.PaintTransform,
                        "Paint": {
                            "Format": ot.PaintFormat.PaintGlyph,
                            "Paint": (ot.PaintFormat.PaintSolid, 1),
                            "Glyph": "glyph00015",
                        },
                        "Transform": (1, 0, 0, 1, 400, 400),
                    },
                ],
            ),
            "uniE003": {
                "Format": ot.PaintFormat.PaintRotate,
                "Paint": {
                    "Format": ot.PaintFormat.PaintColrGlyph,
                    "Glyph": "uniE001",
                },
                "angle": 45,
                "centerX": 250,
                "centerY": 250,
            },
            "uniE004": [
                ("glyph00016", 1),
                ("glyph00017", 2),
            ],
        }, )
    fb.setupCPAL(
        [
            [
                (1.0, 0.0, 0.0, 1.0),  # red
                (0.0, 1.0, 0.0, 1.0),  # green
                (0.0, 0.0, 1.0, 1.0),  # blue
            ],
        ], )

    output_path = tmp_path / "TestCOLRv1.ttf"
    fb.save(output_path)

    return output_path
Example #17
0
def build(instance, isTTF, version):
    font = instance.parent
    source = font.masters[0]

    fea, marks = makeFeatures(instance, source)

    source.blueValues = []
    source.otherBlues = []

    for zone in sorted(source.alignmentZones):
        pos = zone.position
        size = zone.size
        vals = sorted((pos, pos + size))
        if pos == 0 or size >= 0:
            source.blueValues.extend(vals)
        else:
            source.otherBlues.extend(vals)

    fontinfo = f"""
        FontName {instance.fontName}
        OrigEmSqUnits {font.upm}
        DominantV {source.verticalStems}
        DominantH {source.horizontalStems}
        BaselineOvershoot {source.blueValues[0]}
        BaselineYCoord {source.blueValues[1]}
        LcHeight {source.blueValues[2]}
        LcOvershoot {source.blueValues[3] - source.blueValues[2]}
        CapHeight {source.blueValues[4]}
        CapOvershoot {source.blueValues[5] - source.blueValues[4]}
        AscenderHeight {source.blueValues[6]}
        AscenderOvershoot {source.blueValues[7] - source.blueValues[6]}
        Baseline5 {source.otherBlues[1]}
        Baseline5Overshoot {source.otherBlues[0] - source.otherBlues[1]}

        FlexOK true
        BlueFuzz 1
    """

    characterMap = {}
    glyphs = {}
    metrics = {}
    layerSet = {g.name: g.layers[source.id] for g in font.glyphs}

    if isTTF:
        from fontTools.pens.cu2quPen import Cu2QuPen
        from fontTools.pens.recordingPen import RecordingPen

        for glyph in font.glyphs:
            layer = glyph.layers[source.id]
            pen = RecordingPen()
            layer.draw(pen)
            layer.paths = []
            layer.components = []
            pen.replay(Cu2QuPen(layer.getPen(), 1.0, reverse_direction=True))

    for glyph in font.glyphs:
        if not glyph.export and not isTTF:
            continue
        name = glyph.name

        for code in glyph.unicodes:
            characterMap[int(code, 16)] = name

        layer = glyph.layers[source.id]
        width = 0 if name in marks else layer.width

        pen = BoundsPen(layerSet)
        layer.draw(pen)
        metrics[name] = (width, pen.bounds[0] if pen.bounds else 0)

        if isTTF:
            from fontTools.pens.ttGlyphPen import TTGlyphPen

            pen = TTGlyphPen(layerSet)
            if layer.paths:
                # Decompose and remove overlaps.
                path = Path()
                layer.draw(DecomposePathPen(path, layerSet))
                path.simplify(fix_winding=True, keep_starting_points=True)
                path.draw(pen)
            else:
                # Composite-only glyph, no need to decompose.
                layer.draw(FlattenComponentsPen(pen, layerSet))
            glyphs[name] = pen.glyph()
        else:
            from fontTools.pens.t2CharStringPen import T2CharStringPen

            # Draw glyph and remove overlaps.
            path = Path()
            layer.draw(DecomposePathPen(path, layerSet))
            path.simplify(fix_winding=True, keep_starting_points=True)

            # Build CharString.
            pen = T2CharStringPen(width, None)
            path.draw(pen)
            glyphs[name] = pen.getCharString()

    vendor = font.customParameters["vendorID"]
    names = {
        "copyright": font.copyright,
        "familyName": instance.familyName,
        "styleName": instance.name,
        "uniqueFontIdentifier": f"{version};{vendor};{instance.fontName}",
        "fullName": instance.fullName,
        "version": f"Version {version}",
        "psName": instance.fontName,
        "manufacturer": font.manufacturer,
        "designer": font.designer,
        "description": font.customParameters["description"],
        "vendorURL": font.manufacturerURL,
        "designerURL": font.designerURL,
        "licenseDescription": font.customParameters["license"],
        "licenseInfoURL": font.customParameters["licenseURL"],
        "sampleText": font.customParameters["sampleText"],
    }

    date = int(font.date.timestamp()) - epoch_diff
    fb = FontBuilder(font.upm, isTTF=isTTF)
    fb.updateHead(fontRevision=version, created=date, modified=date)
    fb.setupGlyphOrder(font.glyphOrder)
    fb.setupCharacterMap(characterMap)
    fb.setupNameTable(names, mac=False)
    fb.setupHorizontalHeader(
        ascent=source.ascender,
        descent=source.descender,
        lineGap=source.customParameters["typoLineGap"],
    )

    if isTTF:
        fb.setupGlyf(glyphs)
    else:
        privateDict = {
            "BlueValues": source.blueValues,
            "OtherBlues": source.otherBlues,
            "StemSnapH": source.horizontalStems,
            "StemSnapV": source.verticalStems,
            "StdHW": source.horizontalStems[0],
            "StdVW": source.verticalStems[0],
        }

        fontInfo = {
            "FullName": names["fullName"],
            "Notice": names["copyright"].replace("©", "\(c\)"),
            "version": f"{version}",
            "Weight": instance.name,
        }

        fb.setupCFF(names["psName"], fontInfo, glyphs, privateDict)

    fb.setupHorizontalMetrics(metrics)

    codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]]
    fb.setupOS2(
        version=4,
        sTypoAscender=source.ascender,
        sTypoDescender=source.descender,
        sTypoLineGap=source.customParameters["typoLineGap"],
        usWinAscent=source.ascender,
        usWinDescent=-source.descender,
        sxHeight=source.xHeight,
        sCapHeight=source.capHeight,
        achVendID=vendor,
        fsType=calcBits(font.customParameters["fsType"], 0, 16),
        fsSelection=calcFsSelection(instance),
        ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32),
        ulCodePageRange1=calcBits(codePages, 0, 32),
    )

    underlineThickness = int(source.customParameters["underlineThickness"])
    underlinePosition = int(source.customParameters["underlinePosition"])
    fb.setupPost(
        keepGlyphNames=False,
        underlineThickness=underlineThickness,
        underlinePosition=underlinePosition + underlineThickness // 2,
    )

    fb.font["meta"] = meta = newTable("meta")
    meta.data = {"dlng": "Arab", "slng": "Arab"}

    fb.addOpenTypeFeatures(fea)

    if isTTF:
        from fontTools.ttLib.tables import ttProgram

        fb.setupDummyDSIG()

        fb.font["gasp"] = gasp = newTable("gasp")
        gasp.gaspRange = {0xFFFF: 15}

        fb.font["prep"] = prep = newTable("prep")
        prep.program = ttProgram.Program()
        assembly = ["PUSHW[]", "511", "SCANCTRL[]", "PUSHB[]", "4", "SCANTYPE[]"]
        prep.program.fromAssembly(assembly)
    else:
        from cffsubr import subroutinize

        subroutinize(fb.font)

    return fb.font
Example #18
0
def test_build_var(tmpdir):
    outPath = os.path.join(str(tmpdir), "test_var.ttf")

    fb = FontBuilder(1024, isTTF=True)
    fb.setupGlyphOrder([".notdef", ".null", "A", "a"])
    fb.setupCharacterMap({65: "A", 97: "a"})

    advanceWidths = {".notdef": 600, "A": 600, "a": 600, ".null": 600}

    familyName = "HelloTestFont"
    styleName = "TotallyNormal"
    nameStrings = dict(familyName=dict(en="HelloTestFont", nl="HalloTestFont"),
                       styleName=dict(en="TotallyNormal", nl="TotaalNormaal"))
    nameStrings['psName'] = familyName + "-" + styleName

    pen = TTGlyphPen(None)
    pen.moveTo((100, 0))
    pen.lineTo((100, 400))
    pen.lineTo((500, 400))
    pen.lineTo((500, 000))
    pen.closePath()

    glyph = pen.glyph()

    pen = TTGlyphPen(None)
    emptyGlyph = pen.glyph()

    glyphs = {".notdef": emptyGlyph, "A": glyph, "a": glyph, ".null": emptyGlyph}
    fb.setupGlyf(glyphs)
    metrics = {}
    glyphTable = fb.font["glyf"]
    for gn, advanceWidth in advanceWidths.items():
        metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
    fb.setupHorizontalMetrics(metrics)

    fb.setupHorizontalHeader(ascent=824, descent=200)
    fb.setupNameTable(nameStrings)

    axes = [
        ('LEFT', 0, 0, 100, "Left"),
        ('RGHT', 0, 0, 100, "Right"),
        ('UPPP', 0, 0, 100, "Up"),
        ('DOWN', 0, 0, 100, "Down"),
    ]
    instances = [
        dict(location=dict(LEFT=0, RGHT=0, UPPP=0, DOWN=0), stylename="TotallyNormal"),
        dict(location=dict(LEFT=0, RGHT=100, UPPP=100, DOWN=0), stylename="Right Up"),
    ]
    fb.setupFvar(axes, instances)
    variations = {}
    # Four (x, y) pairs and four phantom points:
    leftDeltas = [(-200, 0), (-200, 0), (0, 0), (0, 0), None, None, None, None]
    rightDeltas = [(0, 0), (0, 0), (200, 0), (200, 0), None, None, None, None]
    upDeltas = [(0, 0), (0, 200), (0, 200), (0, 0), None, None, None, None]
    downDeltas = [(0, -200), (0, 0), (0, 0), (0, -200), None, None, None, None]
    variations['a'] = [
        TupleVariation(dict(RGHT=(0, 1, 1)), rightDeltas),
        TupleVariation(dict(LEFT=(0, 1, 1)), leftDeltas),
        TupleVariation(dict(UPPP=(0, 1, 1)), upDeltas),
        TupleVariation(dict(DOWN=(0, 1, 1)), downDeltas),
    ]
    fb.setupGvar(variations)

    fb.setupOS2()
    fb.setupPost()
    fb.setupDummyDSIG()

    fb.save(outPath)

    _verifyOutput(outPath)
def make_font(file_paths, out_dir, revision, gsub_path, gpos_path, uvs_lst):
    cmap, gorder, validated_fpaths = {}, deque(), []
    # build glyph order
    for fpath in file_paths:
        # derive glyph name from file name
        gname = os.path.splitext(os.path.basename(fpath))[0]  # trim extension
        # validate glyph name
        if not glyph_name_is_valid(gname, fpath):
            continue
        # skip any duplicates and 'space'
        if gname in gorder or gname == 'space':
            log.warning("Skipped file '{}'. The glyph name derived from it "
                        "is either a duplicate or 'space'.".format(fpath))
            continue
        # limit the length of glyph name to 31 chars
        if len(gname) > 31:
            num = 0
            trimmed_gname = get_trimmed_glyph_name(gname, num)
            while trimmed_gname in gorder:
                num += 1
                trimmed_gname = get_trimmed_glyph_name(trimmed_gname, num)
            gorder.append(trimmed_gname)
            log.warning("Glyph name '{}' was trimmed to 31 characters: "
                        "'{}'".format(gname, trimmed_gname))
        else:
            gorder.append(gname)
        validated_fpaths.append(fpath)

        # add to cmap
        if RE_UNICODE.match(gname):
            uni_int = int(gname[1:], 16)  # trim leading 'u'
            cmap[uni_int] = gname

    fb = FontBuilder(UPM, isTTF=False)
    fb.font['head'].fontRevision = float(revision)
    fb.font['head'].lowestRecPPEM = 12

    cs_dict = {}
    cs_cache = {}
    for i, svg_file_path in enumerate(validated_fpaths):
        svg_file_realpath = os.path.realpath(svg_file_path)

        if svg_file_realpath not in cs_cache:
            svg_size = get_svg_size(svg_file_realpath)
            if svg_size is None:
                cs_dict[gorder[i]] = SPACE_CHARSTRING
                continue

            pen = T2CharStringPen(EMOJI_H_ADV, None)
            svg = SVGPath(svg_file_realpath,
                          transform=(EMOJI_SIZE / svg_size, 0, 0,
                                     -EMOJI_SIZE / svg_size,
                                     (EMOJI_H_ADV * .5) - (EMOJI_SIZE * .5),
                                     EMOJI_H_ADV * ABOVE_BASELINE))
            svg.draw(pen)
            cs = pen.getCharString()
            cs_cache[svg_file_realpath] = cs
        else:
            cs = cs_cache.get(svg_file_realpath)

        cs_dict[gorder[i]] = cs

    # add '.notdef', 'space' and zero-width joiner
    pen = T2CharStringPen(EMOJI_H_ADV, None)
    draw_notdef(pen)
    gorder.extendleft(reversed(['.notdef', 'space', 'ZWJ']))
    cs_dict.update({
        '.notdef': pen.getCharString(),
        'space': SPACE_CHARSTRING,
        'ZWJ': SPACE_CHARSTRING,
    })
    cmap.update({
        32: 'space',  # U+0020
        160: 'space',  # U+00A0
        8205: 'ZWJ',  # U+200D
    })

    # add TAG LATIN LETTER glyphs and mappings
    for cdpt in TAG_LAT_LETTR:
        tag_gname = f'u{cdpt}'
        gorder.append(tag_gname)
        cs_dict[tag_gname] = SPACE_CHARSTRING
        cmap[int(cdpt, 16)] = tag_gname

    fb.setupGlyphOrder(list(gorder))  # parts of FontTools require a list
    fb.setupCharacterMap(cmap, uvs=uvs_lst)
    fb.setupCFF(
        PS_NAME, {
            'version': revision,
            'Notice': TRADEMARK,
            'Copyright': COPYRIGHT,
            'FullName': FULL_NAME,
            'FamilyName': FAMILY_NAME,
            'Weight': STYLE_NAME
        }, cs_dict, {})

    glyphs_bearings = {}
    for gname, cs in cs_dict.items():
        gbbox = cs.calcBounds(None)
        if gbbox:
            xmin, ymin, _, ymax = gbbox
            if ymax > ASCENT:
                log.warning("Top of glyph '{}' may get clipped. "
                            "Glyph's ymax={}; Font's ascent={}".format(
                                gname, ymax, ASCENT))
            if ymin < DESCENT:
                log.warning("Bottom of glyph '{}' may get clipped. "
                            "Glyph's ymin={}; Font's descent={}".format(
                                gname, ymin, DESCENT))
            lsb = xmin
            tsb = EMOJI_V_ADV - ymax - EMOJI_H_ADV * (1 - ABOVE_BASELINE)
            glyphs_bearings[gname] = (lsb, tsb)
        else:
            glyphs_bearings[gname] = (0, 0)

    h_metrics = {}
    v_metrics = {}
    for gname in gorder:
        h_metrics[gname] = (EMOJI_H_ADV, glyphs_bearings[gname][0])
        v_metrics[gname] = (EMOJI_V_ADV, glyphs_bearings[gname][1])
    fb.setupHorizontalMetrics(h_metrics)
    fb.setupVerticalMetrics(v_metrics)

    fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT)

    v_ascent = EMOJI_H_ADV // 2
    v_descent = EMOJI_H_ADV - v_ascent
    fb.setupVerticalHeader(ascent=v_ascent,
                           descent=-v_descent,
                           caretSlopeRun=1)

    VERSION_STRING = 'Version {};{}'.format(revision, VENDOR)
    UNIQUE_ID = '{};{};{}'.format(revision, VENDOR, PS_NAME)
    name_strings = dict(
        copyright=COPYRIGHT,  # ID 0
        familyName=FAMILY_NAME,  # ID 1
        styleName=STYLE_NAME,  # ID 2
        uniqueFontIdentifier=UNIQUE_ID,  # ID 3
        fullName=FULL_NAME,  # ID 4
        version=VERSION_STRING,  # ID 5
        psName=PS_NAME,  # ID 6
        trademark=TRADEMARK,  # ID 7
        manufacturer=MANUFACTURER,  # ID 8
        designer=DESIGNER,  # ID 9
        vendorURL=VENDOR_URL,  # ID 11
        designerURL=DESIGNER_URL,  # ID 12
        licenseDescription=LICENSE,  # ID 13
        licenseInfoURL=LICENSE_URL,  # ID 14
    )
    fb.setupNameTable(name_strings, mac=False)

    fb.setupOS2(
        fsType=FSTYPE,
        achVendID=VENDOR,
        fsSelection=0x0040,  # REGULAR
        usWinAscent=ASCENT,
        usWinDescent=-DESCENT,
        sTypoAscender=ASCENT,
        sTypoDescender=DESCENT,
        sCapHeight=ASCENT,
        ulCodePageRange1=(1 << 1))  # set 1st CP bit

    if gsub_path:
        addOpenTypeFeatures(fb.font, gsub_path, tables=['GSUB'])

    if gpos_path:
        addOpenTypeFeatures(fb.font, gpos_path, tables=['GPOS'])

    fb.setupPost(isFixedPitch=1,
                 underlinePosition=UNDERLINE_POSITION,
                 underlineThickness=UNDERLINE_THICKNESS)

    fb.setupDummyDSIG()

    fb.save(os.path.join(out_dir, '{}.otf'.format(PS_NAME)))
Example #20
0
def obfuscate_plus(plain_text,
                   filename: str,
                   only_ttf: bool,
                   target_path: str = 'output'):
    """
    :param plain_text: 用户看到的内容
    :param filename: 不含格式后缀的文件名
    :param only_ttf: 是否需要woff、woff2格式
    :param target_path: 生成的目标目录
    """

    if str_has_whitespace(plain_text):
        raise Exception('明文不允许含有空格')

    if str_has_emoji(plain_text):
        raise Exception('明文不允许含有emoji')

    plain_text = deduplicate_str(plain_text)

    original_font = TTFont(root / 'base-font/KaiGenGothicCN-Regular.ttf')
    # https://github.com/fonttools/fonttools/blob/4.0.1/Lib/fontTools/fontBuilder.py#L28

    # <class 'dict'>: {32: 'cid00001', 33: 'cid00002', 34: 'cid00003'...}
    # key 为 ord(字符串)
    original_cmap: dict = original_font.getBestCmap()

    try:
        ensure_cmap_has_all_text(original_cmap, plain_text)
    except Exception as e:
        raise e

    # print('plain_text', plain_text)

    glyphs, metrics, cmap = {}, {}, {}

    # Unicode字符平面映射
    # https://zh.wikipedia.org/wiki/Unicode%E5%AD%97%E7%AC%A6%E5%B9%B3%E9%9D%A2%E6%98%A0%E5%B0%84
    private_codes = random.sample(range(0xE000, 0xF8FF), len(plain_text))

    # 中文汉字和常见英文数字等的unicode编码范围实例页面
    # https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html
    cjk_codes = random.sample(range(0x4E00, 0x9FA5), len(plain_text))

    # print('private_codes', private_codes)
    # print('cjk_codes', cjk_codes)

    # https://github.com/fonttools/fonttools/blob/4.0.1/Tests/pens/ttGlyphPen_test.py#L21
    glyph_set = original_font.getGlyphSet()

    pen = TTGlyphPen(glyph_set)

    glyph_order = original_font.getGlyphOrder()

    # print('glyph_order', glyph_order)

    final_shadow_text: list = []

    if 'null' in glyph_order:
        # print('基础字体含有 null')
        glyph_set['null'].draw(pen)
        glyphs['null'] = pen.glyph()
        metrics['null'] = original_font['hmtx']['null']

        final_shadow_text += ['null']

    if '.notdef' in glyph_order:
        # print('基础字体含有 .notdef')
        glyph_set['.notdef'].draw(pen)
        glyphs['.notdef'] = pen.glyph()
        metrics['.notdef'] = original_font['hmtx']['.notdef']

        final_shadow_text += ['.notdef']

    html_entities = []

    # 理论上这里还可以再打散一次顺序
    for index, plain in enumerate(plain_text):
        # print('index', index, 'plain', plain)

        try:
            shadow_cmap_name = original_cmap[cjk_codes[index]]
            # print('shadow_cmap_name', shadow_cmap_name)
        except KeyError:
            # 遇到基础字库不存在的字会出现这种错误
            traceback.print_exc()
            return obfuscate_plus(filename, plain_text, only_ttf, target_path)

        final_shadow_text += [shadow_cmap_name]

        glyph_set[original_cmap[ord(plain)]].draw(pen)
        glyphs[shadow_cmap_name] = pen.glyph()

        metrics[shadow_cmap_name] = original_font['hmtx'][original_cmap[ord(
            plain)]]

        cmap[private_codes[index]] = shadow_cmap_name
        html_entities += [hex(private_codes[index]).replace('0x', '&#x')]

    # print('cmap', cmap)
    # print('metrics', metrics)
    # print('final_shadow_text', final_shadow_text)
    # print('html_entities', html_entities)

    horizontal_header = {
        'ascent': original_font['hhea'].ascent,
        'descent': original_font['hhea'].descent,
    }

    fb = FontBuilder(original_font['head'].unitsPerEm, isTTF=True)
    fb.setupGlyphOrder(final_shadow_text)
    fb.setupCharacterMap(cmap)
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader(**horizontal_header)
    fb.setupNameTable(NAME_STRING)
    fb.setupOS2()
    fb.setupPost()
    fb.save(f'{root}/{target_path}/{filename}.ttf')
    # print('创建了新字体文件', f'{root}/{target_path}/{filename}.ttf')

    result = dict()
    result['ttf'] = f'{root}/{target_path}/{filename}.ttf'

    if only_ttf:
        return result
    else:
        woff_and_woff2 = subset_ttf_font(f'{root}/{target_path}/{filename}')
        return {
            **result,
            **woff_and_woff2
        }, dict(zip(plain_text, html_entities))
Example #21
0
def obfuscate(plain_text,
              shadow_text,
              filename: str,
              only_ttf: bool,
              target_path: str = 'output') -> dict:
    """
    :param plain_text: 用户看到的内容
    :param shadow_text: 爬虫看到的内容
    :param filename: 不含格式后缀的文件名
    :param only_ttf: 是否需要woff、woff2格式
    :param target_path: 生成的目标目录
    """

    if str_has_whitespace(plain_text) | str_has_whitespace(shadow_text):
        raise Exception('明文或阴书不允许含有空格')

    if str_has_emoji(plain_text) | str_has_emoji(shadow_text):
        raise Exception('明文或阴书不允许含有emoji')

    plain_text = deduplicate_str(plain_text)
    shadow_text = deduplicate_str(shadow_text)

    if plain_text == shadow_text:
        raise Exception('没有意义的混淆')

    if len(plain_text) != len(shadow_text):
        raise Exception('阴书的有效长度需与明文一致')

    original_font = TTFont(root / 'base-font/KaiGenGothicCN-Regular.ttf')
    # https://github.com/fonttools/fonttools/blob/4.0.1/Lib/fontTools/fontBuilder.py#L28

    # <class 'dict'>: {32: 'cid00001', 33: 'cid00002', 34: 'cid00003'...}
    # key 为 ord(字符串)
    original_cmap: dict = original_font.getBestCmap()

    try:
        ensure_cmap_has_all_text(original_cmap, plain_text)
    except Exception as e:
        raise e

    # print('plain_text', plain_text)
    # print('shadow_text', shadow_text)

    glyphs, metrics, cmap = {}, {}, {}

    # https://github.com/fonttools/fonttools/blob/4.0.1/Tests/pens/ttGlyphPen_test.py#L21
    glyph_set = original_font.getGlyphSet()

    pen = TTGlyphPen(glyph_set)

    glyph_order = original_font.getGlyphOrder()

    # print('glyph_order', glyph_order)

    final_shadow_text: list = []

    if 'null' in glyph_order:
        # print('基础字体含有 null')
        glyph_set['null'].draw(pen)
        glyphs['null'] = pen.glyph()
        metrics['null'] = original_font['hmtx']['null']

        final_shadow_text += ['null']

    if '.notdef' in glyph_order:
        # print('基础字体含有 .notdef')
        glyph_set['.notdef'].draw(pen)
        glyphs['.notdef'] = pen.glyph()
        metrics['.notdef'] = original_font['hmtx']['.notdef']

        final_shadow_text += ['.notdef']

    for index, (plain, shadow) in enumerate(zip(plain_text, shadow_text)):
        # print('index', index, 'plain', plain, 'shadow', shadow)

        shadow_cmap_name = original_cmap[ord(shadow)]
        # print('shadow_cmap_name', shadow_cmap_name)

        final_shadow_text += [shadow_cmap_name]

        glyph_set[original_cmap[ord(plain)]].draw(pen)
        glyphs[shadow_cmap_name] = pen.glyph()

        metrics[shadow_cmap_name] = original_font['hmtx'][original_cmap[ord(
            plain)]]

        cmap[ord(shadow)] = shadow_cmap_name

    # print('cmap', cmap)
    # print('metrics', metrics)
    # print('final_shadow_text', final_shadow_text)

    horizontal_header = {
        'ascent': original_font['hhea'].ascent,
        'descent': original_font['hhea'].descent,
    }

    fb = FontBuilder(original_font['head'].unitsPerEm, isTTF=True)
    fb.setupGlyphOrder(final_shadow_text)
    fb.setupCharacterMap(cmap)
    fb.setupGlyf(glyphs)
    fb.setupHorizontalMetrics(metrics)
    fb.setupHorizontalHeader(**horizontal_header)
    fb.setupNameTable(NAME_STRING)
    fb.setupOS2()
    fb.setupPost()
    # print('创建了新字体文件', f'{target_path}/{filename}.ttf')
    fb.save(f'{root}/{target_path}/{filename}.ttf')
    # print('创建了新字体文件', f'{target_path}/{filename}.ttf')

    result = dict()
    result['ttf'] = f'{root}/{target_path}/{filename}.ttf'

    if not only_ttf:
        woff_and_woff2 = subset_ttf_font(f'{root}/{target_path}/{filename}')
        result = {**result, **woff_and_woff2}

    return result
Example #22
0
def build(instance, opts):
    font = instance.parent
    master = font.masters[0]

    fea, marks = makeFeatures(instance, master)

    glyphOrder = []
    advanceWidths = {}
    characterMap = {}
    charStrings = {}
    for glyph in font.glyphs:
        if not glyph.export:
            continue
        name = glyph.name

        glyphOrder.append(name)
        if glyph.unicode:
            characterMap[int(glyph.unicode, 16)] = name

        layer = getLayer(glyph, instance)
        width = 0 if name in marks else layer.width
        path = Path()
        draw(layer, instance, path.getPen())
        path.simplify(fix_winding=True, keep_starting_points=True)
        pen = T2CharStringPen(width, None)
        path.draw(pen)
        charStrings[name] = pen.getCharString(optimize=False)
        advanceWidths[name] = width

    # XXX
    glyphOrder.pop(glyphOrder.index(".notdef"))
    glyphOrder.pop(glyphOrder.index("space"))
    glyphOrder.insert(0, ".notdef")
    glyphOrder.insert(1, "space")

    version = float(opts.version)
    vendor = font.customParameters["vendorID"]
    names = {
        "copyright": font.copyright,
        "familyName": instance.familyName,
        "styleName": instance.name,
        "uniqueFontIdentifier": f"{version:.03f};{vendor};{instance.fontName}",
        "fullName": instance.fullName,
        "version": f"Version {version:.03f}",
        "psName": instance.fontName,
        "manufacturer": font.manufacturer,
        "designer": font.designer,
        "description": font.customParameters["description"],
        "vendorURL": font.manufacturerURL,
        "designerURL": font.designerURL,
        "licenseDescription": font.customParameters["license"],
        "licenseInfoURL": font.customParameters["licenseURL"],
        "sampleText": font.customParameters["sampleText"],
    }

    date = int(font.date.timestamp()) - epoch_diff
    fb = FontBuilder(font.upm, isTTF=False)
    fb.updateHead(fontRevision=version, created=date, modified=date)
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(characterMap)
    fb.setupNameTable(names, mac=False)
    fb.setupHorizontalHeader(ascent=master.ascender, descent=master.descender,
                             lineGap=master.customParameters["typoLineGap"])

    privateDict = {
        "BlueValues": [],
        "OtherBlues": [],
        "StemSnapH": master.horizontalStems,
        "StemSnapV": master.verticalStems,
        "StdHW": master.horizontalStems[0],
        "StdVW": master.verticalStems[0],
    }
    for zone in sorted(master.alignmentZones):
        pos = zone.position
        size = zone.size
        vals = privateDict["BlueValues"] if pos == 0 or size >= 0 else privateDict["OtherBlues"]
        vals.extend(sorted((pos, pos + size)))

    fontInfo = {
        "FullName": names["fullName"],
        "Notice": names["copyright"].replace("©", "\(c\)"),
        "version": f"{version:07.03f}",
        "Weight": instance.name,
    }
    fb.setupCFF(names["psName"], fontInfo, charStrings, privateDict)

    metrics = {}
    for i, (name, width) in enumerate(advanceWidths.items()):
        bounds = charStrings[name].calcBounds(None) or [0]
        metrics[name] = (width, bounds[0])
    fb.setupHorizontalMetrics(metrics)

    codePages = [CODEPAGE_RANGES[v] for v in font.customParameters["codePageRanges"]]
    fb.setupOS2(version=4, sTypoAscender=master.ascender,
                sTypoDescender=master.descender,
                sTypoLineGap=master.customParameters["typoLineGap"],
                usWinAscent=master.ascender, usWinDescent=-master.descender,
                sxHeight=master.xHeight, sCapHeight=master.capHeight,
                achVendID=vendor,
                fsType=calcBits(font.customParameters["fsType"], 0, 16),
                fsSelection=calcFsSelection(instance),
                ulUnicodeRange1=calcBits(font.customParameters["unicodeRanges"], 0, 32),
                ulCodePageRange1=calcBits(codePages, 0, 32))

    ut = int(master.customParameters["underlineThickness"])
    up = int(master.customParameters["underlinePosition"])
    fb.setupPost(underlineThickness=ut, underlinePosition=up + ut//2)

    fb.addOpenTypeFeatures(fea)

    cidinfo = f"""
FontName	({names["psName"]})
FamilyName	({names["familyName"]})
Weight	({fontInfo["Weight"]})
version	({fontInfo["version"]})
Notice	({fontInfo["Notice"]})
Registry	Adobe
Ordering	Identity
Supplement	0
"""

    cidmap = f"mergefonts {instance.fontName}\n" \
            + "\n".join([f"{i} {n}" for i, n in enumerate(glyphOrder)])

    return fb.font, cidinfo, cidmap