[PathKit] Adding test infrastructure to support Gold output

To get the gold images out of the browser tests, this adds
testReporter.js and pathkit_aggregator.go.  testReporter bundles
up the output as a base64 encoded PNG and sends it over the local
network to pathkit_aggregator.  pathkit_aggregator will keep
a list of test results reported in this way and write the PNGs
to /OUT of the container (which is the swarming output directory).
Finally, after all the tests are run, the helper script "test_pathkit.sh"
makes a POST request that creates the JSON file that gold expects
(following the schema https://github.com/google/skia-buildbot/blob/master/golden/docs/INGESTION.md)

pathkit_aggregator takes many command line arguments which control
the keys that Gold needs in order to ingest and handle the data.
Of note, this creates a new set (i.e. source_type) of gold images
called "pathkit", which will distinguish it from "gm", "image", etc.

There will be at least 2 sub-sets of "pathkit" images, "canvas" and "svg",
(representing the 2 output types of PathKit).  This CL doesn't
quite handle SVG yet, as it needs a way to convert SVG to PNG in the
browser and will be addressed in a follow up CL.

A "standard" gm is sized at 600x600. This was arbitrarily picked.

Note that the functions in testReporter.js return Promises based
on the fetch requests to post the data. This eliminates the race
condition between the /report_gold_data and /dump_json since
running the karma tests won't return until all reports are done.

Other changes of note:
 - Adds go to karma-chrome-tests container.
 - renames recipe_modules/build/wasm.py -> pathkit.py to be consistent with
the name of test_pathkit.py and make for easier grepping.
 - Increases the JS test timeout to 10s (up from 5) to hopefully avoid
the flakes seen in the Debug Test.

Bug: skia:8216
Change-Id: Ic2cad54f3d19cc16601cf2e9a87798db1e6887a2
Reviewed-on: https://skia-review.googlesource.com/147042
Reviewed-by: Stephan Altmueller <stephana@google.com>
diff --git a/experimental/pathkit/Makefile b/experimental/pathkit/Makefile
index a8fcbb6..dee9c1d 100644
--- a/experimental/pathkit/Makefile
+++ b/experimental/pathkit/Makefile
@@ -52,12 +52,12 @@
 # test-docker-continuous is better, although if you make changes to the C++/WASM code,
 # you will need to manually call make npm-test to re-build.
 test-docker: npm-test
-	docker run --shm-size=2gb -v $$SKIA_ROOT:/SRC gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v1 \
+	docker run --shm-size=2gb -v $$SKIA_ROOT:/SRC gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v3 \
 karma start /SRC/experimental/pathkit/karma-docker.conf.js --single-run
 
 test-docker-continuous:
 	echo "Assuming make npm-test has also been run by a user (if needed)"
-	docker run --shm-size=2gb -v $$SKIA_ROOT:/SRC gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v1 \
+	docker run --shm-size=2gb -v $$SKIA_ROOT:/SRC gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v3 \
 karma start /SRC/experimental/pathkit/karma-docker.conf.js --no-single-run
 
 npm-test:
diff --git a/experimental/pathkit/docker/README.md b/experimental/pathkit/docker/README.md
deleted file mode 100644
index e22a278..0000000
--- a/experimental/pathkit/docker/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-Docker
-======
-
-Docker files to ease working with PathKit and WASM.
-
-emsdk-base
-----------
-
-This image has an Emscripten SDK environment that can be used for
-compiling projects (e.g. Skia's PathKit) to WASM/asm.js.
-
-This image is standalone and does not have any extra dependencies that make
-it Skia-exclusive.
-
-It gets manually pushed anytime there's an update to the Dockerfile or relevant
-installed libraries.
-
-    docker build -t emsdk-base ./docker/emsdk-base/
-    EMSDK_VERSION="1.38.6_jre"
-    docker tag emsdk-base gcr.io/skia-public/emsdk-release:$EMSDK_VERSION
-    docker push gcr.io/skia-public/emsdk-release:$EMSDK_VERSION
-
-For testing the image locally, the following flow can be helpful:
-
-    docker build -t emsdk-base ./docker/emsdk-base/
-    # Run bash in it to poke around and make sure things are properly installed
-    docker run -it emsdk-release /bin/bash
-    # Compile PathKit with the local image
-    docker run -v $SKIA_ROOT:/SRC -v $SKIA_ROOT/out/dockerpathkit:/OUT emsdk-base /SRC/experimental/pathkit/docker/build_pathkit.sh
-
-karma-chrome-tests
-------------------
-
-This image has Google Chrome and karma/jasmine installed on it, which can
-be used to run JS tests.
-
-This image is standalone and does not have any extra dependencies that make
-it Skia-exclusive.
-
-It gets manually pushed anytime there's an update to the Dockerfile or relevant
-installed libraries.
-
-    docker build -t karma-chrome-tests ./docker/karma-chrome-tests/
-    # check the version of chrome with the following:
-    docker run karma-chrome-tests /usr/bin/google-chrome-stable --version
-    CHROME_VERSION="68.0.3440.106_v1"  # use v1, v2, etc for any re-spins of the container.
-    docker tag karma-chrome-tests gcr.io/skia-public/karma-chrome-tests:$CHROME_VERSION
-    docker push gcr.io/skia-public/karma-chrome-tests:$CHROME_VERSION
-
-Of note, some versions (generally before Chrome 60) run out of space on /dev/shm when
-using the default Docker settings.  To be safe, it is recommended to run the container
-with the flag --shm-size=2gb.
-
-For testing the image locally, the following can be helpful:
-
-    docker build -t karma-chrome-tests ./docker/karma-chrome-tests/
-    # Run bash in it to poke around and make sure things are properly installed
-    docker run -it --shm-size=2gb karma-chrome-tests /bin/bash
-    # Run the tests with the local source repo
-    docker run --shm-size=2gb -v $SKIA_ROOT:/SRC karma-chrome-tests karma start /SRC/experimental/pathkit/karma-docker.conf.js --single-run
-
diff --git a/experimental/pathkit/karma-docker.conf.js b/experimental/pathkit/karma-docker.conf.js
index 4695fd4..8bb1f17 100644
--- a/experimental/pathkit/karma-docker.conf.js
+++ b/experimental/pathkit/karma-docker.conf.js
@@ -10,6 +10,7 @@
     files: [
       { pattern: 'npm-wasm/bin/test/pathkit.wasm', included:false, served:true},
       { pattern: 'tests/*.json', included:false, served:true},
+      'tests/testReporter.js',
       'npm-wasm/bin/test/pathkit.js',
       'tests/*.spec.js'
     ],
diff --git a/experimental/pathkit/karma.conf.js b/experimental/pathkit/karma.conf.js
index c3104b4..e9cbb24 100644
--- a/experimental/pathkit/karma.conf.js
+++ b/experimental/pathkit/karma.conf.js
@@ -10,6 +10,7 @@
     files: [
       { pattern: 'npm-wasm/bin/test/pathkit.wasm', included:false, served:true},
       { pattern: 'tests/*.json', included:false, served:true},
+      'tests/testReporter.js',
       'npm-wasm/bin/test/pathkit.js',
       'tests/*.spec.js'
     ],
diff --git a/experimental/pathkit/tests/path2d.spec.js b/experimental/pathkit/tests/path2d.spec.js
index 303bf87..bbd76f3 100644
--- a/experimental/pathkit/tests/path2d.spec.js
+++ b/experimental/pathkit/tests/path2d.spec.js
@@ -1,13 +1,6 @@
 
 
 describe('PathKit\'s Path2D API', function() {
-    const container = document.createElement('div');
-    document.body.appendChild(container);
-
-    afterEach(function() {
-        container.innerHTML = '';
-    });
-
     // Note, don't try to print the PathKit object - it can cause Karma/Jasmine to lock up.
     var PathKit = null;
     const LoadPathKit = new Promise(function(resolve, reject) {
@@ -67,30 +60,35 @@
             path.addPath(secondPath, 1, 0, 0, 1, 0, 20.5);
 
             let canvas = document.createElement('canvas');
-            container.appendChild(canvas);
             let canvasCtx = canvas.getContext('2d');
+            // Set canvas size and make it a bit bigger to zoom in on the lines
+            standardizedCanvasSize(canvasCtx);
+            canvasCtx.scale(3.0, 3.0);
             canvasCtx.fillStyle = 'blue';
             canvasCtx.stroke(path.toPath2D());
 
             path.delete();
             secondPath.delete();
 
-            done();
+            reportCanvas(canvas, 'path2D_api_example').then(() => {
+                done();
+            }).catch(reportError(done));
         });
     });
 
     it('approximates arcs (conics) with quads', function(done) {
         LoadPathKit.then(() => {
             let path = PathKit.NewPath();
-            path.moveTo(20, 120);
-            path.arc(20, 120, 18, 0, 1.75 * Math.PI);
-            path.lineTo(20, 120);
+            path.moveTo(50, 120);
+            path.arc(50, 120, 45, 0, 1.75 * Math.PI);
+            path.lineTo(50, 120);
 
             let canvas = document.createElement('canvas');
-            container.appendChild(canvas);
             let canvasCtx = canvas.getContext('2d');
-
-            spyOn(canvasCtx, 'quadraticCurveTo');
+            standardizedCanvasSize(canvasCtx);
+            // The and.callThrough is important to make it actually
+            // draw the quadratics
+            spyOn(canvasCtx, 'quadraticCurveTo').and.callThrough();
 
             canvasCtx.beginPath();
             path.toCanvas(canvasCtx);
@@ -100,7 +98,9 @@
             // to the approximation algorithms).
             expect(canvasCtx.quadraticCurveTo).toHaveBeenCalled();
             path.delete();
-            done();
+            reportCanvas(canvas, 'conics_quads_approx').then(() => {
+                done();
+            }).catch(reportError(done));
         });
     });
 
diff --git a/experimental/pathkit/tests/pathops.spec.js b/experimental/pathkit/tests/pathops.spec.js
index 3a9228e..a8f0376 100644
--- a/experimental/pathkit/tests/pathops.spec.js
+++ b/experimental/pathkit/tests/pathops.spec.js
@@ -1,3 +1,4 @@
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
 
 var dumpErrors = false;
 var container;
diff --git a/experimental/pathkit/tests/svg.spec.js b/experimental/pathkit/tests/svg.spec.js
index f4c3007..5d964cc 100644
--- a/experimental/pathkit/tests/svg.spec.js
+++ b/experimental/pathkit/tests/svg.spec.js
@@ -95,15 +95,19 @@
     it('approximates arcs (conics) with quads', function(done) {
         LoadPathKit.then(() => {
             let path = PathKit.NewPath();
-            path.moveTo(20, 120);
-            path.arc(20, 120, 18, 0, 1.75 * Math.PI);
-            path.lineTo(20, 120);
+            path.moveTo(50, 120);
+            path.arc(50, 120, 45, 0, 1.75 * Math.PI);
+            path.lineTo(50, 120);
             let svgStr = path.toSVGString();
             // Q stands for quad.  No need to check the whole path, as that's more
             // what the gold correctness tests are for (can account for changes we make
             // to the approximation algorithms).
             expect(svgStr).toContain('Q');
             path.delete();
+
+             reportSVGString(svgStr, 'conics_quads_approx').then(() => {
+                done();
+            }).catch(reportError(done));
             done();
         });
     });
diff --git a/experimental/pathkit/tests/testReporter.js b/experimental/pathkit/tests/testReporter.js
new file mode 100644
index 0000000..81124d4
--- /dev/null
+++ b/experimental/pathkit/tests/testReporter.js
@@ -0,0 +1,89 @@
+const REPORT_URL = 'http://localhost:8081/report_gold_data'
+// Set this to enforce that the gold server must be up.
+// Typically used for debugging.
+const fail_on_no_gold = false;
+
+function reportCanvas(canvas, testname) {
+    let b64 = canvas.toDataURL('image/png');
+    return _report(b64, 'canvas', testname);
+}
+
+function reportSVG(svg, testname) {
+    // This converts an SVG to a base64 encoded PNG. It basically creates an
+    // <img> element that takes the inlined SVG and draws it on a canvas.
+    // The trick is we have to wait until the image is loaded, thus the Promise
+    // wrapping below.
+    let svgStr = svg.outerHTML;
+    let tempImg = document.createElement('img');
+
+    let tempCanvas = document.createElement('canvas');
+    let canvasCtx = tempCanvas.getContext('2d');
+    setCanvasSize(canvasCtx, svg.getAttribute('width'), svg.getAttribute('height'));
+
+    return new Promise(function(resolve, reject) {
+        tempImg.onload = () => {
+            canvasCtx.drawImage(tempImg, 0, 0);
+            let b64 = tempCanvas.toDataURL('image/png');
+            _report(b64, 'svg', testname).then(() => {
+                resolve();
+            });
+        };
+        tempImg.setAttribute('src', 'data:image/svg+xml;,' + svgStr);
+    });
+}
+
+// For tests that just do a simple path and return it as a string, wrap it in
+// a proper svg and send it off.  Supports fill (nofill means just stroke it).
+// This uses the "standard" size of 600x600.
+function reportSVGString(svgstr, testname, fillRule='nofill') {
+    let newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+    newPath.setAttribute('stroke', 'black');
+    if (fillRule !== 'nofill') {
+        newPath.setAttribute('fill', 'orange');
+        newPath.setAttribute('fill-rule', fillRule);
+    } else {
+        newPath.setAttribute('fill', 'rgba(255,255,255,0.0)');
+    }
+    newPath.setAttribute('d', svgstr);
+    let newSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+    newSVG.appendChild(newPath);
+    // helps with the conversion to PNG.
+    newSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
+    newSVG.setAttribute('width', 600);
+    newSVG.setAttribute('height', 600);
+    return reportSVG(newSVG, testname);
+}
+
+function _report(data, outputType, testname) {
+    return fetch(REPORT_URL, {
+        method: 'POST',
+        mode: 'no-cors',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+            'output_type': outputType,
+            'data': data,
+            'test_name': testname,
+        })
+    }).then(() => console.log(`Successfully reported ${testname} to gold aggregator`));
+}
+
+function reportError(done) {
+    return (e) => {
+        console.log("Error with fetching. Likely could not connect to aggegator server", e.message);
+        if (fail_on_no_gold) {
+            expect(e).toBeUndefined();
+        }
+        done();
+    };
+}
+
+function setCanvasSize(ctx, width, height) {
+    ctx.canvas.width = width;
+    ctx.canvas.height = height;
+}
+
+function standardizedCanvasSize(ctx) {
+    setCanvasSize(ctx, 600, 600);
+}
\ No newline at end of file
diff --git a/infra/bots/cfg.json b/infra/bots/cfg.json
index 46e3ea8..c299d7d 100644
--- a/infra/bots/cfg.json
+++ b/infra/bots/cfg.json
@@ -11,7 +11,6 @@
     "Coverage",
     "MSAN",
     "OpenCL",
