Esempio n. 1
0
def main(args):
    if not args:
        return
    filename, glyphs = args[0], args[1:]
    if not glyphs:
        glyphs = [
            'e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal'
        ]
    from fontemon_blender_addon.fontTools.ttLib import TTFont
    font = TTFont(filename)
    _test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
def main(args=None):
    """Calculate optimum defaultWidthX/nominalWidthX values"""

    import argparse
    parser = argparse.ArgumentParser(
        "fontemon_blender_addon.fontTools cffLib.width",
        description=main.__doc__,
    )
    parser.add_argument('inputs',
                        metavar='FILE',
                        type=str,
                        nargs='+',
                        help="Input TTF files")
    parser.add_argument('-b',
                        '--brute-force',
                        dest="brute",
                        action="store_true",
                        help="Use brute-force approach (VERY slow)")

    args = parser.parse_args(args)

    for fontfile in args.inputs:
        font = TTFont(fontfile)
        hmtx = font['hmtx']
        widths = [m[0] for m in hmtx.metrics.values()]
        if args.brute:
            default, nominal = optimizeWidthsBruteforce(widths)
        else:
            default, nominal = optimizeWidths(widths)
        print("glyphs=%d default=%d nominal=%d byteCost=%d" %
              (len(widths), default, nominal, byteCost(widths, default,
                                                       nominal)))
def main(args=None):
    """Optimize a font's GDEF variation store"""
    from argparse import ArgumentParser
    from fontemon_blender_addon.fontTools import configLogger
    from fontemon_blender_addon.fontTools.ttLib import TTFont
    from fontemon_blender_addon.fontTools.ttLib.tables.otBase import OTTableWriter

    parser = ArgumentParser(prog='varLib.varStore', description=main.__doc__)
    parser.add_argument('fontfile')
    parser.add_argument('outfile', nargs='?')
    options = parser.parse_args(args)

    # TODO: allow user to configure logging via command-line options
    configLogger(level="INFO")

    fontfile = options.fontfile
    outfile = options.outfile

    font = TTFont(fontfile)
    gdef = font['GDEF']
    store = gdef.table.VarStore

    writer = OTTableWriter()
    store.compile(writer, font)
    size = len(writer.getAllData())
    print("Before: %7d bytes" % size)

    varidx_map = store.optimize()

    gdef.table.remap_device_varidxes(varidx_map)
    if 'GPOS' in font:
        font['GPOS'].table.remap_device_varidxes(varidx_map)

    writer = OTTableWriter()
    store.compile(writer, font)
    size = len(writer.getAllData())
    print("After:  %7d bytes" % size)

    if outfile is not None:
        font.save(outfile)
Esempio n. 4
0
def gameToFont(charstring_directory_path, ttxFilePath, out_path, mutable_game,
               smaller, feature_name):
    # type: (str,str, str, bpy.SceneTreeOutputType, bool, str) -> None

    # Don't be a jerk. Leave the logo intact
    # I've made the entire software
    # free as in freedom, all I ask is that my logo stays in the front, so I
    # can attract more people to work on open source software to make the
    # world a better place.
    game = add_code_relay_logo(mutable_game)
    parseGame(out_game=game, smaller=smaller)
    nodeId_to_list_of_frame_blank_glyph_ID, blank_glyph_ranges = get_nodeId_to_list_of_frame_blank_glyph_ID_map(
        game)
    featureFile = createFeatureFile(game,
                                    nodeId_to_list_of_frame_blank_glyph_ID,
                                    blank_glyph_ranges, feature_name)

    xmlOutput = addCharStringsToTTX(charstring_directory_path,
                                    game,
                                    nodeId_to_list_of_frame_blank_glyph_ID,
                                    smaller,
                                    ttxFilePath=ttxFilePath)

    tt = TTFont()
    tt.importXML(StringIO(xmlOutput))
    builder.addOpenTypeFeaturesFromString(tt, featureFile)
    # set the required feature index to the one we will cre
    for scriptRecord in tt['GSUB'].table.ScriptList.ScriptRecord:
        scriptRecord.Script.DefaultLangSys.ReqFeatureIndex = 0

    cff = tt['CFF ']
    cffTable = cff.cff['Fontemon']
    cffTable.Private.defaultWidthX = 0
    cffTable.Private.nominalWidthX = 0
    tt.save(out_path)
