def generate_and_write_autohinted_instance(
    instantiator: instantiator.Instantiator,
    instance_descriptor: fontTools.designspaceLib.InstanceDescriptor,
    output_dir: Path,
    psautohint: str,
):
    # 3. Generate instance UFO.
    instance = instantiator.generate_instance(instance_descriptor)
    file_stem = f"{instance.info.familyName}-{instance.info.styleName}".replace(" ", "")

    # 3.5. Optionally write instance UFO to disk, for debugging.
    # instance.save(output_dir / f"{file_stem}.ufo", overwrite=True)

    # 4. Compile and write instance OTF to disk. Do not optimize, because we have to
    # do it again after autohinting.
    instance_font = ufo2ft.compileOTF(
        instance,
        removeOverlaps=True,
        overlapsBackend="pathops",
        inplace=True,
        useProductionNames=True,
        optimizeCFF=ufo2ft.CFFOptimization.NONE,
    )
    output_path = output_dir / f"{file_stem}.otf"
    instance_font.save(output_path)

    # 5. Run psautohint on it.
    subprocess.run([psautohint, str(output_path)])

    # 6. Subroutinize (compress) it
    instance_font = fontTools.ttLib.TTFont(output_path)
    cffsubr.subroutinize(instance_font).save(output_path)
예제 #2
0
    def test_keep_glyph_names(self):
        font = load_test_font("SourceSansPro-Regular.subset.ttx")
        glyph_order = font.getGlyphOrder()

        assert font["post"].formatType == 3.0
        assert font["post"].glyphOrder is None

        cffsubr.subroutinize(font, cff_version=2)

        assert font["post"].formatType == 2.0
        assert font["post"].glyphOrder == glyph_order

        font2 = recompile_font(font)

        assert font2.getGlyphOrder() == glyph_order

        # now convert from CFF2 to CFF1 and check post format is set to 3.0
        # https://github.com/adobe-type-tools/cffsubr/issues/8
        cffsubr.subroutinize(font2, cff_version=1)

        assert font2["post"].formatType == 3.0
        assert font2["post"].glyphOrder == None

        font3 = recompile_font(font2)

        assert font3.getGlyphOrder() == glyph_order
예제 #3
0
    def test_output_different_cff_version(self, testfile, cff_version):
        font = load_test_font(testfile)
        table_tag = cffsubr._sniff_cff_table_format(font)

        cffsubr.subroutinize(font, cff_version=cff_version)

        assert cffsubr._sniff_cff_table_format(font) != table_tag
예제 #4
0
    def test_inplace(self, testfile):
        font = load_test_font(testfile)

        font2 = cffsubr.subroutinize(font, inplace=False)
        assert font is not font2

        font3 = cffsubr.subroutinize(font, inplace=True)
        assert font3 is font
예제 #5
0
    def test_output_same_cff_version(self, testfile, table_tag):
        font = load_test_font(testfile)

        assert cffsubr._sniff_cff_table_format(font) == table_tag

        cffsubr.subroutinize(font)

        assert cffsubr._sniff_cff_table_format(font) == table_tag
예제 #6
0
def test_desubroutinize(testfile):
    font = load_test_font(testfile)
    cffsubr.subroutinize(font)

    font2 = cffsubr.desubroutinize(font, inplace=False)

    assert cffsubr.has_subroutines(font)
    assert not cffsubr.has_subroutines(font2)
