Adding a test harness and some initial tests (most fail currently)

This commit is contained in:
Christopher Kohnert 2017-06-08 11:12:35 -07:00
parent 746767a62b
commit 3b9d24a6ca
10 changed files with 355 additions and 0 deletions

View File

@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 2.8.12)
project(msdfgen)
include(CTest)
find_package(Freetype REQUIRED)
include(CheckCXXCompilerFlag)
@ -48,6 +50,11 @@ endfunction(folderize_sources)
#----------------------------------------------------------------
# Build Rules
#----------------------------------------------------------------
file(GLOB_RECURSE msdfgen_HEADERS
"core/*.h"
"lib/*.h"
@ -77,3 +84,20 @@ target_link_libraries(lib_msdfgen ${FREETYPE_LIBRARIES})
add_executable(msdfgen main.cpp)
target_compile_definitions(msdfgen PRIVATE MSDFGEN_STANDALONE)
target_link_libraries(msdfgen lib_msdfgen)
#----------------------------------------------------------------
# Tests
#----------------------------------------------------------------
if (BUILD_TESTING)
file(GLOB TEST_DRIVER "test/test_msdf.py")
file(GLOB_RECURSE TEST_SVG_SOURCES
"test/test-*.svg")
foreach(FILE ${TEST_SVG_SOURCES})
get_filename_component(NAME "${FILE}" NAME)
add_test(NAME ${NAME} COMMAND python "${TEST_DRIVER}" --svg "${FILE}" --exe "$<TARGET_FILE:msdfgen>")
endforeach()
endif()

6
test/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*-diff.png
*-render.png
*.msdf.png
*.psdf.png
*.sdf.png
montage-*.png

31
test/montage.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/sh
# Simple helper so I can compare versions and get a bird's eye of the tests.
# This isn't meant to be run as a unit test.
EXE=../Release/msdfgen
DIR=${1:-.}
echo "Running montage in $DIR"
function runtest()
{
echo "--[[ Running $1 v$2 ]]--"
rm -f test-*.$1.png
rm -f test-*.$1-diff.png
rm -f test-*.$1-render.png
python test_msdf.py --svg-dir "$DIR" --legacy $2 --mode $1 --exe "$EXE" --montage
}
function runmode()
{
runtest $1 2
runtest $1 0
compare montage-$1-[02]-diff.png -highlight-color blue montage-$1-2vs0-diff.png
open montage-$1-2vs0-diff.png
}
runmode msdf
#runmode sdf

View File

@ -0,0 +1,12 @@
<svg viewBox="0 0 512 512" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>Example fillrule-evenodd - demonstrates fill-rule:evenodd</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path d="M 130,35 L 203,261 11,121 249,121 57,261 z
M 375,61 A 107,107 0 0,1 375,275 A 107,107 0 0,1 375,61 z
M 375,119 A 49,49 0 0,1 375,217 A 49,49 0 0,1 375,119 z
M 250,281 A 107,107 0 0,1 250,495 A 107,107 0 0,1 250,281 z
M 250,339 A 49,49 0 0,0 250,437 A 49,49 0 0,0 250,339 z"
fill="white"
fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<desc>Another fillrule-nonzero test, but this time using it as a default</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path d="
M300 100l50,25v-50z
M100 300l-50,25v-50z
"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1,12 @@
<svg viewBox="0 0 512 512" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>Example fillrule-nonzero - demonstrates fill-rule:nonzero</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path d="M 130,35 L 203,261 11,121 249,121 57,261 z
M 375,61 A 107,107 0 0,1 375,275 A 107,107 0 0,1 375,61 z
M 375,119 A 49,49 0 0,1 375,217 A 49,49 0 0,1 375,119 z
M 250,281 A 107,107 0 0,1 250,495 A 107,107 0 0,1 250,281 z
M 250,339 A 49,49 0 0,0 250,437 A 49,49 0 0,0 250,339 z"
fill="white"
fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<desc>Illustrates a path that implicitly closes contours without a 'z'</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path fill="white" d="
M278.535 276.134
c12.314 21.92 26.598 38.42 42.854 49.503 16.5 10.837 35.094 16.255 55.782 16.255 24.874 0 45.193-8.25 60.955-24.752 15.762-16.747 23.643-38.05 23.644-63.91 0-24.875-7.265-45.563-21.795-62.064-14.532-16.5-32.757-24.752-54.676-24.752-19.95 0-38.052 8.25-54.306 24.752-16.01 16.255-33.495 44.577-52.46 84.968z
m-45.07-39.53
c-12.067-21.672-26.352-37.926-42.853-48.763-16.254-10.835-34.848-16.254-55.782-16.254-24.875 0-45.193 8.25-60.955 24.752
C58.11 212.593 50.23 233.65 50.23 259.51
c0 24.875 7.265 45.562 21.795 62.063 14.53 16.5 32.756 24.752 54.676 24.752 19.95 0 37.928-8.127 53.937-24.382 16.254-16.255 33.864-44.7 52.828-85.338z
m26.23 67.605c-17.487 33.495-35.835 58-55.045 73.516C185.686 393.242 164.505 401 141.108 401c-33.248 0-61.448-13.792-84.598-41.376-22.905-27.584-34.357-61.694-34.357-102.33 0-43.1 10.22-77.95 30.662-104.55 20.688-26.597 47.533-39.896 80.535-39.897 23.397 0 44.33 7.635 62.803 22.905 18.47 15.023 36.942 39.898 55.414 74.624 16.747-33.987 34.85-58.985 54.306-74.994C325.33 119.128 347.003 111 370.893 111c32.754 0 60.707 13.915 83.86 41.745 23.395 27.83 35.094 62.187 35.094 103.07 0 42.854-10.344 77.58-31.032 104.18-20.442 26.35-47.164 39.527-80.165 39.527-23.398 0-44.21-7.142-62.433-21.426-17.98-14.53-36.82-39.16-56.523-73.886"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,62 @@
<svg viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<desc>Test that validates input by removing degenerate edges and numerical robustness with regard to matching endpoints</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path d="
M242.563,27.656C232.501,27.689 222.437,27.861 212.375,28.156L218.469,93.75L157.374,31.187C126.118,33.757 94.88,37.842 63.624,43.719L97.874,72.124L49.811,71.561C84.969,171.729 56.747,254.136 49.499,363.561L145.249,469.405C155.249,468.079 165.105,466.982 174.812,466.061L196.342,423.905L214.874,452.749L256.374,419.155L257.187,462.249C340.13,462.007 414.921,471.347 492.874,470.062C464.724,397.052 461.744,326.5 460.999,260.905L427.749,241.78L460.437,207.906C460.007,186.573 459.095,165.9 456.812,145.969L399.374,146.687L449.624,105.03C445.781,89.553 440.572,74.63 433.499,60.312L403.969,36.406C377.12,33.667 350.214,31.369 323.265,29.908C296.394,28.451 269.471,28.053 242.563,27.656L242.563,27.656Z
M332.97,138.875L355.406,107.437L403.374,188.937L387.249,198.407L353.874,141.719L311.436,201.124L296.249,190.249L320.249,156.655L298.279,105.249L273.719,145.312L285.686,167.937L269.156,176.687L250.906,142.187L226.061,200.374L208.874,193.03L249.094,98.814L263.562,126.156L301.281,64.626L332.97,138.875Z
M106.439,120.876
C115.724,122.286 125.63,126.156 125.63,126.156
L119.412,143.781
C111.103,140.092 102.474,139.126 102.474,139.126
L106.439,120.876Z
M144.313,134.25
C150.717,137.963 155.804,141.976 161.344,147.063
L147.814,159.938
C142.651,155.095 141.192,154.05 135,150.47
L144.313,134.25Z
M173.004,166.691
C173.797,169.713 174.909,174.55 175.126,177.683
C175.34,180.756 174.922,186.047 174.593,189.094
L156.063,186.688
C156.711,182.075 156.706,176.593 155.125,172.154
L173.004,166.691Z
M151.75,202.345L169.03,209.405
C165.563,217.561 164.621,219.562 160.439,227.219
L144.096,218.155
C147.696,211.566 148.546,209.66 151.754,202.341
L151.75,202.345
L151.75,202.345Z
M135.28,236.438
L152.97,242.468
C150.756,249.362 150.418,251.226 149.407,258.658
L130.813,256.658
C132.13,247.384 132.559,245.058 135.278,236.437
L135.28,236.438Z
M343.125,253.813C346.272,254.161 351.299,254.904 354.324,255.905C357.286,256.886 362.082,259.407 364.781,260.935L355.031,276.873C350.682,274.371 346.468,272.941 341.469,272.433L343.124,253.808L343.125,253.813Z
M321.826,254.741L325.751,273.625C319.97,274.644 314.639,276.275 309.312,278.75L302.5,261.344C308.798,258.515 321.826,254.741 321.826,254.741Z
M285.225,270.508L294.97,286.312C287.672,291.41 285.806,292.615 278.188,296.936L269.251,280.529C275.96,276.74 285.225,270.508 285.225,270.508Z
M150.47,273.876C151.978,278.518 154.314,282.263 157.374,286.031L143.564,298.626C141.633,296.254 138.148,291.744 136.643,289.064C135.059,286.244 133.364,281.592 132.372,278.532L150.466,273.877L150.47,273.876L150.47,273.876Z
M380.78,277.188C384.895,283.171 387.564,289.3 389.5,296.283L371.687,301.909C369.46,295.07 368.656,293.416 364.942,287.096L380.785,277.19L380.78,277.188L380.78,277.188Z
M253.833,287.766L260.345,305.438C252.187,308.821 250.108,309.559 241.685,311.904L236.717,293.904C244.248,292.022 253.833,287.766 253.833,287.766Z
M169.625,294.25C176.353,296.829 178.122,297.284 185.312,298.532L182.53,317.032C173.218,315.416 170.922,314.743 162.001,311.315L169.626,294.251L169.625,294.25L169.625,294.25Z
M219.563,297.78L222.406,316.25C215.806,317.641 209.334,318.219 202.593,318.155L202.187,299.469C209.869,299.285 211.818,299.061 219.561,297.782L219.563,297.78L219.563,297.78Z
M394.063,315.78C395.451,325.424 395.448,325.4 396.251,335.097L377.626,336.597C376.888,327.655 376.894,327.684 375.595,318.784L394.065,315.784L394.063,315.78L394.063,315.78Z
M326.5,346.595C345.393,350.055 371.413,362.782 395.375,380.47C402.835,370.635 409.8,360.07 416.688,349.375L446.75,362.345C442.064,374.555 431.76,387.863 418.187,399.531C435.204,415.697 449.057,434.046 455.187,452.471C441.381,434.363 424.117,420.741 404.907,409.657C381.52,425.503 352.225,435.587 325.281,430.095C348.196,425.712 365.925,413.535 381.095,397.501C364.633,389.997 347.297,383.624 329.845,377.407L326.501,346.595L326.5,346.595L326.5,346.595Z
" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

8
test/test-simple.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<desc>Test a simple triangle for accuracy</desc>
<path d="M0 0h512v512H0z" fill="black"/>
<path d="
M256,156 l100,200h-200z
"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

178
test/test_msdf.py Normal file
View File

@ -0,0 +1,178 @@
import argparse
import os
import re
import sys
from subprocess import Popen, PIPE, call
class Runner:
pass_count = 0
fail_count = 0
stop_on_fail = False
results = []
def __init__(self):
pass
def add_fail(self, path, msg):
print("FAIL \"%s\" %s" % (path, msg))
self.fail_count += 1
if self.stop_on_fail:
sys.exit(1)
return False
def add_pass(self, path, msg):
self.pass_count += 1
print("PASS \"%s\" %s" % (path, msg))
return True
def add_output(self, out_path, render_path, diff_path):
self.results.append({
'out': out_path,
'render': render_path,
'diff': diff_path})
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:
print "Cannot find ImageMagick <http://www.imagemagick.org> 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 / 512.0), '-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
p = Popen(["compare -metric RMSE \"%s\" \"%s\" \"%s\"" % (path, render_path, diff_path)],
stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True)
output, err = p.communicate("")
if p.returncode == 1:
runner.add_output(sdf_path, render_path, diff_path)
# Ran successfully, but they are different. Let's parse the output and check the actual error metric.
# RMSE output = "<error> (<normalized error>)"
match = re.match(r'\S+ \(([^)]+)\)', err)
if match:
e = float(match.group(1))
if e > args.fail_threshold:
return runner.add_fail(path, "Error = %s" % e)
else:
return runner.add_pass(path, "(Acceptable) Error = %s" % e)
else:
runner.add_fail(path, "(Unknown) Error metric = %s" % err)
elif p.returncode != 0:
return runner.add_fail(path, "Error comparing to %s [%d]: %s" % (render_path, p.returncode, err))
runner.add_output(sdf_path, render_path, diff_path)
return runner.add_pass(path, output)
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")
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="Threshold for normalized RMSE that will flag a test as failure (Default: 0.07)",
default=0.07,
type=float)
parser.add_argument("--legacy",
help="Use legacy <Version> mode algorithm",
default='0')
parser.add_argument("--montage",
help="Generate montage image(s) for results",
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:
test_svg(args.svg, args, runner)
if args.montage:
# We keep the diff montage at actual size (we want to see details)
call(["montage -geometry +1+1 %s montage-%s-%s-diff.png" %
(" ".join(runner.get_outputs('diff')), args.mode, args.legacy)], shell=True)
# 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()