Esempio n. 5
0
def ttList(input, output, options):
    ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True)
    reader = ttf.reader
    tags = sorted(reader.keys())
    print('Listing table info for "%s":' % input)
    format = "    %4s  %10s  %8s  %8s"
    print(format % ("tag ", "  checksum", "  length", "  offset"))
    print(format % ("----", "----------", "--------", "--------"))
    for tag in tags:
        entry = reader.tables[tag]
        if ttf.flavor == "woff2":
            # WOFF2 doesn't store table checksums, so they must be calculated
            from fontemon_blender_addon.fontTools.ttLib.sfnt import calcChecksum
            data = entry.loadData(reader.transformBuffer)
            checkSum = calcChecksum(data)
        else:
            checkSum = int(entry.checkSum)
        if checkSum < 0:
            checkSum = checkSum + 0x100000000
        checksum = "0x%08X" % checkSum
        print(format % (tag, checksum, entry.length, entry.offset))
    print()
    ttf.close()
Esempio n. 6
0
def ttCompile(input, output, options):
    log.info('Compiling "%s" to "%s"...' % (input, output))
    if options.useZopfli:
        from fontemon_blender_addon.fontTools.ttLib import sfnt
        sfnt.USE_ZOPFLI = True
    ttf = TTFont(options.mergeFile,
                 flavor=options.flavor,
                 recalcBBoxes=options.recalcBBoxes,
                 recalcTimestamp=options.recalcTimestamp,
                 allowVID=options.allowVID)
    ttf.importXML(input)

    if options.recalcTimestamp is None and 'head' in ttf:
        # use TTX file modification time for head "modified" timestamp
        mtime = os.path.getmtime(input)
        ttf['head'].modified = timestampSinceEpoch(mtime)

    ttf.save(output)
Esempio n. 7
0
def ttDump(input, output, options):
    log.info('Dumping "%s" to "%s"...', input, output)
    if options.unicodedata:
        setUnicodeData(options.unicodedata)
    ttf = TTFont(input,
                 0,
                 allowVID=options.allowVID,
                 ignoreDecompileErrors=options.ignoreDecompileErrors,
                 fontNumber=options.fontNumber)
    ttf.saveXML(output,
                tables=options.onlyTables,
                skipTables=options.skipTables,
                splitTables=options.splitTables,
                splitGlyphs=options.splitGlyphs,
                disassembleInstructions=options.disassembleInstructions,
                bitmapGlyphDataFormat=options.bitmapGlyphDataFormat,
                newlinestr=options.newlinestr)
    ttf.close()
Esempio n. 8
0
def _open_font(path, master_finder=lambda s: s):
	# load TTFont masters from given 'path': this can be either a .TTX or an
	# OpenType binary font; or if neither of these, try use the 'master_finder'
	# callable to resolve the path to a valid .TTX or OpenType font binary.
	from fontemon_blender_addon.fontTools.ttx import guessFileType

	master_path = os.path.normpath(path)
	tp = guessFileType(master_path)
	if tp is None:
		# not an OpenType binary/ttx, fall back to the master finder.
		master_path = master_finder(master_path)
		tp = guessFileType(master_path)
	if tp in ("TTX", "OTX"):
		font = TTFont()
		font.importXML(master_path)
	elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
		font = TTFont(master_path)
	else:
		raise VarLibValidationError("Invalid master path: %r" % master_path)
	return font
Esempio n. 9
0
            "  (The file format will be PNG, regardless of the image file name supplied)"
        )
        sys.exit(0)

    from fontemon_blender_addon.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 fontemon_blender_addon.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)
def main(args=None):
    """Add features from a feature file (.fea) into a OTF font"""
    parser = argparse.ArgumentParser(
        description=
        "Use fontemon_blender_addon.fontTools to compile OpenType feature files (*.fea)."
    )
    parser.add_argument("input_fea",
                        metavar="FEATURES",
                        help="Path to the feature file")
    parser.add_argument("input_font",
                        metavar="INPUT_FONT",
                        help="Path to the input font")
    parser.add_argument(
        "-o",
        "--output",
        dest="output_font",
        metavar="OUTPUT_FONT",
        help="Path to the output font.",
    )
    parser.add_argument(
        "-t",
        "--tables",
        metavar="TABLE_TAG",
        choices=Builder.supportedTables,
        nargs="+",
        help="Specify the table(s) to be built.",
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Add source-level debugging information to font.",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        help="increase the logger verbosity. Multiple -v "
        "options are allowed.",
        action="count",
        default=0,
    )
    parser.add_argument("--traceback",
                        help="show traceback for exceptions.",
                        action="store_true")
    options = parser.parse_args(args)

    levels = ["WARNING", "INFO", "DEBUG"]
    configLogger(level=levels[min(len(levels) - 1, options.verbose)])

    output_font = options.output_font or makeOutputFileName(options.input_font)
    log.info("Compiling features to '%s'" % (output_font))

    font = TTFont(options.input_font)
    try:
        addOpenTypeFeatures(font,
                            options.input_fea,
                            tables=options.tables,
                            debug=options.debug)
    except FeatureLibError as e:
        if options.traceback:
            raise
        log.error(e)
    font.save(output_font)