-    "PathKit",
     "SKQP",
     "TSAN",
     "UBSAN",
diff --git a/infra/bots/recipe_modules/build/api.py b/infra/bots/recipe_modules/build/api.py
index 2b30870..61231b0 100644
--- a/infra/bots/recipe_modules/build/api.py
+++ b/infra/bots/recipe_modules/build/api.py
@@ -13,8 +13,8 @@
 from . import chromecast
 from . import default
 from . import flutter
+from . import pathkit
 from . import util
-from . import wasm
 
 
 class BuildApi(recipe_api.RecipeApi):
@@ -33,8 +33,8 @@
       self.compile_fn = flutter.compile_fn
       self.copy_fn = flutter.copy_extra_build_products
     elif 'EMCC' in b:
-      self.compile_fn = wasm.compile_fn
-      self.copy_fn = wasm.copy_extra_build_products
+      self.compile_fn = pathkit.compile_fn
+      self.copy_fn = pathkit.copy_extra_build_products
     else:
       self.compile_fn = default.compile_fn
       self.copy_fn = default.copy_extra_build_products
diff --git a/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-PathKit.json b/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-PathKit.json
index ba342a7..5519328 100644
--- a/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-PathKit.json
+++ b/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Debug-PathKit.json
@@ -24,7 +24,7 @@
       "-v",
       "[START_DIR]/cache/docker/wasm:/OUT",
       "gcr.io/skia-public/emsdk-release:1.38.6_jre",
