import argparse import os import re import sys import codecs import tempfile from subprocess import Popen, PIPE, call def println(msg): sys.stdout.write(msg) sys.stdout.write('\n') sys.stdout.flush() return msg class Runner: pass_count = 0 fail_count = 0 stop_on_fail = False results = [] def __init__(self): pass def add_pass(self, input_path, msg, out_path=None, render_path=None, diff_path=None): return self.add_result(True, input_path, msg, out_path, render_path, diff_path) def add_fail(self, input_path, msg, out_path=None, render_path=None, diff_path=None): return self.add_result(False, input_path, msg, out_path, render_path, diff_path) def add_result(self, passed, input_path, msg, out_path=None, render_path=None, diff_path=None): if passed: self.pass_count += 1 println("PASS \"%s\" %s" % (input_path, msg)) else: self.fail_count += 1 println("FAIL \"%s\" %s" % (input_path, msg)) if self.stop_on_fail: sys.exit(1) self.results.append({ 'pass': passed, 'in': input_path, 'out': out_path, 'render': render_path, 'diff': diff_path}) return passed def get_outputs(self, key): ret = [] for e in self.results: if key in e: ret.append(e[key]) return ret def check_imagemagick(): p = Popen(["compare -version"], stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True) output, err = p.communicate("") if p.returncode != 0: println("Cannot find ImageMagick on your PATH. It is required to run tests.") sys.exit(1) def test_svg(path, args, runner): (root, ext) = os.path.splitext(path) mode = args.mode sz = args.sdf_size rsz = str(args.render_size) sdf_path = "%s.%s.png" % (root, mode) render_path = "%s.%s-render.png" % (root, mode) diff_path = "%s.%s-diff.png" % (root, mode) try: result = call([args.exe, mode, '-svg', path, '-o', sdf_path, # '-angle', '4', # '-pxrange', '2', # '-range', '8', # '-tolerance', '0.01', # '-legacy', str(args.legacy), '-scale', str(sz / float(args.render_size)), '-size', str(sz), str(sz), # '-exportshape', 'shape.txt', '-testrender', render_path, rsz, rsz ], shell=False) if result != 0: return runner.add_fail(path, "Unable to render %s" % mode) except OSError as er: return runner.add_fail(path, "Error running %s: %s" % (args.exe, er)) # Use imagemagick to rasterize the reference image and compare it. # We use a very fuzzy comparison to try and account for differences in aliasing introduced around edges of # an image, which comes from both the down/up rezzing introduced by going through the SDF process, but also # minor differences with IM's SVG rasterizer. What we're trying to catch is full on pixel mismatches that # indicate a shape was mis-handled (curve shooting off to the edge, fill rule violation, etc). total_pixels = args.render_size * args.render_size p = Popen(["compare -fuzz %f%% -metric AE \"%s\" \"%s\" \"%s\"" % (args.fuzz, path, render_path, diff_path)], stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True) output, err = p.communicate("") passed = False if p.returncode == 1: # Ran successfully, but they are different. Let's parse the output and check the actual error metric. match = re.match(r'^(\d+)$', err) passed = False if match: try: pixels = int(match.group(1)) e = float(pixels) / total_pixels * 100.0 if e > args.fail_threshold: msg = "Error = %.3f %% (%d)" % (e, pixels) else: passed = True msg = "OK = %.3f %% (%d)" % (e, pixels) except ValueError: msg = "(Unknown) Error metric = %s" % err else: msg = "(Unknown) Error metric = %s" % err elif p.returncode != 0: msg = "Error comparing to %s [%d]: %s" % (render_path, p.returncode, err) else: passed = True msg = "Identical" return runner.add_result(passed, path, msg, sdf_path, render_path, diff_path) def main(): parser = argparse.ArgumentParser(description="Test MSDFGEN outputs") parser.add_argument("--svg-dir", help="Directory to scan for SVG files.") parser.add_argument("--svg", help="SVG file to test", action='append') parser.add_argument("--mode", help="Algorithm: [sdf, psdf, msdf] (default=msdf)", default="msdf") parser.add_argument("--sdf_size", help="Size for rendered (M) image. Default = 128", default=128, type=int) parser.add_argument("--render_size", help="Size for the rendered test image. Default = 512", default=512, type=int) parser.add_argument("--exe", help="Path to MSDFGEN executable", default="msdfgen") parser.add_argument("--fail-threshold", help="Percentage of pixels that are allowed to be different without flagging a failure. (Default: 0.06%)", default=0.06, # Found experimentally (~150.0 / (512 * 512)) type=float) parser.add_argument("--fuzz", help="Fuzz percentage to use when comparing", default=99.5, type=float) parser.add_argument("--legacy", help="Use legacy mode algorithm", default='0') parser.add_argument("--montage", help="Generate montage image of failed results (diffs)", default=False, action='store_true') parser.add_argument("--stop-on-fail", help="Stop testing after the first fail", default=False, action='store_true') args = parser.parse_args() if not args.svg_dir and not args.svg: parser.print_help() sys.exit(1) check_imagemagick() runner = Runner() runner.stop_on_fail = args.stop_on_fail if args.svg_dir: for root, dirs, files in os.walk(args.svg_dir): for name in files: if not name.endswith(".svg"): continue path = os.path.join(root, name) test_svg(path, args, runner) if args.svg: for file in args.svg: if file.startswith('@'): # Use ImageMagick style @filename.txt for loading filenames to test. for line in codecs.open(file[1:], 'r', 'utf-8'): test_svg(line.rstrip('\n'), args, runner) else: test_svg(file, args, runner) if args.montage: # We keep the diff montage at actual size (we want to see details) with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write("\n".join(runner.get_outputs('diff'))) temp_file.close() montage_name = "montage-%s-%s-diff.png" % (args.mode, args.legacy) call(["montage -geometry +1+1 @%s %s" % (temp_file.name, montage_name)], shell=True) os.unlink(temp_file.name) # print("Wrote: %s" % montage_name) # The others, we can let IM do some resizing because it's really just for quick glances. # call(["montage %s montage-%s-%s-out.png" % # (" ".join(runner.get_outputs('out')), args.mode, args.legacy)], shell=True) # call(["montage %s montage-%s-%s-render.png" % # (" ".join(runner.get_outputs('render')), args.mode, args.legacy)], shell=True) if runner.fail_count > 0: sys.exit(1) else: sys.exit(0) if __name__ == '__main__': main()