Esempio n. 11
0
def main(args=None):
    """Instantiate a variation font"""
    from fontemon_blender_addon.fontTools import configLogger
    import argparse

    parser = argparse.ArgumentParser(
        "fontemon_blender_addon.fontTools varLib.mutator",
        description="Instantiate a variable font")
    parser.add_argument("input",
                        metavar="INPUT.ttf",
                        help="Input variable TTF file.")
    parser.add_argument(
        "locargs",
        metavar="AXIS=LOC",
        nargs="*",
        help="List of space separated locations. A location consist in "
        "the name of a variation axis, followed by '=' and a number. E.g.: "
        " wght=700 wdth=80. The default is the location of the base master.")
    parser.add_argument(
        "-o",
        "--output",
        metavar="OUTPUT.ttf",
        default=None,
        help="Output instance TTF file (default: INPUT-instance.ttf).")
    logging_group = parser.add_mutually_exclusive_group(required=False)
    logging_group.add_argument("-v",
                               "--verbose",
                               action="store_true",
                               help="Run more verbosely.")
    logging_group.add_argument("-q",
                               "--quiet",
                               action="store_true",
                               help="Turn verbosity off.")
    parser.add_argument(
        "--no-overlap",
        dest="overlap",
        action="store_false",
        help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags.")
    options = parser.parse_args(args)

    varfilename = options.input
    outfile = (os.path.splitext(varfilename)[0] +
               '-instance.ttf' if not options.output else options.output)
    configLogger(level=(
        "DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO"))

    loc = {}
    for arg in options.locargs:
        try:
            tag, val = arg.split('=')
            assert len(tag) <= 4
            loc[tag.ljust(4)] = float(val)
        except (ValueError, AssertionError):
            parser.error("invalid location argument format: %r" % arg)
    log.info("Location: %s", loc)

    log.info("Loading variable font")
    varfont = TTFont(varfilename)

    instantiateVariableFont(varfont,
                            loc,
                            inplace=True,
                            overlap=options.overlap)

    log.info("Saving instance font %s", outfile)
    varfont.save(outfile)
Esempio n. 12
0
def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
    """ Generate a static instance from a variable TTFont and a dictionary
	defining the desired location along the variable font's axes.
	The location values must be specified as user-space coordinates, e.g.:

		{'wght': 400, 'wdth': 100}

	By default, a new TTFont object is returned. If ``inplace`` is True, the
	input varfont is modified and reduced to a static font.

	When the overlap parameter is defined as True,
	OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1.  See
	https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
	"""
    if not inplace:
        # make a copy to leave input varfont unmodified
        stream = BytesIO()
        varfont.save(stream)
        stream.seek(0)
        varfont = TTFont(stream)

    fvar = varfont['fvar']
    axes = {
        a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
        for a in fvar.axes
    }
    loc = normalizeLocation(location, axes)
    if 'avar' in varfont:
        maps = varfont['avar'].segments
        loc = {k: piecewiseLinearMap(v, maps[k]) for k, v in loc.items()}
    # Quantize to F2Dot14, to avoid surprise interpolations.
    loc = {k: floatToFixedToFloat(v, 14) for k, v in loc.items()}
    # Location is normalized now
    log.info("Normalized location: %s", loc)

    if 'gvar' in varfont:
        log.info("Mutating glyf/gvar tables")
        gvar = varfont['gvar']
        glyf = varfont['glyf']
        # get list of glyph names in gvar sorted by component depth
        glyphnames = sorted(
            gvar.variations.keys(),
            key=lambda name:
            (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
             if glyf[name].isComposite() else 0, name))
        for glyphname in glyphnames:
            variations = gvar.variations[glyphname]
            coordinates, _ = glyf.getCoordinatesAndControls(glyphname, varfont)
            origCoords, endPts = None, None
            for var in variations:
                scalar = supportScalar(loc, var.axes)
                if not scalar: continue
                delta = var.coordinates
                if None in delta:
                    if origCoords is None:
                        origCoords, g = glyf.getCoordinatesAndControls(
                            glyphname, varfont)
                    delta = iup_delta(delta, origCoords, g.endPts)
                coordinates += GlyphCoordinates(delta) * scalar
            glyf.setCoordinates(glyphname, coordinates, varfont)
    else:
        glyf = None

    if 'cvar' in varfont:
        log.info("Mutating cvt/cvar tables")
        cvar = varfont['cvar']
        cvt = varfont['cvt ']
        deltas = {}
        for var in cvar.variations:
            scalar = supportScalar(loc, var.axes)
            if not scalar: continue
            for i, c in enumerate(var.coordinates):
                if c is not None:
                    deltas[i] = deltas.get(i, 0) + scalar * c
        for i, delta in deltas.items():
            cvt[i] += otRound(delta)

    if 'CFF2' in varfont:
        log.info("Mutating CFF2 table")
        glyphOrder = varfont.getGlyphOrder()
        CFF2 = varfont['CFF2']
        topDict = CFF2.cff.topDictIndex[0]
        vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes,
                                        loc)
        interpolateFromDeltas = vsInstancer.interpolateFromDeltas
        interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
        CFF2.desubroutinize()
        interpolate_cff2_charstrings(topDict, interpolateFromDeltas,
                                     glyphOrder)
        interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
        del topDict.rawDict['VarStore']
        del topDict.VarStore

    if 'MVAR' in varfont:
        log.info("Mutating MVAR table")
        mvar = varfont['MVAR'].table
        varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
        records = mvar.ValueRecord
        for rec in records:
            mvarTag = rec.ValueTag
            if mvarTag not in MVAR_ENTRIES:
                continue
            tableTag, itemName = MVAR_ENTRIES[mvarTag]
            delta = otRound(varStoreInstancer[rec.VarIdx])
            if not delta:
                continue
            setattr(varfont[tableTag], itemName,
                    getattr(varfont[tableTag], itemName) + delta)

    log.info("Mutating FeatureVariations")
    for tableTag in 'GSUB', 'GPOS':
        if not tableTag in varfont:
            continue
        table = varfont[tableTag].table
        if not getattr(table, 'FeatureVariations', None):
            continue
        variations = table.FeatureVariations
        for record in variations.FeatureVariationRecord:
            applies = True
            for condition in record.ConditionSet.ConditionTable:
                if condition.Format == 1:
                    axisIdx = condition.AxisIndex
                    axisTag = fvar.axes[axisIdx].axisTag
                    Min = condition.FilterRangeMinValue
                    Max = condition.FilterRangeMaxValue
                    v = loc[axisTag]
                    if not (Min <= v <= Max):
                        applies = False
                else:
                    applies = False
                if not applies:
                    break

            if applies:
                assert record.FeatureTableSubstitution.Version == 0x00010000
                for rec in record.FeatureTableSubstitution.SubstitutionRecord:
                    table.FeatureList.FeatureRecord[
                        rec.FeatureIndex].Feature = rec.Feature
                break
        del table.FeatureVariations

    if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
        log.info("Mutating GDEF/GPOS/GSUB tables")
        gdef = varfont['GDEF'].table
        instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)

        merger = MutatorMerger(varfont, instancer)
        merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])

        # Downgrade GDEF.
        del gdef.VarStore
        gdef.Version = 0x00010002
        if gdef.MarkGlyphSetsDef is None:
            del gdef.MarkGlyphSetsDef
            gdef.Version = 0x00010000

        if not (gdef.LigCaretList or gdef.MarkAttachClassDef
                or gdef.GlyphClassDef or gdef.AttachList or
                (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
            del varfont['GDEF']

    addidef = False
    if glyf:
        for glyph in glyf.glyphs.values():
            if hasattr(glyph, "program"):
                instructions = glyph.program.getAssembly()
                # If GETVARIATION opcode is used in bytecode of any glyph add IDEF
                addidef = any(
                    op.startswith("GETVARIATION") for op in instructions)
                if addidef:
                    break
        if overlap:
            for glyph_name in glyf.keys():
                glyph = glyf[glyph_name]
                # Set OVERLAP_COMPOUND bit for compound glyphs
                if glyph.isComposite():
                    glyph.components[0].flags |= OVERLAP_COMPOUND
                # Set OVERLAP_SIMPLE bit for simple glyphs
                elif glyph.numberOfContours > 0:
                    glyph.flags[0] |= flagOverlapSimple
    if addidef:
        log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
        asm = []
        if 'fpgm' in varfont:
            fpgm = varfont['fpgm']
            asm = fpgm.program.getAssembly()
        else:
            fpgm = newTable('fpgm')
            fpgm.program = ttProgram.Program()
            varfont['fpgm'] = fpgm
        asm.append("PUSHB[000] 145")
        asm.append("IDEF[ ]")
        args = [str(len(loc))]
        for a in fvar.axes:
            args.append(str(floatToFixed(loc[a.axisTag], 14)))
        asm.append("NPUSHW[ ] " + ' '.join(args))
        asm.append("ENDF[ ]")
        fpgm.program.fromAssembly(asm)

        # Change maxp attributes as IDEF is added
        if 'maxp' in varfont:
            maxp = varfont['maxp']
            if hasattr(maxp, "maxInstructionDefs"):
                maxp.maxInstructionDefs += 1
            else:
                setattr(maxp, "maxInstructionDefs", 1)
            if hasattr(maxp, "maxStackElements"):
                maxp.maxStackElements = max(len(loc), maxp.maxStackElements)
            else:
                setattr(maxp, "maxInstructionDefs", len(loc))

    if 'name' in varfont:
        log.info("Pruning name table")
        exclude = {a.axisNameID for a in fvar.axes}
        for i in fvar.instances:
            exclude.add(i.subfamilyNameID)
            exclude.add(i.postscriptNameID)
        if 'ltag' in varfont:
            # Drop the whole 'ltag' table if all its language tags are referenced by
            # name records to be pruned.
            # TODO: prune unused ltag tags and re-enumerate langIDs accordingly
            excludedUnicodeLangIDs = [
                n.langID for n in varfont['name'].names if n.nameID in exclude
                and n.platformID == 0 and n.langID != 0xFFFF
            ]
            if set(excludedUnicodeLangIDs) == set(
                    range(len((varfont['ltag'].tags)))):
                del varfont['ltag']
        varfont['name'].names[:] = [
            n for n in varfont['name'].names if n.nameID not in exclude
        ]

    if "wght" in location and "OS/2" in varfont:
        varfont["OS/2"].usWeightClass = otRound(
            max(1, min(location["wght"], 1000)))
    if "wdth" in location:
        wdth = location["wdth"]
        for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
            if wdth < percent:
                varfont["OS/2"].usWidthClass = widthClass
                break
        else:
            varfont["OS/2"].usWidthClass = 9
    if "slnt" in location and "post" in varfont:
        varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))

    log.info("Removing variable tables")
    for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR',
                'STAT'):
        if tag in varfont:
            del varfont[tag]

    return varfont