-      "/SRC/skia/experimental/pathkit/docker/build_pathkit.sh",
+      "/SRC/skia/infra/pathkit/docker/build_pathkit.sh",
       "debug"
     ],
     "env": {
diff --git a/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-PathKit.json b/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-PathKit.json
index 81625d5..88bd5bc 100644
--- a/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-PathKit.json
+++ b/infra/bots/recipe_modules/build/examples/full.expected/Build-Debian9-EMCC-wasm-Release-PathKit.json
@@ -24,7 +24,7 @@
       "-v",
       "[START_DIR]/cache/docker/wasm:/OUT",
       "gcr.io/skia-public/emsdk-release:1.38.6_jre",
-      "/SRC/skia/experimental/pathkit/docker/build_pathkit.sh"
+      "/SRC/skia/infra/pathkit/docker/build_pathkit.sh"
     ],
     "env": {
       "CHROME_HEADLESS": "1",
diff --git a/infra/bots/recipe_modules/build/wasm.py b/infra/bots/recipe_modules/build/pathkit.py
similarity index 95%
rename from infra/bots/recipe_modules/build/wasm.py
rename to infra/bots/recipe_modules/build/pathkit.py
index 41272b7..1287f75 100644
--- a/infra/bots/recipe_modules/build/wasm.py
+++ b/infra/bots/recipe_modules/build/pathkit.py
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 DOCKER_IMAGE = 'gcr.io/skia-public/emsdk-release:1.38.6_jre'
-INNER_BUILD_SCRIPT = '/SRC/skia/experimental/pathkit/docker/build_pathkit.sh'
+INNER_BUILD_SCRIPT = '/SRC/skia/infra/pathkit/docker/build_pathkit.sh'
 
 BUILD_PRODUCTS_ISOLATE_WHITELIST_WASM = [
   'pathkit.*'
@@ -15,7 +15,7 @@
   configuration = api.vars.builder_cfg.get('configuration', '')
 
   # We want to make sure the directories exist and were created by chrome-bot,
-  # because if that isn' the case, docker will make them and they will be
+  # because if that isn't the case, docker will make them and they will be
   # owned by root, which causes mysterious failures. To mitigate this risk
   # further, we don't use the same out_dir as everyone else (thus the _ignore)
   # param. Instead, we use a "wasm" subdirectory in the "docker" named_cache.
diff --git a/infra/bots/recipes/test_pathkit.expected/pathkit_test.json b/infra/bots/recipes/test_pathkit.expected/pathkit_test.json
index 632106a..3e16155 100644
--- a/infra/bots/recipes/test_pathkit.expected/pathkit_test.json
+++ b/infra/bots/recipes/test_pathkit.expected/pathkit_test.json
@@ -112,13 +112,14 @@
     "cmd": [
       "python",
       "-u",
-      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nhelper_js = sys.argv[2]\nwasm = sys.argv[3]\n\n# Clean out old binaries (if any)\ntry:\n  shutil.rmtree(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.ENOENT:\n    raise\n\n# Make folder\ntry:\n  os.makedirs(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.EEXIST:\n    raise\n\ndest = os.path.join(copy_dest, 'pathkit.js')\nshutil.copyfile(helper_js, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\ndest = os.path.join(copy_dest, 'pathkit.wasm')\nshutil.copyfile(wasm, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nhelper_js = sys.argv[2]\nwasm = sys.argv[3]\nout_dir = sys.argv[4]\n\n# Clean out old binaries (if any)\ntry:\n  shutil.rmtree(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.ENOENT:\n    raise\n\n# Make folder\ntry:\n  os.makedirs(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.EEXIST:\n    raise\n\n# Copy binaries (pathkit.js and pathkit.wasm) to where the karma tests\n# expect them ($SKIA_ROOT/experimental/pathkit/npm-wasm/test/)\ndest = os.path.join(copy_dest, 'pathkit.js')\nshutil.copyfile(helper_js, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\ndest = os.path.join(copy_dest, 'pathkit.wasm')\nshutil.copyfile(wasm, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder\nos.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.\n",
       "[START_DIR]/cache/work/skia/experimental/pathkit/npm-wasm/bin/test",
       "[START_DIR]/build/pathkit.js",
-      "[START_DIR]/build/pathkit.wasm"
+      "[START_DIR]/build/pathkit.wasm",
+      "[START_DIR]/[SWARM_OUT_DIR]"
     ],
     "infra_step": true,
-    "name": "copy built wasm to location docker can see",
+    "name": "Set up for docker",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@python.inline@import errno@@@",
       "@@@STEP_LOG_LINE@python.inline@import os@@@",
@@ -128,6 +129,7 @@
       "@@@STEP_LOG_LINE@python.inline@copy_dest = sys.argv[1]@@@",
       "@@@STEP_LOG_LINE@python.inline@helper_js = sys.argv[2]@@@",
       "@@@STEP_LOG_LINE@python.inline@wasm = sys.argv[3]@@@",
+      "@@@STEP_LOG_LINE@python.inline@out_dir = sys.argv[4]@@@",
       "@@@STEP_LOG_LINE@python.inline@@@@",
       "@@@STEP_LOG_LINE@python.inline@# Clean out old binaries (if any)@@@",
       "@@@STEP_LOG_LINE@python.inline@try:@@@",
@@ -143,6 +145,8 @@
       "@@@STEP_LOG_LINE@python.inline@  if e.errno != errno.EEXIST:@@@",
       "@@@STEP_LOG_LINE@python.inline@    raise@@@",
       "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Copy binaries (pathkit.js and pathkit.wasm) to where the karma tests@@@",
+      "@@@STEP_LOG_LINE@python.inline@# expect them ($SKIA_ROOT/experimental/pathkit/npm-wasm/test/)@@@",
       "@@@STEP_LOG_LINE@python.inline@dest = os.path.join(copy_dest, 'pathkit.js')@@@",
       "@@@STEP_LOG_LINE@python.inline@shutil.copyfile(helper_js, dest)@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.@@@",
@@ -150,6 +154,37 @@
       "@@@STEP_LOG_LINE@python.inline@dest = os.path.join(copy_dest, 'pathkit.wasm')@@@",
       "@@@STEP_LOG_LINE@python.inline@shutil.copyfile(wasm, dest)@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Prepare output folder@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_BOT_ID', '')\n"
+    ],
+    "name": "get swarming bot id",
+    "stdout": "/path/to/tmp/",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_BOT_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_TASK_ID', '')\n"
+    ],
+    "name": "get swarming task id",
+    "stdout": "/path/to/tmp/",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_TASK_ID', '')@@@",
       "@@@STEP_LOG_END@python.inline@@@"
     ]
   },