예제 #7
0
def main(args=None):
    """Compress OpenType Font's CFF or CFF2 table by computing subroutines."""

    parser = argparse.ArgumentParser("cffsubr", description=main.__doc__)
    parser.add_argument(
        "input_file", help="input font file. Must contain either CFF or CFF2 table"
    )
    output_group = parser.add_mutually_exclusive_group()
    output_group.add_argument(
        "-o",
        "--output-file",
        default=None,
        help="optional path to output file. By default, dump binary data to stdout",
    )
    output_group.add_argument(
        "-i",
        "--inplace",
        action="store_true",
        help="whether to overwrite the input file",
    )
    parser.add_argument(
        "-f",
        "--cff-version",
        default=None,
        type=int,
        choices=(1, 2),
        help="output CFF table format version",
    )
    parser.add_argument(
        "-N",
        "--no-glyph-names",
        dest="keep_glyph_names",
        action="store_false",
        help="whether to drop postscript glyph names when converting from CFF to CFF2.",
    )
    parser.add_argument(
        "-d",
        "--desubroutinize",
        action="store_true",
        help="Don't subroutinize, instead remove all subroutines (in any).",
    )
    options = parser.parse_args(args)

    if options.inplace:
        options.output_file = options.input_file
    elif not options.output_file:
        options.output_file = sys.stdout.buffer

    # Load TTFont lazily by default assuming output != input; load non-lazily if -i
    # option is passed, so that fontTools let us overwrite the input file.
    lazy = True if not options.inplace else None

    with ttLib.TTFont(options.input_file, lazy=lazy) as font:
        if options.desubroutinize:
            cffsubr.desubroutinize(font)
        else:
            cffsubr.subroutinize(font, options.cff_version, options.keep_glyph_names)
        font.save(options.output_file)
예제 #8
0
def generateFont(options, font):
    generateFeatures(font, options)

    from datetime import datetime
    info = font.info
    major, minor = options.version.split(".")
    info.versionMajor, info.versionMinor = int(major), int(minor)
    year = datetime.now().year
    info.copyright = f"Copyright 2010-{year} The Amiri Project Authors (https://github.com/alif-type/amiri)."
    info.openTypeNameLicense = "This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL"
    info.openTypeNameLicenseURL = "https://scripts.sil.org/OFL"

    if options.output.endswith(".ttf"):
        from fontTools.ttLib import newTable
        from fontTools.ttLib.tables import ttProgram
        otf = compileTTF(font,
                         inplace=True,
                         removeOverlaps=True,
                         overlapsBackend="pathops",
                         featureWriters=[])

        otf["DSIG"] = DSIG = newTable("DSIG")
        DSIG.ulVersion = 1
        DSIG.usFlag = 0
        DSIG.usNumSigs = 0
        DSIG.signatureRecords = []

        otf["prep"] = prep = newTable("prep")
        prep.program = ttProgram.Program()
        prep.program.fromAssembly(
            ['PUSHW[]', '511', 'SCANCTRL[]', 'PUSHB[]', '4', 'SCANTYPE[]'])
    else:
        import cffsubr
        otf = compileOTF(font,
                         inplace=True,
                         optimizeCFF=0,
                         removeOverlaps=True,
                         overlapsBackend="pathops",
                         featureWriters=[])
        cffsubr.subroutinize(otf)

    if info.styleMapStyleName and "italic" in info.styleMapStyleName:
        otf['name'].names = [n for n in otf['name'].names if n.nameID != 17]
        for name in otf['name'].names:
            if name.nameID == 2:
                name.string = info.styleName
    glyf = otf.get("glyf")
    if glyf:
        from fontTools.ttLib.tables._g_l_y_f import UNSCALED_COMPONENT_OFFSET
        for name, glyph in glyf.glyphs.items():
            glyph.expand(glyf)
            if glyph.isComposite():
                for component in glyph.components:
                    component.flags |= UNSCALED_COMPONENT_OFFSET

    return otf
예제 #9
0
    def test_non_standard_upem_mute_font_matrix_warning(self, caplog):
        # See https://github.com/adobe-type-tools/cffsubr/issues/13
        font = load_test_font("FontMatrixTest.ttx")

        assert font["CFF "].cff[0].FontMatrix == [0.0005, 0, 0, 0.0005, 0, 0]

        cffsubr.subroutinize(font, cff_version=2)

        with caplog.at_level(logging.WARNING, logger=cffLib.log.name):
            font2 = recompile_font(font)

        assert (
            "Some CFF FDArray/FontDict keys were ignored upon compile: FontMatrix"
            not in caplog.text)