def main(args=None):
    """Test for interpolatability issues between fonts"""
    import argparse

    parser = argparse.ArgumentParser(
        "fontemon_blender_addon.fontTools varLib.interpolatable",
        description=main.__doc__,
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output report in JSON format",
    )
    parser.add_argument(
        "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF/UFO files"
    )

    args = parser.parse_args(args)
    glyphs = None
    # glyphs = ['uni08DB', 'uniFD76']
    # glyphs = ['uni08DE', 'uni0034']
    # glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina']

    from os.path import basename

    names = [basename(filename).rsplit(".", 1)[0] for filename in args.inputs]

    fonts = []
    for filename in args.inputs:
        if filename.endswith(".ufo"):
            from fontemon_blender_addon.fontTools.ufoLib import UFOReader

            fonts.append(UFOReader(filename))
        else:
            from fontemon_blender_addon.fontTools.ttLib import TTFont

            fonts.append(TTFont(filename))

    glyphsets = [font.getGlyphSet() for font in fonts]
    problems = test(glyphsets, glyphs=glyphs, names=names)
    if args.json:
        import json

        print(json.dumps(problems))
    else:
        for glyph, glyph_problems in problems.items():
            print(f"Glyph {glyph} was not compatible: ")
            for p in glyph_problems:
                if p["type"] == "missing":
                    print("    Glyph was missing in master %s" % p["master"])
                if p["type"] == "open_path":
                    print("    Glyph has an open path in master %s" % p["master"])
                if p["type"] == "path_count":
                    print(
                        "    Path count differs: %i in %s, %i in %s"
                        % (p["value_1"], p["master_1"], p["value_2"], p["master_2"])
                    )
                if p["type"] == "node_count":
                    print(
                        "    Node count differs in path %i: %i in %s, %i in %s"
                        % (
                            p["path"],
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "node_incompatibility":
                    print(
                        "    Node %o incompatible in path %i: %s in %s, %s in %s"
                        % (
                            p["node"],
                            p["path"],
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "contour_order":
                    print(
                        "    Contour order differs: %s in %s, %s in %s"
                        % (
                            p["value_1"],
                            p["master_1"],
                            p["value_2"],
                            p["master_2"],
                        )
                    )
                if p["type"] == "high_cost":
                    print(
                        "    Interpolation has high cost: cost of %s to %s = %i, threshold %i"
                        % (
                            p["master_1"],
                            p["master_2"],
                            p["value_1"],
                            p["value_2"],
                        )
                    )
    if problems:
        return problems