@@ -163,11 +198,22 @@
       "[START_DIR]/cache/work:/SRC",
       "-v",
       "[START_DIR]/[SWARM_OUT_DIR]:/OUT",
-      "gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v1",
-      "karma",
-      "start",
-      "/SRC/skia/experimental/pathkit/karma-docker.conf.js",
-      "--single-run"
+      "gcr.io/skia-public/gold-karma-chrome-tests:68.0.3440.106_v1",
+      "/SRC/skia/infra/pathkit/docker/test_pathkit.sh",
+      "--builder",
+      "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit",
+      "--git_hash",
+      "abc123",
+      "--buildbucket_build_id",
+      "",
+      "--bot_id",
+      "",
+      "--task_id",
+      "",
+      "--browser",
+      "Chrome",
+      "--config",
+      "Debug"
     ],
     "env": {
       "CHROME_HEADLESS": "1",
diff --git a/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json b/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json
new file mode 100644
index 0000000..ea2bb24
--- /dev/null
+++ b/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json
@@ -0,0 +1,235 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/cache/work"
+    ],
+    "infra_step": true,
+    "name": "makedirs checkout_path"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "remove",
+      "[START_DIR]/cache/work/.gclient_entries"
+    ],
+    "infra_step": true,
+    "name": "remove [START_DIR]/cache/work/.gclient_entries"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[depot_tools::bot_update]/resources/bot_update.py",
+      "--spec-path",
+      "cache_dir = '[START_DIR]/cache/git'\nsolutions = [{'deps_file': '.DEPS.git', 'managed': False, 'name': 'skia', 'url': 'https://skia.googlesource.com/skia.git'}]",
+      "--patch_root",
+      "skia",
+      "--revision_mapping_file",
+      "{\"got_revision\": \"skia\"}",
+      "--git-cache-dir",
+      "[START_DIR]/cache/git",
+      "--cleanup-dir",
+      "[CLEANUP]/bot_update",
+      "--output_json",
+      "/path/to/tmp/json",
+      "--revision",
+      "skia@abc123"
+    ],
+    "cwd": "[START_DIR]/cache/work",
+    "env_prefixes": {
+      "PATH": [
+        "RECIPE_PACKAGE_REPO[depot_tools]"
+      ]
+    },
+    "infra_step": true,
+    "name": "bot_update",
+    "~followup_annotations": [
+      "@@@STEP_TEXT@Some step text@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"did_run\": true, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"fixed_revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": \"abc123\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"repository\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@      \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_failure\": false, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"patch_root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"properties\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\", @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"got_revision_cp\": \"refs/heads/master@{#164710}\"@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"root\": \"skia\", @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"source_manifest\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"directories\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"skia\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"git_checkout\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@          \"repo_url\": \"https://fake.org/skia.git\", @@@",
+      "@@@STEP_LOG_LINE@json.output@          \"revision\": \"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@STEP_LOG_LINE@json.output@        }@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }, @@@",
+      "@@@STEP_LOG_LINE@json.output@    \"version\": 0@@@",
+      "@@@STEP_LOG_LINE@json.output@  }, @@@",
+      "@@@STEP_LOG_LINE@json.output@  \"step_text\": \"Some step text\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision@\"9046e2e693bb92a76e972b694580e5d17ad10748\"@@@",
+      "@@@SET_BUILD_PROPERTY@got_revision_cp@\"refs/heads/master@{#164710}\"@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "mkdirs out_dir"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nhelper_js = sys.argv[2]\nwasm = sys.argv[3]\nout_dir = sys.argv[4]\n\n# Clean out old binaries (if any)\ntry:\n  shutil.rmtree(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.ENOENT:\n    raise\n\n# Make folder\ntry:\n  os.makedirs(copy_dest)\nexcept OSError as e:\n  if e.errno != errno.EEXIST:\n    raise\n\n# Copy binaries (pathkit.js and pathkit.wasm) to where the karma tests\n# expect them ($SKIA_ROOT/experimental/pathkit/npm-wasm/test/)\ndest = os.path.join(copy_dest, 'pathkit.js')\nshutil.copyfile(helper_js, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\ndest = os.path.join(copy_dest, 'pathkit.wasm')\nshutil.copyfile(wasm, dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder\nos.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.\n",
+      "[START_DIR]/cache/work/skia/experimental/pathkit/npm-wasm/bin/test",
+      "[START_DIR]/build/pathkit.js",
+      "[START_DIR]/build/pathkit.wasm",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "Set up for docker",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import errno@@@",
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@import shutil@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@copy_dest = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@helper_js = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@wasm = sys.argv[3]@@@",
+      "@@@STEP_LOG_LINE@python.inline@out_dir = sys.argv[4]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Clean out old binaries (if any)@@@",
+      "@@@STEP_LOG_LINE@python.inline@try:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  shutil.rmtree(copy_dest)@@@",
+      "@@@STEP_LOG_LINE@python.inline@except OSError as e:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if e.errno != errno.ENOENT:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    raise@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Make folder@@@",
+      "@@@STEP_LOG_LINE@python.inline@try:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  os.makedirs(copy_dest)@@@",
+      "@@@STEP_LOG_LINE@python.inline@except OSError as e:@@@",
+      "@@@STEP_LOG_LINE@python.inline@  if e.errno != errno.EEXIST:@@@",
+      "@@@STEP_LOG_LINE@python.inline@    raise@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Copy binaries (pathkit.js and pathkit.wasm) to where the karma tests@@@",
+      "@@@STEP_LOG_LINE@python.inline@# expect them ($SKIA_ROOT/experimental/pathkit/npm-wasm/test/)@@@",
+      "@@@STEP_LOG_LINE@python.inline@dest = os.path.join(copy_dest, 'pathkit.js')@@@",
+      "@@@STEP_LOG_LINE@python.inline@shutil.copyfile(helper_js, dest)@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@dest = os.path.join(copy_dest, 'pathkit.wasm')@@@",
+      "@@@STEP_LOG_LINE@python.inline@shutil.copyfile(wasm, dest)@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Prepare output folder@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_BOT_ID', '')\n"
+    ],
+    "name": "get swarming bot id",
+    "stdout": "/path/to/tmp/",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_BOT_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "import os\nprint os.environ.get('SWARMING_TASK_ID', '')\n"
+    ],
+    "name": "get swarming task id",
+    "stdout": "/path/to/tmp/",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@print os.environ.get('SWARMING_TASK_ID', '')@@@",
+      "@@@STEP_LOG_END@python.inline@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "docker",
+      "run",
+      "--shm-size=2gb",
+      "--rm",
+      "-v",
+      "[START_DIR]/cache/work:/SRC",
+      "-v",
+      "[START_DIR]/[SWARM_OUT_DIR]:/OUT",
+      "gcr.io/skia-public/gold-karma-chrome-tests:68.0.3440.106_v1",
+      "/SRC/skia/infra/pathkit/docker/test_pathkit.sh",
+      "--builder",
+      "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit",
+      "--git_hash",
+      "abc123",
+      "--buildbucket_build_id",
+      "",
+      "--bot_id",
+      "",
+      "--task_id",
+      "",
+      "--browser",
+      "Chrome",
+      "--config",
+      "Debug",
+      "--issue",
+      "1234",
+      "--patchset",
+      "7",
+      "--patch_storage",
+      "gerrit"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_PACKAGE_REPO[depot_tools]"
+    },
+    "name": "Test PathKit with Docker"
+  },
+  {
+    "name": "$result",
+    "recipe_result": null,
+    "status_code": 0
+  }
+]
\ No newline at end of file
diff --git a/infra/bots/recipes/test_pathkit.py b/infra/bots/recipes/test_pathkit.py
index 25b27c4..c620ce5 100644
--- a/infra/bots/recipes/test_pathkit.py
+++ b/infra/bots/recipes/test_pathkit.py
@@ -20,8 +20,8 @@
 ]
 
 
