Add docker-based infrastructure for Gold/lottie-web integration

This adds 2 docker containers (based on the ones used for PathKit
added in https://skia-review.googlesource.com/c/skia/+/147042)
which bundle Chrome and puppeteer to allow the lottiecap.js
to be run anywhere.

This adds a recipe (test_lottie_web.py) to drive the docker
container and do a little bit of set-up (and a few docs/bug
fixes discovered in the test_pathkit.py that it was based on).

Additionally, this modifies lottiecap.js to support POSTing
output to a running go server (again, the same as PathKit)
which has the image data hashed and the metadata added to
a large JSON output.

This re-works driver.html to avoid re-loading the JSON object
25 times. The performance boost is important because, right now,
the lottie files are processed individually.

In a future CL, I want to address the two TODOs in
lottie-web-aggregator.go

Bug: skia:8108
Change-Id: I100c9ce23dcc5033a27287211cbf0db898960da9
Reviewed-on: https://skia-review.googlesource.com/149282
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Stephan Altmueller <stephana@google.com>
diff --git a/infra/bots/gen_tasks.go b/infra/bots/gen_tasks.go
index eac223d..48cecff 100644
--- a/infra/bots/gen_tasks.go
+++ b/infra/bots/gen_tasks.go
@@ -1044,6 +1044,8 @@
 		recipe = "compute_test"
 	} else if strings.Contains(name, "PathKit") {
 		recipe = "test_pathkit"
+	} else if strings.Contains(name, "LottieWeb") {
+		recipe = "test_lottie_web"
 	}
 	extraProps := map[string]string{
 		"gold_hashes_url": CONFIG.GoldHashesURL,
@@ -1053,7 +1055,7 @@
 		extraProps["internal_hardware_label"] = strconv.Itoa(*iid)
 	}
 	isolate := "test_skia_bundled.isolate"