예제 #10
0
    def _optimize(self, otf, name, fmt):
        if fmt == Format.OTF:
            import cffsubr
            from fontTools.cffLib.specializer import specializeProgram

            logger.info(f"Optimizing {name}.{fmt.value}")
            topDict = otf["CFF "].cff.topDictIndex[0]
            charStrings = topDict.CharStrings
            for charString in charStrings.values():
                charString.decompile()
                charString.program = specializeProgram(charString.program)

            logger.info(f"Subroutinizing {name}.{fmt.value}")
            cffsubr.subroutinize(otf, keep_glyph_names=False)
        return otf
예제 #11
0
    def test_drop_glyph_names(self):
        font = load_test_font("SourceSansPro-Regular.subset.ttx")
        glyph_order = font.getGlyphOrder()

        assert font["post"].formatType == 3.0
        assert font["post"].glyphOrder is None

        cffsubr.subroutinize(font, cff_version=2, keep_glyph_names=False)

        assert font["post"].formatType == 3.0
        assert font["post"].glyphOrder is None

        buf = io.BytesIO()
        font.save(buf)
        buf.seek(0)
        font2 = ttLib.TTFont(buf)

        assert font2.getGlyphOrder() != glyph_order
예제 #12
0
    def _subroutinize_with_cffsubr(cls, otf, cffVersion):
        import cffsubr

        cffInputVersion = cls._get_cff_version(otf)
        assert cffInputVersion is not None, "Missing required 'CFF ' or 'CFF2' table"

        msg = f"Subroutinizing {cffInputVersion.name} table with cffsubr"
        if cffInputVersion != cffVersion:
            msg += f" (output format: {cffVersion.name})"
        logger.info(msg)

        return cffsubr.subroutinize(otf, cff_version=cffVersion, keep_glyph_names=False)
예제 #13
0
def buildVF(opts):
    font = GSFont(opts.glyphs)
    glyphOrder = buildAltGlyphs(font)
    prepare(font)

    for instance in font.instances:
        print(f" MASTER  {instance.name}")
        build(instance, opts, glyphOrder)
        if instance.name == "Regular":
            regular = instance

    ds = DesignSpaceDocument()

    for i, axisDef in enumerate(font.axes):
        axis = ds.newAxisDescriptor()
        axis.tag = axisDef.axisTag
        axis.name = axisDef.name
        axis.maximum = max(x.axes[i] for x in font.instances)
        axis.minimum = min(x.axes[i] for x in font.instances)
        axis.default = regular.axes[i]
        ds.addAxis(axis)

    for instance in font.instances:
        source = ds.newSourceDescriptor()
        source.font = instance.font
        source.familyName = instance.familyName
        source.styleName = instance.name
        source.name = instance.fullName
        source.location = {
            a.name: instance.axes[i]
            for i, a in enumerate(ds.axes)
        }
        ds.addSource(source)

    print(f" MERGE   {font.familyName}")
    otf, _, _ = merge(ds)
    subroutinize(otf)
    if not opts.debug:
        otf["post"].formatType = 3.0
    return otf
예제 #14
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
예제 #15
0
def test_has_subroutines(testfile):
    font = load_test_font(testfile)

    assert not cffsubr.has_subroutines(font)
    assert cffsubr.has_subroutines(cffsubr.subroutinize(font))
designspace.instances = [
    s for s in designspace.instances
    if s.lib.get("com.schriftgestaltung.export", True)
]

# 2. Compile variable OTF from the masters. Do not optimize, because we have to do
# it again after autohinting.
varfont = ufo2ft.compileVariableCFF2(
    designspace,
    inplace=True,
    useProductionNames=True,
    optimizeCFF=ufo2ft.CFFOptimization.NONE,
)

# 3. Generate STAT table.
stylespace = statmake.classes.Stylespace.from_file(stylespace_path)
statmake.lib.apply_stylespace_to_variable_font(stylespace, varfont, {})

# 4. Save. External tools after this point.
varfont.save(output_path)

# 5. Autohint
subprocess.check_call(
    [os.fspath(args.psautohint_path),
     os.fspath(output_path)])

# 6. Subroutinize (compress)
varfont = fontTools.ttLib.TTFont(output_path)
cffsubr.subroutinize(varfont).save(output_path)