-DOCKER_IMAGE = 'gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v1'
-INNER_KARMA_CONFIG = '/SRC/skia/experimental/pathkit/karma-docker.conf.js'
+DOCKER_IMAGE = 'gcr.io/skia-public/gold-karma-chrome-tests:68.0.3440.106_v1'
+INNER_KARMA_SCRIPT = '/SRC/skia/infra/pathkit/docker/test_pathkit.sh'
 
 
 
@@ -32,7 +32,7 @@
   api.checkout.bot_update(checkout_root=checkout_root)
 
   # Make sure this exists, otherwise Docker will make it with root permissions.
-  api.file.ensure_directory('mkdirs out_dir', out_dir)
+  api.file.ensure_directory('mkdirs out_dir', out_dir, mode=0777)
 
   copy_dest = api.path.join(checkout_root, 'skia', 'experimental', 'pathkit',
                         'npm-wasm', 'bin', 'test')
@@ -41,7 +41,7 @@
   wasm = api.vars.build_dir.join('pathkit.wasm')
 
   api.python.inline(
-      name='copy built wasm to location docker can see',
+      name='Set up for docker',
       program='''import errno
 import os
 import shutil
@@ -50,6 +50,7 @@
 copy_dest = sys.argv[1]
 helper_js = sys.argv[2]
 wasm = sys.argv[3]
+out_dir = sys.argv[4]
 
 # Clean out old binaries (if any)
 try:
@@ -65,6 +66,8 @@
   if e.errno != errno.EEXIST:
     raise
 
+# Copy binaries (pathkit.js and pathkit.wasm) to where the karma tests
+# expect them ($SKIA_ROOT/experimental/pathkit/npm-wasm/test/)
 dest = os.path.join(copy_dest, 'pathkit.js')
 shutil.copyfile(helper_js, dest)
 os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.
@@ -72,16 +75,34 @@
 dest = os.path.join(copy_dest, 'pathkit.wasm')
 shutil.copyfile(wasm, dest)
 os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.
+
+# Prepare output folder
+os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.
 ''',
-      args=[copy_dest, helper_js, wasm],
+      args=[copy_dest, helper_js, wasm, out_dir],
       infra_step=True)
 
-  # Remove any previous npm-wasm/bin/test and mkdir
-  # Copy built binaries to npm-wasm/bin/test
+
 
   cmd = ['docker', 'run', '--shm-size=2gb', '--rm',
          '-v', '%s:/SRC' % checkout_root, '-v', '%s:/OUT' % out_dir,
-         DOCKER_IMAGE, 'karma', 'start', INNER_KARMA_CONFIG, '--single-run']
+         DOCKER_IMAGE,  INNER_KARMA_SCRIPT,
+         '--builder',              api.vars.builder_name,
+         '--git_hash',             api.properties['revision'],
+         '--buildbucket_build_id', api.properties.get('buildbucket_build_id',
+                                                      ''),
+         '--bot_id',               api.vars.swarming_bot_id,
+         '--task_id',              api.vars.swarming_task_id,
+         '--browser',              'Chrome',
+         '--config',               api.vars.configuration,
+         ]
+
+  if api.vars.is_trybot:
+    cmd.extend([
+      '--issue',         api.vars.issue,
+      '--patchset',      api.vars.patchset,
+      '--patch_storage', api.vars.patch_storage,
+    ])
 
   api.run(
     api.step,
@@ -99,3 +120,18 @@
                      path_config='kitchen',
                      swarm_out_dir='[SWARM_OUT_DIR]')
   )