-	if strings.Contains(name, "PathKit") {
+	if strings.Contains(name, "PathKit") || strings.Contains(name, "LottieWeb") {
 		isolate = "swarm_recipe.isolate"
 	}
 	task := kitchenTask(name, recipe, isolate, "", swarmDimensions(parts), extraProps, OUTPUT_TEST)
@@ -1061,7 +1063,11 @@
 	if strings.Contains(name, "Lottie") {
 		task.CipdPackages = append(task.CipdPackages, b.MustGetCipdPackageFromAsset("lottie-samples"))
 	}
-	task.Dependencies = append(task.Dependencies, compileTaskName)
+	if !strings.Contains(name, "LottieWeb") {
+		// Test.+LottieWeb doesn't require anything in Skia to be compiled.
+		task.Dependencies = append(task.Dependencies, compileTaskName)
+	}
+
 	if strings.Contains(name, "Android_ASAN") {
 		task.Dependencies = append(task.Dependencies, isolateCIPDAsset(b, ISOLATE_NDK_LINUX_NAME))
 	}
@@ -1302,7 +1308,8 @@
 		!strings.Contains(name, "Android_Framework") &&
 		!strings.Contains(name, "RecreateSKPs") &&
 		!strings.Contains(name, "-CT_") &&
-		!strings.Contains(name, "Housekeeper-PerCommit-Isolate") {
+		!strings.Contains(name, "Housekeeper-PerCommit-Isolate") &&
+		!strings.Contains(name, "LottieWeb") {
 		compile(b, compileTaskName, compileTaskParts)
 		if parts["role"] == "Calmbench" {
 			compile(b, compileParentName, compileParentParts)
@@ -1365,7 +1372,8 @@
 	if strings.Contains(name, "ProcDump") {
 		pkgs = append(pkgs, b.MustGetCipdPackageFromAsset("procdump_win"))
 	}
-	if strings.Contains(name, "PathKit") {
+	if strings.Contains(name, "PathKit") || strings.Contains(name, "LottieWeb") {
+		// Docker-based tests that don't need the standard CIPD assets
 		pkgs = []*specs.CipdPackage{}
 	}
 
diff --git a/infra/bots/recipe_modules/build/pathkit.py b/infra/bots/recipe_modules/build/pathkit.py
index 6c0c882..d14c3fe 100644
--- a/infra/bots/recipe_modules/build/pathkit.py
+++ b/infra/bots/recipe_modules/build/pathkit.py
@@ -20,7 +20,7 @@
   # 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.
-  api.file.ensure_directory('mkdirs out_dir', out_dir)
+  api.file.ensure_directory('mkdirs out_dir', out_dir, mode=0777)
 
   # This uses the emscriptem sdk docker image and says "run the
   # build_pathkit.sh helper script in there". Additionally, it binds two
diff --git a/infra/bots/recipes/test_lottie_web.expected/Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb.json b/infra/bots/recipes/test_lottie_web.expected/Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb.json
new file mode 100644
index 0000000..d5834d4
--- /dev/null
+++ b/infra/bots/recipes/test_lottie_web.expected/Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb.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",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "/tmp/lottie_files"
+    ],
+    "infra_step": true,
+    "name": "remove previous lottie files"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copytree",
+      "[START_DIR]/lottie-samples",
+      "/tmp/lottie_files"
+    ],
+    "infra_step": true,
+    "name": "copy lottie files"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport os\nimport sys\n\nlottie_files_dir = sys.argv[1]\nout_dir = sys.argv[2]\n\n# Make sure all the lottie files are readable by everyone so we can see\n# them in the docker container.\nos.system('chmod 0644 %s/*' % lottie_files_dir)\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\n# This typically means that the non-privileged docker won't be able to write.\nos.chmod(out_dir, 0o777)\n",
+      "/tmp/lottie_files",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "Set up for docker",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@lottie_files_dir = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@out_dir = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Make sure all the lottie files are readable by everyone so we can see@@@",
+      "@@@STEP_LOG_LINE@python.inline@# them in the docker container.@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.system('chmod 0644 %s/*' % lottie_files_dir)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
+      "@@@STEP_LOG_LINE@python.inline@# This typically means that the non-privileged docker won't be able to write.@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777)@@@",
+      "@@@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",
+      "-v",
+      "/tmp/lottie_files:/LOTTIE_FILES",
+      "gcr.io/skia-public/gold-lottie-web-puppeteer:5.2.1_v1",
+      "/SRC/skia/infra/lottiecap/docker/lottiecap_gold.sh",
+      "--builder",
+      "Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb",
+      "--git_hash",
+      "abc123",
+      "--buildbucket_build_id",
+      "",
+      "--bot_id",
+      "",
+      "--task_id",
+      "",
+      "--browser",
+      "Chrome",
+      "--config",
+      "Debug"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_PACKAGE_REPO[depot_tools]"
+    },
+    "name": "Create lottie-web Gold output with Docker"
+  },
+  {
+    "name": "$result",
+    "recipe_result": null,
+    "status_code": 0
+  }
+]
\ No newline at end of file
diff --git a/infra/bots/recipes/test_lottie_web.expected/lottie_web_trybot.json b/infra/bots/recipes/test_lottie_web.expected/lottie_web_trybot.json
new file mode 100644
index 0000000..0558dab
--- /dev/null
+++ b/infra/bots/recipes/test_lottie_web.expected/lottie_web_trybot.json
@@ -0,0 +1,241 @@
+[
+  {
+    "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",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "/tmp/lottie_files"
+    ],
+    "infra_step": true,
+    "name": "remove previous lottie files"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copytree",
+      "[START_DIR]/lottie-samples",
+      "/tmp/lottie_files"
+    ],
+    "infra_step": true,
+    "name": "copy lottie files"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "\nimport os\nimport sys\n\nlottie_files_dir = sys.argv[1]\nout_dir = sys.argv[2]\n\n# Make sure all the lottie files are readable by everyone so we can see\n# them in the docker container.\nos.system('chmod 0644 %s/*' % lottie_files_dir)\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\n# This typically means that the non-privileged docker won't be able to write.\nos.chmod(out_dir, 0o777)\n",
+      "/tmp/lottie_files",
+      "[START_DIR]/[SWARM_OUT_DIR]"
+    ],
+    "infra_step": true,
+    "name": "Set up for docker",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@import os@@@",
+      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@lottie_files_dir = sys.argv[1]@@@",
+      "@@@STEP_LOG_LINE@python.inline@out_dir = sys.argv[2]@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Make sure all the lottie files are readable by everyone so we can see@@@",
+      "@@@STEP_LOG_LINE@python.inline@# them in the docker container.@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.system('chmod 0644 %s/*' % lottie_files_dir)@@@",
+      "@@@STEP_LOG_LINE@python.inline@@@@",
+      "@@@STEP_LOG_LINE@python.inline@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
+      "@@@STEP_LOG_LINE@python.inline@# This typically means that the non-privileged docker won't be able to write.@@@",
+      "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777)@@@",
+      "@@@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",
+      "-v",
+      "/tmp/lottie_files:/LOTTIE_FILES",
+      "gcr.io/skia-public/gold-lottie-web-puppeteer:5.2.1_v1",
+      "/SRC/skia/infra/lottiecap/docker/lottiecap_gold.sh",
+      "--builder",
+      "Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb",
+      "--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": "Create lottie-web Gold output with Docker"
+  },
+  {
+    "name": "$result",
+    "recipe_result": null,
+    "status_code": 0
+  }
+]
\ No newline at end of file
diff --git a/infra/bots/recipes/test_lottie_web.py b/infra/bots/recipes/test_lottie_web.py
new file mode 100644
index 0000000..d87b2fe
--- /dev/null
+++ b/infra/bots/recipes/test_lottie_web.py
@@ -0,0 +1,118 @@
+# 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.
+
+# Recipe which generates the Gold images for lottie-web using docker
+
+DEPS = [
+  'checkout',
+  'infra',
+  'recipe_engine/file',
+  'recipe_engine/path',
+  'recipe_engine/properties',
+  'recipe_engine/python',
+  'recipe_engine/step',
+  'run',
+  'vars',
+]
+
+
+DOCKER_IMAGE = 'gcr.io/skia-public/gold-lottie-web-puppeteer:5.2.1_v1'
+LOTTIECAP_SCRIPT = '/SRC/skia/infra/lottiecap/docker/lottiecap_gold.sh'
+
+
+def RunSteps(api):
+  api.vars.setup()
+  checkout_root = api.checkout.default_checkout_root
+  out_dir = api.vars.swarming_out_dir
+  lottie_files_src = api.vars.slave_dir.join('lottie-samples')
+  lottie_files_dir = '/tmp/lottie_files'
+  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, mode=0777)
+  # When lottie files are brought in via isolate or CIPD, they are just
+  # symlinks, which does not work well with Docker because we can't mount
+  # the folder as a volume.
+  # e.g. /LOTTIE_FILES/foo.json -> ../.cipd/alpha/beta/foo.json
+  # To get around this, we just copy the lottie files into a temporary
+  # directory.
+  api.file.rmtree('remove previous lottie files', lottie_files_dir)
+  api.file.copytree('copy lottie files', lottie_files_src, lottie_files_dir)
+
+  api.python.inline(
+      name='Set up for docker',
+      program='''
+import os
+import sys
+
+lottie_files_dir = sys.argv[1]
+out_dir = sys.argv[2]
+
+# Make sure all the lottie files are readable by everyone so we can see
+# them in the docker container.
+os.system('chmod 0644 %s/*' % lottie_files_dir)
+
+# Prepare output folder, api.file.ensure_directory doesn't touch
+# the permissions of the out directory if it already exists.
+# This typically means that the non-privileged docker won't be able to write.
+os.chmod(out_dir, 0o777)
+''',
+      args=[lottie_files_dir, out_dir],
+      infra_step=True)
+
+  cmd = ['docker', 'run', '--shm-size=2gb', '--rm',
+         '-v', '%s:/SRC' % checkout_root,
+         '-v', '%s:/OUT' % out_dir,
+         '-v', '%s:/LOTTIE_FILES' % lottie_files_dir]
+
+  cmd.extend([
+         DOCKER_IMAGE,             LOTTIECAP_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,
+    'Create lottie-web Gold output with Docker',
+    cmd=cmd)
+
+
+def GenTests(api):
+  yield (
+      api.test('Test-Debian9-none-GCE-CPU-AVX2-x86_64-Debug-All-LottieWeb') +
+      api.properties(buildername=('Test-Debian9-none-GCE-CPU-AVX2'
+                                  '-x86_64-Debug-All-LottieWeb'),
+                     repository='https://skia.googlesource.com/skia.git',
+                     revision='abc123',
+                     path_config='kitchen',
+                     swarm_out_dir='[SWARM_OUT_DIR]')
+  )
+
+  yield (
+      api.test('lottie_web_trybot') +
+      api.properties(buildername=('Test-Debian9-none-GCE-CPU-AVX2'
+                                  '-x86_64-Debug-All-LottieWeb'),
+                     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/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json
index cdc667d..17dbe9a 100644
--- a/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json
+++ b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Debug-All-PathKit.json
@@ -112,7 +112,7 @@
     "cmd": [
       "python",
       "-u",
-      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.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",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\nos.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.\n",
       "[START_DIR]/cache/work/skia/experimental/pathkit/npm-asmjs/bin/test",
       "[START_DIR]/build",
       "",
@@ -156,7 +156,8 @@
       "@@@STEP_LOG_LINE@python.inline@  shutil.copyfile(os.path.join(base_dir, bundle_name), 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@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
       "@@@STEP_LOG_END@python.inline@@@"
     ]
diff --git a/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json
index 17bcd60..8ad1086 100644
--- a/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json
+++ b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-asmjs-Release-All-PathKit.json
@@ -112,7 +112,7 @@
     "cmd": [
       "python",
       "-u",
-      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.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",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\nos.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.\n",
       "[START_DIR]/cache/work/skia/experimental/pathkit/npm-asmjs/bin/test",
       "[START_DIR]/build",
       "pathkit.js.mem",
@@ -156,7 +156,8 @@
       "@@@STEP_LOG_LINE@python.inline@  shutil.copyfile(os.path.join(base_dir, bundle_name), 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@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
       "@@@STEP_LOG_END@python.inline@@@"
     ]
diff --git a/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json
index b243fd1..237b683 100644
--- a/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json
+++ b/infra/bots/recipes/test_pathkit.expected/Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit.json
@@ -112,7 +112,7 @@
     "cmd": [
       "python",
       "-u",
-      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.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",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\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.wasm",
@@ -156,7 +156,8 @@
       "@@@STEP_LOG_LINE@python.inline@  shutil.copyfile(os.path.join(base_dir, bundle_name), 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@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
       "@@@STEP_LOG_END@python.inline@@@"
     ]
diff --git a/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json b/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json
index cb5e35b..19dec41 100644
--- a/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json
+++ b/infra/bots/recipes/test_pathkit.expected/pathkit_trybot.json
@@ -112,7 +112,7 @@
     "cmd": [
       "python",
       "-u",
-      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.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",
+      "import errno\nimport os\nimport shutil\nimport sys\n\ncopy_dest = sys.argv[1]\nbase_dir = sys.argv[2]\nbundle_name = 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(os.path.join(base_dir, 'pathkit.js'), dest)\nos.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\nif bundle_name:\n  dest = os.path.join(copy_dest, bundle_name)\n  shutil.copyfile(os.path.join(base_dir, bundle_name), dest)\n  os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.\n\n# Prepare output folder, api.file.ensure_directory doesn't touch\n# the permissions of the out directory if it already exists.\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.wasm",
@@ -156,7 +156,8 @@
       "@@@STEP_LOG_LINE@python.inline@  shutil.copyfile(os.path.join(base_dir, bundle_name), 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@# Prepare output folder, api.file.ensure_directory doesn't touch@@@",
+      "@@@STEP_LOG_LINE@python.inline@# the permissions of the out directory if it already exists.@@@",
       "@@@STEP_LOG_LINE@python.inline@os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.@@@",
       "@@@STEP_LOG_END@python.inline@@@"
     ]
diff --git a/infra/bots/recipes/test_pathkit.py b/infra/bots/recipes/test_pathkit.py
index aa42068..fdb27cf 100644
--- a/infra/bots/recipes/test_pathkit.py
+++ b/infra/bots/recipes/test_pathkit.py
@@ -2,10 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-
 # Recipe which runs the PathKit tests using docker
 
-
 DEPS = [
   'checkout',
   'infra',
@@ -23,7 +21,6 @@
 INNER_KARMA_SCRIPT = '/SRC/skia/infra/pathkit/docker/test_pathkit.sh'
 
 
-
 def RunSteps(api):
   api.vars.setup()
   checkout_root = api.checkout.default_checkout_root
@@ -88,7 +85,8 @@
   shutil.copyfile(os.path.join(base_dir, bundle_name), dest)
   os.chmod(dest, 0o644) # important, otherwise non-privileged docker can't read.
 
-# Prepare output folder
+# Prepare output folder, api.file.ensure_directory doesn't touch
+# the permissions of the out directory if it already exists.
 os.chmod(out_dir, 0o777) # important, otherwise non-privileged docker can't write.
 ''',
       args=[copy_dest, base_dir, bundle_name, out_dir],
@@ -103,7 +101,7 @@
     cmd.extend(['-e', 'ASM_JS=1'])  # -e sets environment variables
 
   cmd.extend([
-         DOCKER_IMAGE,  INNER_KARMA_SCRIPT,
+         DOCKER_IMAGE,             INNER_KARMA_SCRIPT,
          '--builder',              api.vars.builder_name,
          '--git_hash',             api.properties['revision'],
          '--buildbucket_build_id', api.properties.get('buildbucket_build_id',
diff --git a/infra/lottiecap/.gitignore b/infra/lottiecap/.gitignore
new file mode 100644
index 0000000..1c2f433
--- /dev/null
+++ b/infra/lottiecap/.gitignore
@@ -0,0 +1 @@
+tmp
\ No newline at end of file
diff --git a/infra/lottiecap/Makefile b/infra/lottiecap/Makefile
new file mode 100644
index 0000000..7ab6c03
--- /dev/null
+++ b/infra/lottiecap/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-lottie-web-puppeteer -f ./docker/gold-lottie-web-puppeteer/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/lottiecap/docker/README.md b/infra/lottiecap/docker/README.md
new file mode 100644
index 0000000..dd97b16
--- /dev/null
+++ b/infra/lottiecap/docker/README.md
@@ -0,0 +1,68 @@
+Docker
+======
+
+Docker files to handle Gold + lottie-web integration
+
+
+lottie-web-puppeteer
+--------------------
+
+This image has Google Chrome, [puppeteer](https://github.com/GoogleChrome/puppeteer),
+and a few other tools for automating web-browser 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 lottie-web-puppeteer ./lottie-web-puppeteer/
+    LOTTIE_VERSION="5.2.1_v1"  # use v1, v2, etc for any re-spins of the container.
+    docker tag lottie-web-puppeteer gcr.io/skia-public/lottie-web-puppeteer:$LOTTIE_VERSION
+    docker push gcr.io/skia-public/lottie-web-puppeteer:$LOTTIE_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 lottie-web-puppeteer ./lottie-web-puppeteer/
+    # Run bash in it to poke around and make sure things are properly installed
+    docker run -it --shm-size=2gb lottie-web-puppeteer /bin/bash
+    # Create a screenshot of a single .json file which will be put in
+    # $SKIA_ROOT/tools/lottiecap/docker_strip.png
+    docker run -it -v $SKIA_ROOT:/SRC -v ~/lottie-samples:/LOTTIE_FILES -w /SRC/tools/lottiecap lottie-web-puppeteer node /SRC/tools/lottiecap/lottiecap.js --input /LOTTIE_FILES/body_movin.json --lottie_player /usr/local/lib/node_modules/lottie-web/build/player/lottie.min.js --in_docker --output docker_strip.png
+
+gold-lottie-web-puppeteer
+------------------
+
+This image has Google Chrome, [puppeteer](https://github.com/GoogleChrome/puppeteer),
+and a few other tools for automating web-browser 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 relevant
+installed libraries.
+
+    # Run the following from $SKIA_ROOT/infra/pathkit
+    make gold-docker-image
+    LOTTIE_VERSION="5.2.1_v1"  # use v1, v2, etc for any re-spins of the container.
+    docker tag gold-lottie-web-puppeteer gcr.io/skia-public/gold-lottie-web-puppeteer:$LOTTIE_VERSION
+    docker push gcr.io/skia-public/gold-lottie-web-puppeteer:$LOTTIE_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
+    docker run -it --shm-size=2gb gold-lottie-web-puppeteer /bin/bash
+    # Collect the gold output with the local source repo and *all* of the files
+    # from lottie-samples
+    mkdir -p -m 0777 /tmp/dockerout
+    docker run -v ~/lottie-samples:/LOTTIE_FILES -v $SKIA_ROOT:/SRC -v /tmp/dockerout:/OUT gold-lottie-web-puppeteer /SRC/infra/lottiecap/docker/lottiecap_gold.sh
\ No newline at end of file
diff --git a/infra/lottiecap/docker/gold-lottie-web-puppeteer/Dockerfile b/infra/lottiecap/docker/gold-lottie-web-puppeteer/Dockerfile
new file mode 100644
index 0000000..3ba7491
--- /dev/null
+++ b/infra/lottiecap/docker/gold-lottie-web-puppeteer/Dockerfile
@@ -0,0 +1,9 @@
+# Docker container with Google Chrome and puppeteer; to be used to
+# 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/lottie-web-puppeteer:5.2.1_v1
+
+COPY /tmp/gold-aggregator /opt/gold-aggregator
\ No newline at end of file
diff --git a/infra/lottiecap/docker/lottie-web-puppeteer/Dockerfile b/infra/lottiecap/docker/lottie-web-puppeteer/Dockerfile
new file mode 100644
index 0000000..caf820c
--- /dev/null
+++ b/infra/lottiecap/docker/lottie-web-puppeteer/Dockerfile
@@ -0,0 +1,47 @@
+# Docker container with Google Chrome and puppeteer.
+#
+# 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
+
+RUN apt-get update && apt-get upgrade -y
+
+RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64.deb
+RUN dpkg -i dumb-init_*.deb
+
+# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
+# recommends using dumb-init to "prevent zombie chrome processes"
+ENTRYPOINT ["/usr/bin/dumb-init", "--"]
+
+RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
+RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
+RUN apt-get update && apt-get install -y google-chrome-stable
+
+ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
+
+RUN npm install --global \
+    command-line-args@5.0.2 \
+    command-line-usage@5.0.3 \
+    express@4.16.3 \
+    lottie-web@5.2.1 \
+    node-fetch@2.2.0 \
+    puppeteer@1.6.2
+
+# Allows require('puppeteer') to work from anywhere.
+# https://stackoverflow.com/a/15646750
+ENV NODE_PATH=/usr/local/lib/node_modules
+
+#Add user so we don't have to run as root (prevents us from over-writing files in /SRC)
+RUN groupadd -g 2000 skia \
+    && useradd -u 2000 -g 2000 skia \
+    && mkdir -p /home/skia \
+    && chown -R skia:skia /home/skia
+
+# These directories can be used for mounting a source checkout and having a place to put outputs.
+RUN mkdir -m 0777 /SRC /OUT
+
+# Run everything after as non-privileged user.
+USER skia
+
+WORKDIR /home/skia
\ No newline at end of file
diff --git a/infra/lottiecap/docker/lottiecap_gold.sh b/infra/lottiecap/docker/lottiecap_gold.sh
new file mode 100755
index 0000000..f367dd0
--- /dev/null
+++ b/infra/lottiecap/docker/lottiecap_gold.sh
@@ -0,0 +1,42 @@
+#!/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, the output directory
+# is mounted at /OUT, and any lottie json files are in a folder and mounted
+# at /LOTTIE_FILES.
+
+# For example:
+# docker run -v ~/lottie-samples:/LOTTIE_FILES -v $SKIA_ROOT:/SRC -v /tmp/dockerout:/OUT gcr.io/skia-public/gold-lottie-web-puppeteer:5.2.1_v1 /SRC/infra/lottiecap/docker/lottiecap_gold.sh
+
+set -ex
+
+#BASE_DIR is the dir this script is in ($SKIA_ROOT/infra/lottiecap/docker)
+BASE_DIR=`cd $(dirname ${BASH_SOURCE[0]}) && pwd`
+LOTTIECAP_DIR=$BASE_DIR/../../../tools/lottiecap
+
+# Start the aggregator in the background
+/opt/gold-aggregator $@ &
+
+cd $LOTTIECAP_DIR
+
+# lottie files may have spaces in their names, so a naive bash for loop
+# did not work here.
+find /LOTTIE_FILES -not -path /LOTTIE_FILES -exec \
+    node ./lottiecap.js --port 8082 \
+            --lottie_player /usr/local/lib/node_modules/lottie-web/build/player/lottie.min.js \
+            --in_docker \
+            --post_to http://localhost:8081/report_gold_data \
+            --input {} \;
+
+
+# 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/lottiecap/gold/lottie-web-aggregator.go b/infra/lottiecap/gold/lottie-web-aggregator.go
new file mode 100644
index 0000000..f05672c
--- /dev/null
+++ b/infra/lottiecap/gold/lottie-web-aggregator.go
@@ -0,0 +1,235 @@
+// 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 alongside lottiecap.js and istens for POST requests
+// when any test case reports it has output for Gold.
+
+// TODO(kjlubick): Deduplicate with pathkit-aggregator
+// TODO(kjlubick): Handle uninteresting_hash.txt if needed.
+
+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"
+)
+
+// This allows us to use upload_dm_results.py out of the box
+const JSON_FILENAME = "dm.json"
+
+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'")
+	renderer      = flag.String("renderer", "lottie-web", "e.g. lottie-web or skottie")
+	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")
+)
+
+// reportBody is the JSON recieved from the JS side. It represents
+// exactly one unique Gold image/test.
+type reportBody struct {
+	// 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{
+		"browser":          *browser,
+		"renderer":         *renderer,
+		"configuration":    *config,
+		"cpu_or_gpu":       "CPU",
+		"cpu_or_gpu_value": "Browser",
+		"os":               *hostOS,
+		"source_type":      "lottie",
+	}
+
+	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,
+		},
+		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")
+}
+
+// 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
+}
diff --git a/tools/lottiecap/.gitignore b/tools/lottiecap/.gitignore
new file mode 100644
index 0000000..d8b83df
--- /dev/null
+++ b/tools/lottiecap/.gitignore
@@ -0,0 +1 @@
+package-lock.json
diff --git a/tools/lottiecap/driver.html b/tools/lottiecap/driver.html
index 381eeef..a562904 100644
--- a/tools/lottiecap/driver.html
+++ b/tools/lottiecap/driver.html
@@ -32,6 +32,12 @@
       const TARGET_SIZE = 1000; // Image size in pixels both x and y direction.
       const PATH = '/lottie.json';
 
+      let renderer = 'svg';
+      let hash = window.location.hash;
+      if (hash) {
+        renderer = hash.slice(1);
+      }
+
       // This global is used by puppeteer to determine if all tiles have finished drawing.
       window._tileCount = 0;
 
@@ -40,7 +46,7 @@
       // the filmstrip should look like.
       let anim = lottie.loadAnimation({
         container: document.querySelector('.anim'),
-        renderer: 'svg',
+        renderer: renderer,
         loop: false,
         autoplay: true,
         path: PATH,
@@ -52,6 +58,7 @@
       anim.addEventListener('data_ready', (e) => {
         // Once the first tile is loaded, calculate what
         // the filmstrip should look like.
+        let animationData = anim.animationData;
         let width = anim.animationData.w;
         let height = anim.animationData.h;
         let scale = TARGET_SIZE / (TILE_COUNT * Math.max(width, height));
@@ -64,12 +71,6 @@
 
         let main = document.querySelector('main');
 
-        let renderer = 'svg';
-        let hash = window.location.hash;
-        if (hash) {
-          renderer = hash.slice(1);
-        }
-
         // Clear out the first div now that our measurements are done.
         main.firstElementChild.remove();
 
@@ -83,23 +84,27 @@
 
           let frameStop = i * frameStep;
 
+          // create a new animation for each tile. It is tempting to try having
+          // one animation and "clone" each frame, but that doesn't work
+          // because of how bodymovin cleans up the URLObjects that are the path
+          // data for the svgs.
+          // We can re-use the animationData to avoid having to hit the
+          // (local) network a bunch of times.
           let anim = lottie.loadAnimation({
             container: div,
             renderer: renderer,
             loop: false,
             autoplay: false,
-            path: PATH,
+            animationData: animationData,
             rendererSettings: {
               preserveAspectRatio:'xMidYMid meet'
             },
           });
 
-          anim.addEventListener('data_ready', (e) => {
-            console.log(frameStop*1000);
-            // Once data is loaded, jump to the right frame.
-            anim.goToAndStop(frameStop * anim.frameRate, true);
-            window._tileCount += 1;
-          });
+          // don't need to wait for data_ready because it's instantly ready.
+          console.log(frameStop*1000);
+          anim.goToAndStop(frameStop * anim.frameRate, true);
+          window._tileCount += 1;
         }
       });
     })();
diff --git a/tools/lottiecap/lottiecap.js b/tools/lottiecap/lottiecap.js
index 74d1e75..ba8123a 100644
--- a/tools/lottiecap/lottiecap.js
+++ b/tools/lottiecap/lottiecap.js
@@ -8,6 +8,7 @@
 const fs = require('fs');
 const commandLineArgs = require('command-line-args');
 const commandLineUsage= require('command-line-usage');
+const fetch = require('node-fetch');
 
 // Valid values for the --renderer flag.
 const RENDERERS = ['svg', 'canvas'];
@@ -34,6 +35,27 @@
     type: Number,
   },
   {
+    name: 'lottie_player',
+    description: 'The path to lottie.min.js, defaults to a local npm install location.',
+    type: String,
+  },
+  {
+    name: 'post_to',
+    description: 'If set, the url to post results to for Gold Ingestion.',
+    type: String,
+  },
+  {
+    name: 'in_docker',
+    description: 'Is this being run in docker, defaults to false',
+    type: Boolean,
+  },
+  {
+    name: 'skip_automation',
+    description: 'If the automation of the screenshot taking should be skipped ' +
+                 '(e.g. debugging). Defaults to false.',
+    type: Boolean,
+  },
+  {
     name: 'help',
     alias: 'h',
     type: Boolean,
@@ -63,6 +85,9 @@
 if (!options.port) {
   options.port = 8081;
 }
+if (!options.lottie_player) {
+  options.lottie_player = 'node_modules/lottie-web/build/player/lottie.min.js';
+}
 
 if (options.help) {
   console.log(commandLineUsage(usage));
@@ -86,7 +111,7 @@
 }
 
 // Start up a web server to serve the three files we need.
-let lottieJS = fs.readFileSync('node_modules/lottie-web/build/player/lottie.min.js', 'utf8');
+let lottieJS = fs.readFileSync(options.lottie_player, 'utf8');
 let driverHTML = fs.readFileSync('driver.html', 'utf8');
 let lottieJSON = fs.readFileSync(options.input, 'utf8');
 
@@ -102,28 +127,92 @@
     return ms;
 }
 
+const targetURL = `http://localhost:${options.port}/#${options.renderer}`;
+
 // Drive chrome to load the web page from the server we have running.
 async function driveBrowser() {
   console.log('- Launching chrome in headless mode.');
-  const browser = await puppeteer.launch();
+  let browser = null;
+  if (options.in_docker) {
+    browser = await puppeteer.launch({
+      'executablePath': '/usr/bin/google-chrome-stable',
+      'args': ['--no-sandbox'],
+    });
+  } else {
+    browser = await puppeteer.launch();
+  }
+
   const page = await browser.newPage();
-  console.log('- Loading our Lottie exercising page.');
-  await page.goto('http://localhost:' + options.port + '/' + '#' + options.renderer, {waitUntil: 'networkidle2'});
-  console.log('- Waiting for all the tiles to be drawn.');
-  await page.waitForFunction('window._tileCount === 25');
+  console.log(`- Loading our Lottie exercising page for ${options.input}.`);
+  try {
+     // 20 seconds is plenty of time to wait for the json to be loaded once
+     // This usually times out for super large json.
+    await page.goto(targetURL, {
+      timeout: 20000,
+      waitUntil: 'networkidle0'
+    });
+    // 20 seconds is plenty of time to wait for the frames to be drawn.
+    // This usually times out for json that causes errors in the player.
+    console.log('- Waiting 15s for all the tiles to be drawn.');
+    await page.waitForFunction('window._tileCount === 25', {
+      timeout: 20000,
+    });
+  } catch(e) {
+    console.log('Timed out while loading or drawing. Either the JSON file was ' +
+                'too big or hit a bug in the player.', e);
+    await browser.close();
+    process.exit(0);
+  }
+
   console.log('- Taking screenshot.');
-  await page.screenshot({
+  let encoding = 'binary';
+  if (options.post_to) {
+    encoding = 'base64';
+    // prevent writing the image to disk
+    options.output = '';
+  }
+
+  // See https://github.com/GoogleChrome/puppeteer/blob/v1.6.0/docs/api.md#pagescreenshotoptions
+  let result = await page.screenshot({
     path: options.output,
+    type: 'png',
     clip: {
       x: 0,
       y: 0,
       width: 1000,
       height: 1000,
     },
+    encoding: encoding,
   });
+
+  if (options.post_to) {
+    console.log(`- Reporting ${options.input} to Gold server ${options.post_to}`);
+    let shortenedName = options.input;
+    let lastSlash = shortenedName.lastIndexOf('/');
+    if (lastSlash !== -1) {
+      shortenedName = shortenedName.slice(lastSlash+1);
+    }
+    await fetch(options.post_to, {
+        method: 'POST',
+        mode: 'no-cors',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+            'data': result,
+            'test_name': shortenedName,
+        })
+    });
+  }
+
   await browser.close();
   // Need to call exit() because the web server is still running.
   process.exit(0);
 }
 
-driveBrowser();
+if (!options.skip_automation) {
+  driveBrowser();
+} else {
+  console.log(`open ${targetURL} to see the animation.`)
+}
+
diff --git a/tools/lottiecap/package-lock.json b/tools/lottiecap/package-lock.json
deleted file mode 100644
index ec7a582..0000000
--- a/tools/lottiecap/package-lock.json
+++ /dev/null
@@ -1,785 +0,0 @@
-{
-  "requires": true,
-  "lockfileVersion": 1,
-  "dependencies": {
-    "accepts": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
-      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
-      "requires": {
-        "mime-types": "2.1.18",
-        "negotiator": "0.6.1"
-      }
-    },
-    "agent-base": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
-      "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
-      "requires": {
-        "es6-promisify": "5.0.0"
-      }
-    },
-    "ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "requires": {
-        "color-convert": "1.9.1"
-      }
-    },
-    "argv-tools": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/argv-tools/-/argv-tools-0.1.1.tgz",
-      "integrity": "sha512-Cc0dBvx4dvrjjKpyDA6w8RlNAw8Su30NvZbWl/Tv9ZALEVlLVkWQiHMi84Q0xNfpVuSaiQbYkdmWK8g1PLGhKw==",
-      "requires": {
-        "array-back": "2.0.0",
-        "find-replace": "2.0.1"
-      }
-    },
-    "array-back": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz",
-      "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==",
-      "requires": {
-        "typical": "2.6.1"
-      }
-    },
-    "array-flatten": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
-    },
-    "async-limiter": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
-      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
-    },
-    "balanced-match": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-    },
-    "body-parser": {
-      "version": "1.18.2",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
-      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
-      "requires": {
-        "bytes": "3.0.0",
-        "content-type": "1.0.4",
-        "debug": "2.6.9",
-        "depd": "1.1.2",
-        "http-errors": "1.6.2",
-        "iconv-lite": "0.4.19",
-        "on-finished": "2.3.0",
-        "qs": "6.5.1",
-        "raw-body": "2.3.2",
-        "type-is": "1.6.16"
-      }
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "requires": {
-        "balanced-match": "1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "bytes": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
-      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
-    },
-    "chalk": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
-      "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
-      "requires": {
-        "ansi-styles": "3.2.1",
-        "escape-string-regexp": "1.0.5",
-        "supports-color": "5.3.0"
-      }
-    },
-    "color-convert": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
-      "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
-      "requires": {
-        "color-name": "1.1.3"
-      }
-    },
-    "color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
-    },
-    "command-line-args": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.0.2.tgz",
-      "integrity": "sha512-/qPcbL8zpqg53x4rAaqMFlRV4opN3pbla7I7k9x8kyOBMQoGT6WltjN6sXZuxOXw6DgdK7Ad+ijYS5gjcr7vlA==",
-      "requires": {
-        "argv-tools": "0.1.1",
-        "array-back": "2.0.0",
-        "find-replace": "2.0.1",
-        "lodash.camelcase": "4.3.0",
-        "typical": "2.6.1"
-      }
-    },
-    "command-line-usage": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-5.0.3.tgz",
-      "integrity": "sha512-5dlmACnO31ervCzlnGq6SR4ht4cnax20t14zDkgYE/9C8L0w5xFmJIGyd0oMPLscgTKIgrT4zySu47akbz/uTA==",
-      "requires": {
-        "array-back": "2.0.0",
-        "chalk": "2.3.2",
-        "table-layout": "0.4.3",
-        "typical": "2.6.1"
-      }
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
-    },
-    "concat-stream": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
-      "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
-      "requires": {
-        "inherits": "2.0.3",
-        "readable-stream": "2.3.5",
-        "typedarray": "0.0.6"
-      }
-    },
-    "content-disposition": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
-      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
-    },
-    "content-type": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
-      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
-    },
-    "cookie": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
-      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
-    },
-    "cookie-signature": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
-    },
-    "core-util-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-    },
-    "debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "requires": {
-        "ms": "2.0.0"
-      }
-    },
-    "deep-extend": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.0.tgz",
-      "integrity": "sha1-bvSgmwX5iw41jW2T1Mo8rsZnKAM="
-    },
-    "depd": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
-      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
-    },
-    "destroy": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
-      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
-    },
-    "ee-first": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
-      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
-    },
-    "encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
-    },
-    "es6-promise": {
-      "version": "4.2.4",
-      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
-      "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
-    },
-    "es6-promisify": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
-      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
-      "requires": {
-        "es6-promise": "4.2.4"
-      }
-    },
-    "escape-html": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
-      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
-    },
-    "escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
-    },
-    "etag": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
-      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
-    },
-    "express": {
-      "version": "4.16.3",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
-      "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
-      "requires": {
-        "accepts": "1.3.5",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.18.2",
-        "content-disposition": "0.5.2",
-        "content-type": "1.0.4",
-        "cookie": "0.3.1",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "1.1.2",
-        "encodeurl": "1.0.2",
-        "escape-html": "1.0.3",
-        "etag": "1.8.1",
-        "finalhandler": "1.1.1",
-        "fresh": "0.5.2",
-        "merge-descriptors": "1.0.1",
-        "methods": "1.1.2",
-        "on-finished": "2.3.0",
-        "parseurl": "1.3.2",
-        "path-to-regexp": "0.1.7",
-        "proxy-addr": "2.0.3",
-        "qs": "6.5.1",
-        "range-parser": "1.2.0",
-        "safe-buffer": "5.1.1",
-        "send": "0.16.2",
-        "serve-static": "1.13.2",
-        "setprototypeof": "1.1.0",
-        "statuses": "1.4.0",
-        "type-is": "1.6.16",
-        "utils-merge": "1.0.1",
-        "vary": "1.1.2"
-      }
-    },
-    "extract-zip": {
-      "version": "1.6.6",
-      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz",
-      "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=",
-      "requires": {
-        "concat-stream": "1.6.0",
-        "debug": "2.6.9",
-        "mkdirp": "0.5.0",
-        "yauzl": "2.4.1"
-      }
-    },
-    "fd-slicer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
-      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
-      "requires": {
-        "pend": "1.2.0"
-      }
-    },
-    "finalhandler": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
-      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
-      "requires": {
-        "debug": "2.6.9",
-        "encodeurl": "1.0.2",
-        "escape-html": "1.0.3",
-        "on-finished": "2.3.0",
-        "parseurl": "1.3.2",
-        "statuses": "1.4.0",
-        "unpipe": "1.0.0"
-      }
-    },
-    "find-replace": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-2.0.1.tgz",
-      "integrity": "sha512-LzDo3Fpa30FLIBsh6DCDnMN1KW2g4QKkqKmejlImgWY67dDFPX/x9Kh/op/GK522DchQXEvDi/wD48HKW49XOQ==",
-      "requires": {
-        "array-back": "2.0.0",
-        "test-value": "3.0.0"
-      }
-    },
-    "forwarded": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
-      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
-    },
-    "fresh": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
-      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
-    },
-    "glob": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
-      "requires": {
-        "fs.realpath": "1.0.0",
-        "inflight": "1.0.6",
-        "inherits": "2.0.3",
-        "minimatch": "3.0.4",
-        "once": "1.4.0",
-        "path-is-absolute": "1.0.1"
-      }
-    },
-    "has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
-    },
-    "http-errors": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
-      "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
-      "requires": {
-        "depd": "1.1.1",
-        "inherits": "2.0.3",
-        "setprototypeof": "1.0.3",
-        "statuses": "1.4.0"
-      },
-      "dependencies": {
-        "depd": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
-          "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
-        },
-        "setprototypeof": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
-          "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
-        }
-      }
-    },
-    "https-proxy-agent": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.0.tgz",
-      "integrity": "sha512-uUWcfXHvy/dwfM9bqa6AozvAjS32dZSTUYd/4SEpYKRg6LEcPLshksnQYRudM9AyNvUARMfAg5TLjUDyX/K4vA==",
-      "requires": {
-        "agent-base": "4.2.0",
-        "debug": "3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        }
-      }
-    },
-    "iconv-lite": {
-      "version": "0.4.19",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
-      "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "requires": {
-        "once": "1.4.0",
-        "wrappy": "1.0.2"
-      }
-    },
-    "inherits": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
-      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
-    },
-    "ipaddr.js": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
-      "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
-    },
-    "isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
-    },
-    "lodash.camelcase": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
-    },
-    "lodash.padend": {
-      "version": "4.6.1",
-      "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz",
-      "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4="
-    },
-    "lottie-web": {
-      "version": "5.1.8",
-      "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.1.8.tgz",
-      "integrity": "sha512-/Sr5yuR1hRW3S0be/TOF6XFn5rLAZj6ADOh4GRed/2gEGAggdj6aNqFaU3YGfTi5iwOhHGky3uG3qm/ra0C9Hg=="
-    },
-    "media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
-    },
-    "merge-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
-      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
-    },
-    "methods": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
-      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
-    },
-    "mime": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
-      "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
-    },
-    "mime-db": {
-      "version": "1.33.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
-      "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
-    },
-    "mime-types": {
-      "version": "2.1.18",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
-      "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
-      "requires": {
-        "mime-db": "1.33.0"
-      }
-    },
-    "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "requires": {
-        "brace-expansion": "1.1.11"
-      }
-    },
-    "minimist": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
-    },
-    "mkdirp": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
-      "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
-      "requires": {
-        "minimist": "0.0.8"
-      }
-    },
-    "ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-    },
-    "negotiator": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
-      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
-    },
-    "on-finished": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
-      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
-      "requires": {
-        "ee-first": "1.1.1"
-      }
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "requires": {
-        "wrappy": "1.0.2"
-      }
-    },
-    "parseurl": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
-      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
-    },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
-    },
-    "path-to-regexp": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
-      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
-    },
-    "pend": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
-      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
-    },
-    "process-nextick-args": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
-      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
-    },
-    "progress": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
-      "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8="
-    },
-    "proxy-addr": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
-      "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
-      "requires": {
-        "forwarded": "0.1.2",
-        "ipaddr.js": "1.6.0"
-      }
-    },
-    "proxy-from-env": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
-      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
-    },
-    "puppeteer": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.1.1.tgz",
-      "integrity": "sha1-rb8l5J9e8DRDwQq44JqVTKDHv+4=",
-      "requires": {
-        "debug": "2.6.9",
-        "extract-zip": "1.6.6",
-        "https-proxy-agent": "2.2.0",
-        "mime": "1.4.1",
-        "progress": "2.0.0",
-        "proxy-from-env": "1.0.0",
-        "rimraf": "2.6.2",
-        "ws": "3.3.3"
-      }
-    },
-    "qs": {
-      "version": "6.5.1",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
-      "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
-    },
-    "range-parser": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
-      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
-    },
-    "raw-body": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
-      "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
-      "requires": {
-        "bytes": "3.0.0",
-        "http-errors": "1.6.2",
-        "iconv-lite": "0.4.19",
-        "unpipe": "1.0.0"
-      }
-    },
-    "readable-stream": {
-      "version": "2.3.5",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
-      "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
-      "requires": {
-        "core-util-is": "1.0.2",
-        "inherits": "2.0.3",
-        "isarray": "1.0.0",
-        "process-nextick-args": "2.0.0",
-        "safe-buffer": "5.1.1",
-        "string_decoder": "1.0.3",
-        "util-deprecate": "1.0.2"
-      }
-    },
-    "reduce-flatten": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz",
-      "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc="
-    },
-    "rimraf": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
-      "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
-      "requires": {
-        "glob": "7.1.2"
-      }
-    },
-    "safe-buffer": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
-      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
-    },
-    "send": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
-      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
-      "requires": {
-        "debug": "2.6.9",
-        "depd": "1.1.2",
-        "destroy": "1.0.4",
-        "encodeurl": "1.0.2",
-        "escape-html": "1.0.3",
-        "etag": "1.8.1",
-        "fresh": "0.5.2",
-        "http-errors": "1.6.2",
-        "mime": "1.4.1",
-        "ms": "2.0.0",
-        "on-finished": "2.3.0",
-        "range-parser": "1.2.0",
-        "statuses": "1.4.0"
-      }
-    },
-    "serve-static": {
-      "version": "1.13.2",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
-      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
-      "requires": {
-        "encodeurl": "1.0.2",
-        "escape-html": "1.0.3",
-        "parseurl": "1.3.2",
-        "send": "0.16.2"
-      }
-    },
-    "setprototypeof": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
-      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
-    },
-    "statuses": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
-      "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
-    },
-    "string_decoder": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-      "requires": {
-        "safe-buffer": "5.1.1"
-      }
-    },
-    "supports-color": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
-      "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
-      "requires": {
-        "has-flag": "3.0.0"
-      }
-    },
-    "table-layout": {
-      "version": "0.4.3",
-      "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.3.tgz",
-      "integrity": "sha512-MIhflPM38ejKrFwWwC3P9x3eHvMo5G5AmNo29Qtz2HpBl5KD2GCcmOErjgNtUQLv/qaqVDagfJY3rJLPDvEgLg==",
-      "requires": {
-        "array-back": "2.0.0",
-        "deep-extend": "0.5.0",
-        "lodash.padend": "4.6.1",
-        "typical": "2.6.1",
-        "wordwrapjs": "3.0.0"
-      }
-    },
-    "test-value": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/test-value/-/test-value-3.0.0.tgz",
-      "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==",
-      "requires": {
-        "array-back": "2.0.0",
-        "typical": "2.6.1"
-      }
-    },
-    "type-is": {
-      "version": "1.6.16",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
-      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
-      "requires": {
-        "media-typer": "0.3.0",
-        "mime-types": "2.1.18"
-      }
-    },
-    "typedarray": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
-    },
-    "typical": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz",
-      "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0="
-    },
-    "ultron": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
-      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
-    },
-    "unpipe": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
-      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
-    },
-    "util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
-    },
-    "utils-merge": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
-      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
-    },
-    "vary": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
-      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
-    },
-    "wordwrapjs": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz",
-      "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==",
-      "requires": {
-        "reduce-flatten": "1.0.1",
-        "typical": "2.6.1"
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-    },
-    "ws": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
-      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
-      "requires": {
-        "async-limiter": "1.0.0",
-        "safe-buffer": "5.1.1",
-        "ultron": "1.1.1"
-      }
-    },
-    "yauzl": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
-      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
-      "requires": {
-        "fd-slicer": "1.0.1"
-      }
-    }
-  }
-}
diff --git a/tools/lottiecap/package.json b/tools/lottiecap/package.json
index f27498b..6bce532 100644
--- a/tools/lottiecap/package.json
+++ b/tools/lottiecap/package.json
@@ -3,7 +3,8 @@
     "command-line-args": "^5.0.2",
     "command-line-usage": "^5.0.3",
     "express": "^4.16.3",
-    "lottie-web": "^5.1.8",
-    "puppeteer": "^1.1.1"
+    "lottie-web": "5.2.1",
+    "node-fetch": "^2.2.0",
+    "puppeteer": "~1.6.2"
   }
 }