+
+  yield (
+      api.test('pathkit_trybot') +
+      api.properties(buildername=('Test-Debian9-EMCC-GCE-CPU-AVX2'
+                                  '-wasm-Debug-All-PathKit'),
+                     repository='https://skia.googlesource.com/skia.git',
+                     revision='abc123',
+                     path_config='kitchen',
+                     swarm_out_dir='[SWARM_OUT_DIR]',
+                     patch_storage='gerrit',
+                     patch_set=7,
+                     patch_issue=1234,
+                     gerrit_project='skia',
+                     gerrit_url='https://skia-review.googlesource.com/')
+  )
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index 394e409..c79cdc6 100644
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -2230,12 +2230,12 @@
     },
     "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit": {
       "tasks": [
-        "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit"
+        "Upload-Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit"
       ]
     },
     "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit": {
       "tasks": [
-        "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit"
+        "Upload-Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit"
       ]
     },
     "Test-Debian9-GCC-GCE-CPU-AVX2-x86-Debug-All": {
@@ -92914,6 +92914,204 @@
       "isolate": "swarm_recipe.isolate",
       "service_account": "skia-external-gm-uploader@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "Upload-Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:546aae39f1fb9dce9add528e2011afa574535ecd"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:e1abc57be62d198b5c2f487bfb2fa2d2eb0e867c"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:b6cdec8586c9f8d3d728b1bc0bd4331330ba66fc"
+        },
+        {
+          "name": "infra/gsutil",
+          "path": "cipd_bin_packages",
+          "version": "version:4.28"
+        }
+      ],
+      "command": [
+        "./kitchen${EXECUTABLE_SUFFIX}",
+        "cook",
+        "-checkout-dir",
+        "recipe_bundle",
+        "-mode",
+        "swarming",
+        "-luci-system-account",
+        "system",
+        "-cache-dir",
+        "cache",
+        "-temp-dir",
+        "tmp",
+        "-known-gerrit-host",
+        "android.googlesource.com",
+        "-known-gerrit-host",
+        "boringssl.googlesource.com",
+        "-known-gerrit-host",
+        "chromium.googlesource.com",
+        "-known-gerrit-host",
+        "dart.googlesource.com",
+        "-known-gerrit-host",
+        "fuchsia.googlesource.com",
+        "-known-gerrit-host",
+        "go.googlesource.com",
+        "-known-gerrit-host",
+        "llvm.googlesource.com",
+        "-known-gerrit-host",
+        "skia.googlesource.com",
+        "-known-gerrit-host",
+        "webrtc.googlesource.com",
+        "-output-result-json",
+        "${ISOLATED_OUTDIR}/build_result_filename",
+        "-workdir",
+        ".",
+        "-recipe",
+        "upload_dm_results",
+        "-properties",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit\",\"gs_bucket\":\"skia-infra-gm\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"output_ignored\"}",
+        "-logdog-annotation-url",
+        "logdog://logs.chromium.org/skia/<(TASK_ID)/+/annotations"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes",
+        "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-highmem-2",
+        "os:Debian-9.4",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "${cache_dir}/vpython"
+        ]
+      },
+      "execution_timeout_ns": 3600000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/<(TASK_ID)/+/annotations"
+      },
+      "io_timeout_ns": 3600000000000,
+      "isolate": "swarm_recipe.isolate",
+      "service_account": "skia-external-gm-uploader@skia-swarming-bots.iam.gserviceaccount.com"
+    },
+    "Upload-Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit": {
+      "caches": [
+        {
+          "name": "vpython",
+          "path": "cache/vpython"
+        }
+      ],
+      "cipd_packages": [
+        {
+          "name": "infra/tools/luci/kitchen/${platform}",
+          "path": ".",
+          "version": "git_revision:546aae39f1fb9dce9add528e2011afa574535ecd"
+        },
+        {
+          "name": "infra/tools/luci-auth/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:e1abc57be62d198b5c2f487bfb2fa2d2eb0e867c"
+        },
+        {
+          "name": "infra/tools/luci/vpython/${platform}",
+          "path": "cipd_bin_packages",
+          "version": "git_revision:b6cdec8586c9f8d3d728b1bc0bd4331330ba66fc"
+        },
+        {
+          "name": "infra/gsutil",
+          "path": "cipd_bin_packages",
+          "version": "version:4.28"
+        }
+      ],
+      "command": [
+        "./kitchen${EXECUTABLE_SUFFIX}",
+        "cook",
+        "-checkout-dir",
+        "recipe_bundle",
+        "-mode",
+        "swarming",
+        "-luci-system-account",
+        "system",
+        "-cache-dir",
+        "cache",
+        "-temp-dir",
+        "tmp",
+        "-known-gerrit-host",
+        "android.googlesource.com",
+        "-known-gerrit-host",
+        "boringssl.googlesource.com",
+        "-known-gerrit-host",
+        "chromium.googlesource.com",
+        "-known-gerrit-host",
+        "dart.googlesource.com",
+        "-known-gerrit-host",
+        "fuchsia.googlesource.com",
+        "-known-gerrit-host",
+        "go.googlesource.com",
+        "-known-gerrit-host",
+        "llvm.googlesource.com",
+        "-known-gerrit-host",
+        "skia.googlesource.com",
+        "-known-gerrit-host",
+        "webrtc.googlesource.com",
+        "-output-result-json",
+        "${ISOLATED_OUTDIR}/build_result_filename",
+        "-workdir",
+        ".",
+        "-recipe",
+        "upload_dm_results",
+        "-properties",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit\",\"gs_bucket\":\"skia-infra-gm\",\"patch_issue\":\"<(ISSUE)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"revision\":\"<(REVISION)\",\"swarm_out_dir\":\"output_ignored\"}",
+        "-logdog-annotation-url",
+        "logdog://logs.chromium.org/skia/<(TASK_ID)/+/annotations"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BundleRecipes",
+        "Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Release-All-PathKit"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-highmem-2",
+        "os:Debian-9.4",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "cipd_bin_packages",
+          "cipd_bin_packages/bin"
+        ],
+        "VPYTHON_VIRTUALENV_ROOT": [
+          "${cache_dir}/vpython"
+        ]
+      },
+      "execution_timeout_ns": 3600000000000,
+      "extra_tags": {
+        "log_location": "logdog://logs.chromium.org/skia/<(TASK_ID)/+/annotations"
+      },
+      "io_timeout_ns": 3600000000000,
+      "isolate": "swarm_recipe.isolate",
+      "service_account": "skia-external-gm-uploader@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "Upload-Test-Debian9-GCC-GCE-CPU-AVX2-x86-Debug-All": {
       "caches": [
         {
diff --git a/infra/pathkit/.gitignore b/infra/pathkit/.gitignore
new file mode 100644
index 0000000..1c2f433
--- /dev/null
+++ b/infra/pathkit/.gitignore
@@ -0,0 +1 @@
+tmp
\ No newline at end of file
diff --git a/infra/pathkit/Makefile b/infra/pathkit/Makefile
new file mode 100644
index 0000000..56e23e9
--- /dev/null
+++ b/infra/pathkit/Makefile
@@ -0,0 +1,8 @@
+gold-docker-image: aggregator
+	# Set the build context to the current work dir, so we can copy
+	# the built binary to where we need it.
+	docker build -t gold-karma-chrome-tests -f ./docker/gold-karma-chrome-tests/Dockerfile .
+
+aggregator:
+	mkdir -p ./tmp
+	CGO_ENABLED=0 GOOS=linux go build -o ./tmp/gold-aggregator -a ./gold/
\ No newline at end of file
diff --git a/infra/pathkit/docker/README.md b/infra/pathkit/docker/README.md
new file mode 100644
index 0000000..e244db6
--- /dev/null
+++ b/infra/pathkit/docker/README.md
@@ -0,0 +1,94 @@
+Docker
+======
+
+Docker files to ease working with PathKit and WASM.
+
+emsdk-base
+----------
+
+This image has an Emscripten SDK environment that can be used for
+compiling projects (e.g. Skia's PathKit) to WASM/asm.js.
+
+This image is standalone and does not have any extra dependencies that make
+it Skia-exclusive.
+
+It gets manually pushed anytime there's an update to the Dockerfile or relevant
+installed libraries.
+
+    docker build -t emsdk-base ./emsdk-base/
+    EMSDK_VERSION="1.38.6_jre"
+    docker tag emsdk-base gcr.io/skia-public/emsdk-release:$EMSDK_VERSION
+    docker push gcr.io/skia-public/emsdk-release:$EMSDK_VERSION
+
+For testing the image locally, the following flow can be helpful:
+
+    docker build -t emsdk-base ./emsdk-base/
+    # Run bash in it to poke around and make sure things are properly installed
+    docker run -it emsdk-release /bin/bash
+    # Compile PathKit with the local image
+    docker run -v $SKIA_ROOT:/SRC -v $SKIA_ROOT/out/dockerpathkit:/OUT emsdk-base /SRC/infra/pathkit/docker/build_pathkit.sh
+
+karma-chrome-tests
+------------------
+
+This image has Google Chrome and karma/jasmine installed on it, which can
+be used to run JS tests.
+
+This image is standalone and does not have any extra dependencies that make
+it Skia-exclusive.
+
+It gets manually pushed anytime there's an update to the Dockerfile or relevant
+installed libraries.
+
+    docker build -t karma-chrome-tests ./karma-chrome-tests/
+    # check the version of chrome with the following:
+    docker run karma-chrome-tests /usr/bin/google-chrome-stable --version
+    CHROME_VERSION="68.0.3440.106_v3"  # use v1, v2, etc for any re-spins of the container.
+    docker tag karma-chrome-tests gcr.io/skia-public/karma-chrome-tests:$CHROME_VERSION
+    docker push gcr.io/skia-public/karma-chrome-tests:$CHROME_VERSION
+
+Of note, some versions (generally before Chrome 60) run out of space on /dev/shm when
+using the default Docker settings.  To be safe, it is recommended to run the container
+with the flag --shm-size=2gb.
+
+For testing the image locally, the following can be helpful:
+
+    docker build -t karma-chrome-tests ./karma-chrome-tests/
+    # Run bash in it to poke around and make sure things are properly installed
+    docker run -it --shm-size=2gb karma-chrome-tests /bin/bash
+    # Run the tests (but not capturing Gold output) with the local source repo
+    docker run --shm-size=2gb -v $SKIA_ROOT:/SRC karma-chrome-tests karma start /SRC/infra/pathkit/karma-docker.conf.js --single-run
+
+gold-karma-chrome-tests
+------------------
+
+This image has Google Chrome and karma/jasmine installed on it, which can
+be used to run JS tests.
+
+This image assumes the runner wants to collect the output images and JSON data
+specific to Skia Infra's Gold tool (image correctness).
+
+It gets manually pushed anytime there's an update to the Dockerfile or the parent
+image (karma-chrome-tests).
+
+    # Run the following from $SKIA_ROOT/infra/pathkit
+    make gold-docker-image
+    # check the version of chrome with the following:
+    docker run gold-karma-chrome-tests /usr/bin/google-chrome-stable --version
+    CHROME_VERSION="68.0.3440.106_v1"  # use v1, v2, etc for any re-spins of the container.
+    docker tag gold-karma-chrome-tests gcr.io/skia-public/gold-karma-chrome-tests:$CHROME_VERSION
+    docker push gcr.io/skia-public/gold-karma-chrome-tests:$CHROME_VERSION
+
+Of note, some versions (generally before Chrome 60) run out of space on /dev/shm when
+using the default Docker settings.  To be safe, it is recommended to run the container
+with the flag --shm-size=2gb.
+
+For testing the image locally, the following can be helpful:
+
+    # Run the following from $SKIA_ROOT/infra/pathkit
+    make gold-docker-image
+    # Run bash in it to poke around and make sure things are properly installed
+    docker run -it --shm-size=2gb gold-karma-chrome-tests /bin/bash
+    # Run the tests and collect Gold output with the local source repo
+    mkdir -p -m 0777 /tmp/dockergold
+    docker run --shm-size=2gb -v $SKIA_ROOT:/SRC -v /tmp/dockergold:/OUT gold-karma-chrome-tests /SRC/infra/pathkit/docker/test_pathkit.sh
diff --git a/experimental/pathkit/docker/build_pathkit.sh b/infra/pathkit/docker/build_pathkit.sh
similarity index 62%
rename from experimental/pathkit/docker/build_pathkit.sh
rename to infra/pathkit/docker/build_pathkit.sh
index ddc4edb..726ee30 100755
--- a/experimental/pathkit/docker/build_pathkit.sh
+++ b/infra/pathkit/docker/build_pathkit.sh
@@ -9,7 +9,10 @@
 # is mounted at /OUT
 
 # For example:
-# docker run -v $SKIA_ROOT:/SRC -v $SKIA_ROOT/out/dockerpathkit:/OUT gcr.io/skia-public/emsdk-release:1.38.6_jre /SRC/experimental/pathkit/docker/build_pathkit.sh
+# docker run -v $SKIA_ROOT:/SRC -v $SKIA_ROOT/out/dockerpathkit:/OUT gcr.io/skia-public/emsdk-release:1.38.6_jre /SRC/infra/pathkit/docker/build_pathkit.sh
 
+#BASE_DIR is the dir this script is in ($SKIA_ROOT/infra/pathkit/docker)
 BASE_DIR=`cd $(dirname ${BASH_SOURCE[0]}) && pwd`
-BUILD_DIR=/OUT $BASE_DIR/../compile.sh $@
+PATHKIT_DIR=$BASE_DIR/../../../experimental/pathkit
+
+BUILD_DIR=/OUT $PATHKIT_DIR/compile.sh $@
diff --git a/experimental/pathkit/docker/emsdk-base/Dockerfile b/infra/pathkit/docker/emsdk-base/Dockerfile
similarity index 100%
rename from experimental/pathkit/docker/emsdk-base/Dockerfile
rename to infra/pathkit/docker/emsdk-base/Dockerfile
diff --git a/infra/pathkit/docker/gold-karma-chrome-tests/Dockerfile b/infra/pathkit/docker/gold-karma-chrome-tests/Dockerfile
new file mode 100644
index 0000000..2ae151f
--- /dev/null
+++ b/infra/pathkit/docker/gold-karma-chrome-tests/Dockerfile
@@ -0,0 +1,9 @@
+# Docker container with Chrome, and karma/jasmine, to be used to run JS tests and
+# collect output for Skia Infra's Gold tool (correctness checking).
+#
+# Tests will be run as non-root (user skia, in fact), so /OUT should have permissions
+# 777 so as to be able to create output there.
+
+FROM gcr.io/skia-public/karma-chrome-tests:68.0.3440.106_v3
+
+COPY /tmp/gold-aggregator /opt/gold-aggregator
\ No newline at end of file
diff --git a/experimental/pathkit/docker/karma-chrome-tests/Dockerfile b/infra/pathkit/docker/karma-chrome-tests/Dockerfile
similarity index 83%
rename from experimental/pathkit/docker/karma-chrome-tests/Dockerfile
rename to infra/pathkit/docker/karma-chrome-tests/Dockerfile
index 7c3c458..a90b95b 100644
--- a/experimental/pathkit/docker/karma-chrome-tests/Dockerfile
+++ b/infra/pathkit/docker/karma-chrome-tests/Dockerfile
@@ -1,5 +1,8 @@
-# Docker container with Chrome and karma/jasmine, to be used to run JS tests.
+# Docker container with Chrome, and karma/jasmine, to be used to run JS tests.
 # Inspired by https://github.com/eirslett/chrome-karma-docker
+#
+# Tests will be run as non-root (user skia, in fact), so /OUT should have permissions
+# 777 so as to be able to create output there.
 
 FROM node:8.11
 
diff --git a/infra/pathkit/docker/test_pathkit.sh b/infra/pathkit/docker/test_pathkit.sh
new file mode 100755
index 0000000..e738d18
--- /dev/null
+++ b/infra/pathkit/docker/test_pathkit.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Copyright 2018 Google LLC
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This assumes it is being run inside a docker container of gold-karma-chrome-tests
+# and a Skia checkout has been mounted at /SRC and the output directory
+# is mounted at /OUT
+
+# For example:
+# docker run -v $SKIA_ROOT:/SRC -v /tmp/dockerout:/OUT gcr.io/skia-public/gold-karma-chrome-tests:68.0.3440.106_v1 /SRC/infra/pathkit/docker/test_pathkit.sh
+
+set -ex
+
+#BASE_DIR is the dir this script is in ($SKIA_ROOT/infra/pathkit/docker)
+BASE_DIR=`cd $(dirname ${BASH_SOURCE[0]}) && pwd`
+PATHKIT_DIR=$BASE_DIR/../../../experimental/pathkit
+
+# Start the aggregator in the background
+/opt/gold-aggregator $@ &
+# Run the tests
+npx karma start $PATHKIT_DIR/karma-docker.conf.js --single-run
+# Tell the aggregator to dump the json
+# This curl command gets the HTTP code and stores it into $CODE
+CODE=`curl -s -o /dev/null -I -w "%{http_code}" -X POST localhost:8081/dump_json`
+if [ $CODE -ne 200 ]; then
+    # If we don't get 200 back, something is wrong with writing to disk, so exit with error
+    exit 1
+fi
diff --git a/infra/pathkit/gold/pathkit_aggregator.go b/infra/pathkit/gold/pathkit_aggregator.go
new file mode 100644
index 0000000..02f1848
--- /dev/null
+++ b/infra/pathkit/gold/pathkit_aggregator.go
@@ -0,0 +1,252 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+// This server runs along side the karma tests and listens for POST requests
+// when any test case reports it has output for Gold. See testReporter.js
+// for the browser side part.
+
+import (
+	"bytes"
+	"crypto/md5"
+	"encoding/base64"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"image"
+	"image/png"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+
+	"go.skia.org/infra/golden/go/goldingestion"
+)
+
+// These files allow us to use upload_dm_results.py out of the box
+const JSON_FILENAME = "dm.json"
+const LOG_FILENAME = "verbose.log"
+
+var (
+	outDir = flag.String("out_dir", "/OUT/", "location to dump the Gold JSON and pngs")
+	port   = flag.String("port", "8081", "Port to listen on.")
+
+	botId            = flag.String("bot_id", "", "swarming bot id")
+	browser          = flag.String("browser", "Chrome", "Browser Key")
+	buildBucketID    = flag.Int64("buildbucket_build_id", 0, "Buildbucket build id key")
+	builder          = flag.String("builder", "", "Builder, like 'Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit'")
+	compiledLanguage = flag.String("compiled_language", "wasm", "wasm or asm.js")
+	config           = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key")
+	gitHash          = flag.String("git_hash", "-", "The git commit hash of the version being tested")
+	hostOS           = flag.String("host_os", "Debian9", "OS Key")
+	issue            = flag.Int64("issue", 0, "issue (if tryjob)")
+	patch_storage    = flag.String("patch_storage", "", "patch storage (if tryjob)")
+	patchset         = flag.Int64("patchset", 0, "patchset (if tryjob)")
+	taskId           = flag.String("task_id", "", "swarming task id")
+)
+
+// Received from the JS side.
+type reportBody struct {
+	// e.g. "canvas" or "svg"
+	OutputType string `json:"output_type"`
+	// a base64 encoded PNG image.
+	Data string `json:"data"`
+	// a name describing the test. Should be unique enough to allow use of grep.
+	TestName string `json:"test_name"`
+}
+
+// The keys to be used at the top level for all Results.
+var defaultKeys map[string]string
+
+// contains all the results reported in through report_gold_data
+var results []*goldingestion.Result
+
+func main() {
+	flag.Parse()
+
+	defaultKeys = map[string]string{
+		"arch":              "WASM",
+		"browser":           *browser,
+		"compiled_language": *compiledLanguage,
+		"compiler":          "emsdk",
+		"configuration":     *config,
+		"cpu_or_gpu":        "CPU",
+		"cpu_or_gpu_value":  "Browser",
+		"os":                *hostOS,
+		"source_type":       "pathkit",
+	}
+
+	results = []*goldingestion.Result{}
+
+	http.HandleFunc("/report_gold_data", reporter)
+	http.HandleFunc("/dump_json", dumpJSON)
+
+	fmt.Printf("Waiting for gold ingestion on port %s\n", *port)
+
+	log.Fatal(http.ListenAndServe(":"+*port, nil))
+}
+
+// reporter handles when the client reports a test has Gold output.
+// It writes the corresponding PNG to disk and appends a Result, assuming
+// no errors.
+func reporter(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		http.Error(w, "Only POST accepted", 400)
+		return
+	}
+	defer r.Body.Close()
+
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, "Malformed body", 400)
+		return
+	}
+
+	testOutput := reportBody{}
+	if err := json.Unmarshal(body, &testOutput); err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not unmarshal JSON", 400)
+		return
+	}
+
+	hash := ""
+	if hash, err = writeBase64EncodedPNG(testOutput.Data); err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not write image to disk", 500)
+		return
+	}
+
+	if _, err := w.Write([]byte("Accepted")); err != nil {
+		fmt.Printf("Could not write response: %s\n", err)
+		return
+	}
+
+	results = append(results, &goldingestion.Result{
+		Digest: hash,
+		Key: map[string]string{
+			"name":   testOutput.TestName,
+			"config": testOutput.OutputType,
+		},
+		Options: map[string]string{
+			"ext": "png",
+		},
+	})
+}
+
+// createOutputFile creates a file and set permissions correctly.
+func createOutputFile(p string) (*os.File, error) {
+	outputFile, err := os.Create(p)
+	if err != nil {
+		return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err)
+	}
+	// Make this accessible (and deletable) by all users
+	if err = outputFile.Chmod(0666); err != nil {
+		return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err)
+	}
+	return outputFile, nil
+}
+
+// dumpJSON writes out a JSON file with all the results, typically at the end of
+// all the tests.
+func dumpJSON(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		http.Error(w, "Only POST accepted", 400)
+		return
+	}
+
+	p := path.Join(*outDir, JSON_FILENAME)
+	outputFile, err := createOutputFile(p)
+	defer outputFile.Close()
+	if err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not open json file on disk", 500)
+		return
+	}
+
+	results := goldingestion.DMResults{
+		BuildBucketID:  *buildBucketID,
+		Builder:        *builder,
+		GitHash:        *gitHash,
+		Issue:          *issue,
+		Key:            defaultKeys,
+		PatchStorage:   *patch_storage,
+		Patchset:       *patchset,
+		Results:        results,
+		SwarmingBotID:  *botId,
+		SwarmingTaskID: *taskId,
+	}
+
+	enc := json.NewEncoder(outputFile)
+	enc.SetIndent("", "  ") // Make it human readable.
+	if err := enc.Encode(&results); err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not write json to disk", 500)
+		return
+	}
+	fmt.Println("JSON Written")
+
+	// Create verbose.log with a few characters in it.
+	p = path.Join(*outDir, LOG_FILENAME)
+	logFile, err := createOutputFile(p)
+	if err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not open log file on disk", 500)
+		return
+	}
+	if _, err := logFile.WriteString("done"); err != nil {
+		fmt.Println(err)
+		http.Error(w, "Could not write to logfile on disk", 500)
+		return
+	}
+}
+
+// writeBase64EncodedPNG writes a PNG to disk and returns the md5 of the
+// decoded PNG bytes and any error. This hash is what will be used as
+// the gold digest and the file name.
+func writeBase64EncodedPNG(data string) (string, error) {
+	// data starts with something like data:image/png;base64,[data]
+	// https://en.wikipedia.org/wiki/Data_URI_scheme
+	start := strings.Index(data, ",")
+	b := bytes.NewBufferString(data[start+1:])
+	pngReader := base64.NewDecoder(base64.StdEncoding, b)
+
+	pngBytes, err := ioutil.ReadAll(pngReader)
+	if err != nil {
+		return "", fmt.Errorf("Could not decode base 64 encoding %s", err)
+	}
+
+	// compute the hash of the pixel values, like DM does
+	img, err := png.Decode(bytes.NewBuffer(pngBytes))
+	if err != nil {
+		return "", fmt.Errorf("Not a valid png: %s", err)
+	}
+	hash := ""
+	switch img.(type) {
+	case *image.NRGBA:
+		i := img.(*image.NRGBA)
+		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
+	case *image.RGBA:
+		i := img.(*image.RGBA)
+		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
+	case *image.RGBA64:
+		i := img.(*image.RGBA64)
+		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
+	default:
+		return "", fmt.Errorf("Unknown type of image")
+	}
+
+	p := path.Join(*outDir, hash+".png")
+	outputFile, err := createOutputFile(p)
+	defer outputFile.Close()
+	if err != nil {
+		return "", fmt.Errorf("Could not create png file %s: %s", p, err)
+	}
+	if _, err = outputFile.Write(pngBytes); err != nil {
+		return "", fmt.Errorf("Could not write to file %s: %s", p, err)
+	}
+	return hash, nil
+}