Add ability to output ImageBaseGSUrl to render_picture and use in rebaseline server.

BUG=skia:2230
R=epoger@google.com

Author: rmistry@google.com

Review URL: https://codereview.chromium.org/479613002
diff --git a/gm/gm_json.py b/gm/gm_json.py
index 1a53997..109bba3 100644
--- a/gm/gm_json.py
+++ b/gm/gm_json.py
@@ -94,6 +94,7 @@
 JSONKEY_IMAGE_FILEPATH = 'filepath'
 JSONKEY_SOURCE_TILEDIMAGES = 'tiled-images'
 JSONKEY_SOURCE_WHOLEIMAGE = 'whole-image'
+JSONKEY_IMAGE_BASE_GS_URL = 'image-base-gs-url'
 
 
 # Root directory where the buildbots store their actually-generated images...
diff --git a/gm/rebaseline_server/compare_configs.py b/gm/rebaseline_server/compare_configs.py
index 73a5570..36c7f86 100755
--- a/gm/rebaseline_server/compare_configs.py
+++ b/gm/rebaseline_server/compare_configs.py
@@ -151,7 +151,8 @@
             try:
               image_pair = imagepair.ImagePair(
                   image_diff_db=self._image_diff_db,
-                  base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
+                  imageA_base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
+                  imageB_base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
                   imageA_relative_url=configA_image_relative_url,
                   imageB_relative_url=configB_image_relative_url,
                   extra_columns=extra_columns_dict)
diff --git a/gm/rebaseline_server/compare_rendered_pictures.py b/gm/rebaseline_server/compare_rendered_pictures.py
index 399117d..73cb36b 100755
--- a/gm/rebaseline_server/compare_rendered_pictures.py
+++ b/gm/rebaseline_server/compare_rendered_pictures.py
@@ -268,18 +268,25 @@
       self._validate_dict_version(dictB)
       dictB_results = self.get_default(dictB, {}, setB_section)
 
+      image_A_base_url = self.get_default(
+          setA_dicts, self._image_base_gs_url, dict_path,
+          gm_json.JSONKEY_IMAGE_BASE_GS_URL)
+      image_B_base_url = self.get_default(
+          setB_dicts, self._image_base_gs_url, dict_path,
+          gm_json.JSONKEY_IMAGE_BASE_GS_URL)
+
       # get the builders and render modes for each set
-      builder_A     = self.get_default(dictA, None, 
-                        gm_json.JSONKEY_DESCRIPTIONS, 
+      builder_A     = self.get_default(dictA, None,
+                        gm_json.JSONKEY_DESCRIPTIONS,
                         gm_json.JSONKEY_DESCRIPTIONS_BUILDER)
-      render_mode_A = self.get_default(dictA, None, 
-                        gm_json.JSONKEY_DESCRIPTIONS, 
+      render_mode_A = self.get_default(dictA, None,
+                        gm_json.JSONKEY_DESCRIPTIONS,
                         gm_json.JSONKEY_DESCRIPTIONS_RENDER_MODE)
-      builder_B     = self.get_default(dictB, None, 
-                        gm_json.JSONKEY_DESCRIPTIONS, 
+      builder_B     = self.get_default(dictB, None,
+                        gm_json.JSONKEY_DESCRIPTIONS,
                         gm_json.JSONKEY_DESCRIPTIONS_BUILDER)
-      render_mode_B = self.get_default(dictB, None, 
-                        gm_json.JSONKEY_DESCRIPTIONS, 
+      render_mode_B = self.get_default(dictB, None,
+                        gm_json.JSONKEY_DESCRIPTIONS,
                         gm_json.JSONKEY_DESCRIPTIONS_RENDER_MODE)
 
       skp_names = sorted(set(dictA_results.keys() + dictB_results.keys()))
@@ -295,8 +302,11 @@
         whole_image_B = self.get_default(
             dictB_results, None,
             skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
+
         imagepairs_for_this_skp.append(self._create_image_pair(
             image_dict_A=whole_image_A, image_dict_B=whole_image_B,
+            image_A_base_url=image_A_base_url,
+            image_B_base_url=image_B_base_url,
             builder_A=builder_A, render_mode_A=render_mode_A,
             builder_B=builder_B, render_mode_B=render_mode_B,
             source_json_file=dict_path,
@@ -318,6 +328,8 @@
                               if tile_num < num_tiles_A else None),
                 image_dict_B=(tiled_images_B[tile_num]
                               if tile_num < num_tiles_B else None),
+                image_A_base_url=image_A_base_url,
+                image_B_base_url=image_B_base_url,
                 builder_A=builder_A, render_mode_A=render_mode_A,
                 builder_B=builder_B, render_mode_B=render_mode_B,
                 source_json_file=dict_path,
@@ -368,8 +380,9 @@
       raise Exception('expected header_revision %d, but got %d' % (
           expected_header_revision, header_revision))
 
-  def _create_image_pair(self, image_dict_A, image_dict_B, 
-                         builder_A, render_mode_A, 
+  def _create_image_pair(self, image_dict_A, image_dict_B,
+                         image_A_base_url, image_B_base_url,
+                         builder_A, render_mode_A,
                          builder_B, render_mode_B,
                          source_json_file,
                          source_skp_name, tilenum):
@@ -378,11 +391,13 @@
     Args:
       image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image
       image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image
+      image_A_base_url: base URL for image A
+      image_B_base_url: base URL for image B
       builder_A: builder that created image set A or None if unknow
-      render_mode_A: render mode used to generate image set A or None if 
+      render_mode_A: render mode used to generate image set A or None if
                      unknown.
       builder_B: builder that created image set A or None if unknow
-      render_mode_B: render mode used to generate image set A or None if 
+      render_mode_B: render mode used to generate image set A or None if
                      unknown.
       source_json_file: string; relative path of the JSON file where this
                         result came from, within setA and setB.
@@ -436,7 +451,8 @@
     try:
       return imagepair.ImagePair(
           image_diff_db=self._image_diff_db,
-          base_url=self._image_base_gs_url,
+          imageA_base_url=image_A_base_url,
+          imageB_base_url=image_B_base_url,
           imageA_relative_url=imageA_relative_url,
           imageB_relative_url=imageB_relative_url,
           extra_columns=extra_columns_dict,
diff --git a/gm/rebaseline_server/compare_rendered_pictures_test.py b/gm/rebaseline_server/compare_rendered_pictures_test.py
index c50c1a0..2b15462 100755
--- a/gm/rebaseline_server/compare_rendered_pictures_test.py
+++ b/gm/rebaseline_server/compare_rendered_pictures_test.py
@@ -32,6 +32,7 @@
 import find_run_binary
 import gm_json
 import imagediffdb
+import imagepairset
 import results
 
 
@@ -78,6 +79,64 @@
             results.KEY__HEADER__RESULTS_ALL),
         os.path.join(self.output_dir_actual, 'compare_rendered_pictures.json'))
 
+  def test_endToEnd_withImageBaseGSUrl(self):
+    """Generate two sets of SKPs, run render_pictures over both, and compare
+    the results."""
+    setA_subdir = 'before_patch'
+    setB_subdir = 'after_patch'
+    imageA_gs_base = 'superman/kent-camera/pictures'
+    imageB_gs_base = 'batman/batarang/pictures'
+    self._generate_skps_and_run_render_pictures(
+        subdir=setA_subdir, skpdict={
+            'changed.skp': 200,
+            'unchanged.skp': 100,
+            'only-in-before.skp': 128,
+        },
+        image_base_gs_url='gs://%s' % imageA_gs_base)
+    self._generate_skps_and_run_render_pictures(
+        subdir=setB_subdir, skpdict={
+            'changed.skp': 201,
+            'unchanged.skp': 100,
+            'only-in-after.skp': 128,
+        },
+        image_base_gs_url='gs://%s' % imageB_gs_base)
+
+    results_obj = compare_rendered_pictures.RenderedPicturesComparisons(
+        setA_dir=os.path.join(self.temp_dir, setA_subdir),
+        setB_dir=os.path.join(self.temp_dir, setB_subdir),
+        setA_section=gm_json.JSONKEY_ACTUALRESULTS,
+        setB_section=gm_json.JSONKEY_ACTUALRESULTS,
+        image_diff_db=imagediffdb.ImageDiffDB(self.temp_dir),
+        image_base_gs_url='gs://fakebucket/fake/path',
+        diff_base_url='/static/generated-images')
+    results_obj.get_timestamp = mock_get_timestamp
+
+    output_dict = results_obj.get_packaged_results_of_type(
+        results.KEY__HEADER__RESULTS_ALL)
+    # Assert that the baseURLs are as expected.
+    self.assertEquals(
+        output_dict[imagepairset.KEY__ROOT__IMAGESETS]
+                   [imagepairset.KEY__IMAGESETS__SET__IMAGE_A]
+                   [imagepairset.KEY__IMAGESETS__FIELD__BASE_URL],
+        'http://storage.cloud.google.com/%s' % imageA_gs_base)
+    self.assertEquals(
+        output_dict[imagepairset.KEY__ROOT__IMAGESETS]
+                   [imagepairset.KEY__IMAGESETS__SET__IMAGE_B]
+                   [imagepairset.KEY__IMAGESETS__FIELD__BASE_URL],
+        'http://storage.cloud.google.com/%s' % imageB_gs_base)
+    # Overwrite elements within the results that change from one test run
+    # to the next.
+    # pylint: disable=W0212
+    results_obj._setA_descriptions[results.KEY__SET_DESCRIPTIONS__DIR] = [
+        'before-patch-fake-dir']
+    results_obj._setB_descriptions[results.KEY__SET_DESCRIPTIONS__DIR] = [
+        'after-patch-fake-dir']
+
+    gm_json.WriteToFile(
+        output_dict,
+        os.path.join(self.output_dir_actual,
+                               'compare_rendered_pictures.json'))
+
   def test_repo_url(self):
     """Use repo: URL to specify summary files."""
     base_repo_url = 'repo:gm/rebaseline_server/testdata/inputs/skp-summaries'
@@ -104,7 +163,8 @@
             results.KEY__HEADER__RESULTS_ALL),
         os.path.join(self.output_dir_actual, 'compare_rendered_pictures.json'))
 
-  def _generate_skps_and_run_render_pictures(self, subdir, skpdict):
+  def _generate_skps_and_run_render_pictures(self, subdir, skpdict,
+                                             image_base_gs_url=None):
     """Generate SKPs and run render_pictures on them.
 
     Args:
@@ -121,13 +181,16 @@
     # and fix its result!  (imageURLs within whole-image entries are wrong when
     # I tried adding that)
     binary = find_run_binary.find_path_to_program('render_pictures')
-    return subprocess.check_output([
+    render_pictures_cmd = [
         binary,
         '--config', '8888',
         '-r', out_path,
         '--writeChecksumBasedFilenames',
         '--writeJsonSummaryPath', os.path.join(out_path, 'summary.json'),
-        '--writePath', out_path])
+        '--writePath', out_path]
+    if image_base_gs_url:
+      render_pictures_cmd.extend(['--imageBaseGSUrl', image_base_gs_url])
+    return subprocess.check_output(render_pictures_cmd)
 
   def _run_skpmaker(self, output_path, red=0, green=0, blue=0,
                     width=640, height=400):
diff --git a/gm/rebaseline_server/compare_to_expectations.py b/gm/rebaseline_server/compare_to_expectations.py
index a93d9b9..303294c 100755
--- a/gm/rebaseline_server/compare_to_expectations.py
+++ b/gm/rebaseline_server/compare_to_expectations.py
@@ -347,7 +347,8 @@
           try:
             image_pair = imagepair.ImagePair(
                 image_diff_db=self._image_diff_db,
-                base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
+                imageA_base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
+                imageB_base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
                 imageA_relative_url=expected_image_relative_url,
                 imageB_relative_url=actual_image_relative_url,
                 expectations=expectations_dict,
diff --git a/gm/rebaseline_server/imagepair.py b/gm/rebaseline_server/imagepair.py
index 0ac0c42..e85c219 100644
--- a/gm/rebaseline_server/imagepair.py
+++ b/gm/rebaseline_server/imagepair.py
@@ -32,17 +32,19 @@
   """
 
   def __init__(self, image_diff_db,
-               base_url, imageA_relative_url, imageB_relative_url,
+               imageA_base_url, imageB_base_url,
+               imageA_relative_url, imageB_relative_url,
                expectations=None, extra_columns=None, source_json_file=None,
                download_all_images=False):
     """
     Args:
       image_diff_db: ImageDiffDB instance we use to generate/store image diffs
-      base_url: base of all image URLs
+      imageA_base_url: string; base URL for image A
+      imageB_base_url: string; base URL for image B
       imageA_relative_url: string; URL pointing at an image, relative to
-          base_url; or None, if this image is missing
+          imageA_base_url; or None, if this image is missing
       imageB_relative_url: string; URL pointing at an image, relative to
-          base_url; or None, if this image is missing
+          imageB_base_url; or None, if this image is missing
       expectations: optional dictionary containing expectations-specific
           metadata (ignore-failure, bug numbers, etc.)
       extra_columns: optional dictionary containing more metadata (test name,
@@ -55,7 +57,8 @@
           (imageA == imageB, or one of them is missing)
     """
     self._image_diff_db = image_diff_db
-    self.base_url = base_url
+    self.imageA_base_url = imageA_base_url
+    self.imageB_base_url = imageB_base_url
     self.imageA_relative_url = imageA_relative_url
     self.imageB_relative_url = imageB_relative_url
     self.expectations_dict = expectations
@@ -76,9 +79,11 @@
     if self._diff_record != None or download_all_images:
       image_diff_db.add_image_pair(
           expected_image_locator=imageA_relative_url,
-          expected_image_url=self.posixpath_join(base_url, imageA_relative_url),
+          expected_image_url=self.posixpath_join(imageA_base_url,
+                                                 imageA_relative_url),
           actual_image_locator=imageB_relative_url,
-          actual_image_url=self.posixpath_join(base_url, imageB_relative_url))
+          actual_image_url=self.posixpath_join(imageB_base_url,
+                                               imageB_relative_url))
 
   def as_dict(self):
     """Returns a dictionary describing this ImagePair.
diff --git a/gm/rebaseline_server/imagepair_test.py b/gm/rebaseline_server/imagepair_test.py
index c82e4b5..773f6a3 100755
--- a/gm/rebaseline_server/imagepair_test.py
+++ b/gm/rebaseline_server/imagepair_test.py
@@ -91,7 +91,7 @@
                     'perceptualDifference': 0.06620300000000157,
                     'diffUrl': 'arcofzorro_16206093933823793653_png_png-vs-' +
                         'arcofzorro_13786535001616823825_png_png.png',
-                    'whiteDiffUrl': 'arcofzorro_16206093933823793653_png_png' + 
+                    'whiteDiffUrl': 'arcofzorro_16206093933823793653_png_png' +
                         '-vs-arcofzorro_13786535001616823825_png_png.png',
                 },
                 'imageAUrl': 'arcofzorro/16206093933823793653.png',
@@ -195,7 +195,8 @@
     for selftest in selftests:
       image_pair = imagepair.ImagePair(
           image_diff_db=db,
-          base_url=IMG_URL_BASE,
+          imageA_base_url=IMG_URL_BASE,
+          imageB_base_url=IMG_URL_BASE,
           imageA_relative_url=selftest[0],
           imageB_relative_url=selftest[1],
           expectations=selftest[2],
diff --git a/gm/rebaseline_server/imagepairset.py b/gm/rebaseline_server/imagepairset.py
index a6101b9..b492d9f 100644
--- a/gm/rebaseline_server/imagepairset.py
+++ b/gm/rebaseline_server/imagepairset.py
@@ -57,7 +57,8 @@
     self._descriptions = descriptions or DEFAULT_DESCRIPTIONS
     self._extra_column_tallies = {}  # maps column_id -> values
                                      #                -> instances_per_value
-    self._image_base_url = None
+    self._imageA_base_url = None
+    self._imageB_base_url = None
     self._diff_base_url = diff_base_url
 
     # We build self._image_pair_objects incrementally as calls come into
@@ -70,11 +71,15 @@
     """Adds an ImagePair; this may be repeated any number of times."""
     # Special handling when we add the first ImagePair...
     if not self._image_pair_objects:
-      self._image_base_url = image_pair.base_url
+      self._imageA_base_url = image_pair.imageA_base_url
+      self._imageB_base_url = image_pair.imageB_base_url
 
-    if image_pair.base_url != self._image_base_url:
+    if(image_pair.imageA_base_url != self._imageA_base_url):
       raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
-          image_pair.base_url, self._image_base_url))
+          image_pair.imageA_base_url, self._imageA_base_url))
+    if(image_pair.imageB_base_url != self._imageB_base_url):
+      raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
+          image_pair.imageB_base_url, self._imageB_base_url))
     self._image_pair_objects.append(image_pair)
     extra_columns_dict = image_pair.extra_columns_dict
     if extra_columns_dict:
@@ -171,10 +176,14 @@
 
     key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
     key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
-    if gs_utils.GSUtils.is_gs_url(self._image_base_url):
-      value_base_url = self._convert_gs_url_to_http_url(self._image_base_url)
+    if gs_utils.GSUtils.is_gs_url(self._imageA_base_url):
+      valueA_base_url = self._convert_gs_url_to_http_url(self._imageA_base_url)
     else:
-      value_base_url = self._image_base_url
+      valueA_base_url = self._imageA_base_url
+    if gs_utils.GSUtils.is_gs_url(self._imageB_base_url):
+      valueB_base_url = self._convert_gs_url_to_http_url(self._imageB_base_url)
+    else:
+      valueB_base_url = self._imageB_base_url
 
     # We've waited as long as we can to ask ImageDiffDB for details of the
     # image diffs, so that it has time to compute them.
@@ -188,11 +197,11 @@
         KEY__ROOT__IMAGESETS: {
             KEY__IMAGESETS__SET__IMAGE_A: {
                 key_description: self._descriptions[0],
-                key_base_url: value_base_url,
+                key_base_url: valueA_base_url,
             },
             KEY__IMAGESETS__SET__IMAGE_B: {
                 key_description: self._descriptions[1],
-                key_base_url: value_base_url,
+                key_base_url: valueB_base_url,
             },
             KEY__IMAGESETS__SET__DIFFS: {
                 key_description: 'color difference per channel',
diff --git a/gm/rebaseline_server/imagepairset_test.py b/gm/rebaseline_server/imagepairset_test.py
index 5e17faa..a931e04 100755
--- a/gm/rebaseline_server/imagepairset_test.py
+++ b/gm/rebaseline_server/imagepairset_test.py
@@ -79,9 +79,12 @@
     """Assembles some ImagePairs into an ImagePairSet, and validates results.
     """
     image_pairs = [
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT),
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT),
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_3_AS_DICT),
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_1_AS_DICT),
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_2_AS_DICT),
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_3_AS_DICT),
     ]
     expected_imageset_dict = {
         'extraColumnHeaders': {
@@ -150,12 +153,14 @@
     image_pair_set = imagepairset.ImagePairSet(
         diff_base_url=DIFF_BASE_URL)
     image_pair_set.add_image_pair(
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT))
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_1_AS_DICT))
     image_pair_set.add_image_pair(
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT))
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_2_AS_DICT))
     with self.assertRaises(Exception):
       image_pair_set.add_image_pair(
-          MockImagePair(base_url=BASE_URL_2,
+          MockImagePair(imageA_base_url=BASE_URL_2, imageB_base_url=BASE_URL_2,
                         dict_to_return=IMAGEPAIR_3_AS_DICT))
 
   def test_missing_column_ids(self):
@@ -164,9 +169,11 @@
     image_pair_set = imagepairset.ImagePairSet(
         diff_base_url=DIFF_BASE_URL)
     image_pair_set.add_image_pair(
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT))
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_1_AS_DICT))
     image_pair_set.add_image_pair(
-        MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT))
+        MockImagePair(imageA_base_url=BASE_URL_1, imageB_base_url=BASE_URL_1,
+                      dict_to_return=IMAGEPAIR_2_AS_DICT))
     # Call as_dict() with default or reasonable column_ids_in_order.
     image_pair_set.as_dict()
     image_pair_set.as_dict(column_ids_in_order=['test', 'builder'])
@@ -178,13 +185,14 @@
 
 class MockImagePair(object):
   """Mock ImagePair object, which will return canned results."""
-  def __init__(self, base_url, dict_to_return):
+  def __init__(self, imageA_base_url, imageB_base_url, dict_to_return):
     """
     Args:
       base_url: base_url attribute for this object
       dict_to_return: dictionary to return from as_dict()
     """
-    self.base_url = base_url
+    self.imageA_base_url = imageA_base_url
+    self.imageB_base_url = imageB_base_url
     self.extra_columns_dict = dict_to_return.get(
         imagepair.KEY__IMAGEPAIRS__EXTRACOLUMNS, None)
     self._dict_to_return = dict_to_return
diff --git a/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd_withImageBaseGSUrl/compare_rendered_pictures.json b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd_withImageBaseGSUrl/compare_rendered_pictures.json
new file mode 100644
index 0000000..e62b5cb
--- /dev/null
+++ b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd_withImageBaseGSUrl/compare_rendered_pictures.json
@@ -0,0 +1,237 @@
+{
+  "extraColumnHeaders": {
+    "builderA": {
+      "headerText": "builderA", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          null, 
+          4
+        ]
+      ]
+    }, 
+    "builderB": {
+      "headerText": "builderB", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          null, 
+          4
+        ]
+      ]
+    }, 
+    "renderModeA": {
+      "headerText": "renderModeA", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          null, 
+          4
+        ]
+      ]
+    }, 
+    "renderModeB": {
+      "headerText": "renderModeB", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          null, 
+          4
+        ]
+      ]
+    }, 
+    "resultType": {
+      "headerText": "resultType", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          "failed", 
+          1
+        ], 
+        [
+          "no-comparison", 
+          2
+        ], 
+        [
+          "succeeded", 
+          1
+        ]
+      ]
+    }, 
+    "sourceSkpFile": {
+      "headerText": "sourceSkpFile", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": true, 
+      "valuesAndCounts": [
+        [
+          "changed.skp", 
+          1
+        ], 
+        [
+          "only-in-after.skp", 
+          1
+        ], 
+        [
+          "only-in-before.skp", 
+          1
+        ], 
+        [
+          "unchanged.skp", 
+          1
+        ]
+      ]
+    }, 
+    "tiledOrWhole": {
+      "headerText": "tiledOrWhole", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": false, 
+      "valuesAndCounts": [
+        [
+          "whole", 
+          4
+        ]
+      ]
+    }, 
+    "tilenum": {
+      "headerText": "tilenum", 
+      "isFilterable": true, 
+      "isSortable": true, 
+      "useFreeformFilter": true, 
+      "valuesAndCounts": [
+        [
+          "N/A", 
+          4
+        ]
+      ]
+    }
+  }, 
+  "extraColumnOrder": [
+    "resultType", 
+    "sourceSkpFile", 
+    "tiledOrWhole", 
+    "tilenum", 
+    "builderA", 
+    "renderModeA", 
+    "builderB", 
+    "renderModeB"
+  ], 
+  "header": {
+    "dataHash": "-5707186260478709107", 
+    "isEditable": false, 
+    "isExported": true, 
+    "schemaVersion": 5, 
+    "setA": {
+      "dir": [
+        "before-patch-fake-dir"
+      ], 
+      "repoRevision": null, 
+      "section": "actual-results"
+    }, 
+    "setB": {
+      "dir": [
+        "after-patch-fake-dir"
+      ], 
+      "repoRevision": null, 
+      "section": "actual-results"
+    }, 
+    "timeNextUpdateAvailable": null, 
+    "timeUpdated": 12345678, 
+    "type": "all"
+  }, 
+  "imagePairs": [
+    {
+      "extraColumns": {
+        "builderA": null, 
+        "builderB": null, 
+        "renderModeA": null, 
+        "renderModeB": null, 
+        "resultType": "failed", 
+        "sourceSkpFile": "changed.skp", 
+        "tiledOrWhole": "whole", 
+        "tilenum": "N/A"
+      }, 
+      "imageAUrl": "changed_skp/bitmap-64bitMD5_3101044995537104462.png", 
+      "imageBUrl": "changed_skp/bitmap-64bitMD5_13623922271964399662.png", 
+      "isDifferent": true, 
+      "sourceJsonFile": "./summary.json"
+    }, 
+    {
+      "extraColumns": {
+        "builderA": null, 
+        "builderB": null, 
+        "renderModeA": null, 
+        "renderModeB": null, 
+        "resultType": "no-comparison", 
+        "sourceSkpFile": "only-in-after.skp", 
+        "tiledOrWhole": "whole", 
+        "tilenum": "N/A"
+      }, 
+      "imageAUrl": null, 
+      "imageBUrl": "only-in-after_skp/bitmap-64bitMD5_2320185040577047131.png", 
+      "isDifferent": true, 
+      "sourceJsonFile": "./summary.json"
+    }, 
+    {
+      "extraColumns": {
+        "builderA": null, 
+        "builderB": null, 
+        "renderModeA": null, 
+        "renderModeB": null, 
+        "resultType": "no-comparison", 
+        "sourceSkpFile": "only-in-before.skp", 
+        "tiledOrWhole": "whole", 
+        "tilenum": "N/A"
+      }, 
+      "imageAUrl": "only-in-before_skp/bitmap-64bitMD5_2320185040577047131.png", 
+      "imageBUrl": null, 
+      "isDifferent": true, 
+      "sourceJsonFile": "./summary.json"
+    }, 
+    {
+      "extraColumns": {
+        "builderA": null, 
+        "builderB": null, 
+        "renderModeA": null, 
+        "renderModeB": null, 
+        "resultType": "succeeded", 
+        "sourceSkpFile": "unchanged.skp", 
+        "tiledOrWhole": "whole", 
+        "tilenum": "N/A"
+      }, 
+      "imageAUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png", 
+      "imageBUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png", 
+      "isDifferent": false, 
+      "sourceJsonFile": "./summary.json"
+    }
+  ], 
+  "imageSets": {
+    "diffs": {
+      "baseUrl": "/static/generated-images/diffs", 
+      "description": "color difference per channel"
+    }, 
+    "imageA": {
+      "baseUrl": "http://storage.cloud.google.com/superman/kent-camera/pictures", 
+      "description": "setA"
+    }, 
+    "imageB": {
+      "baseUrl": "http://storage.cloud.google.com/batman/batarang/pictures", 
+      "description": "setB"
+    }, 
+    "whiteDiffs": {
+      "baseUrl": "/static/generated-images/whitediffs", 
+      "description": "differing pixels in white"
+    }
+  }
+}
\ No newline at end of file
diff --git a/tools/image_expectations.cpp b/tools/image_expectations.cpp
index f0c9cfb..05a905d 100644
--- a/tools/image_expectations.cpp
+++ b/tools/image_expectations.cpp
@@ -30,6 +30,7 @@
 const static char kJsonKey_ActualResults[] = "actual-results";
 const static char kJsonKey_Descriptions[] = "descriptions";
 const static char kJsonKey_ExpectedResults[] = "expected-results";
+const static char kJsonKey_ImageBaseGSUrl[] = "image-base-gs-url";
 const static char kJsonKey_Header[] = "header";
 const static char kJsonKey_Header_Type[] = "type";
 const static char kJsonKey_Header_Revision[] = "revision";
@@ -198,6 +199,10 @@
         fDescriptions[key] = value;
     }
 
+    void ImageResultsAndExpectations::setImageBaseGSUrl(const char *imageBaseGSUrl) {
+        fImageBaseGSUrl = imageBaseGSUrl;
+    }
+
     Expectation ImageResultsAndExpectations::getExpectation(const char *sourceName,
                                                             const int *tileNumber) {
         if (fExpectedResults.isNull()) {
@@ -228,6 +233,7 @@
         root[kJsonKey_ActualResults] = fActualResults;
         root[kJsonKey_Descriptions] = fDescriptions;
         root[kJsonKey_Header] = header;
+        root[kJsonKey_ImageBaseGSUrl] = fImageBaseGSUrl;
         std::string jsonStdString = root.toStyledString();
         SkFILEWStream stream(filename);
         stream.write(jsonStdString.c_str(), jsonStdString.length());
diff --git a/tools/image_expectations.h b/tools/image_expectations.h
index 2d58b92..422c64d 100644
--- a/tools/image_expectations.h
+++ b/tools/image_expectations.h
@@ -185,6 +185,13 @@
         void addDescription(const char *key, const char *value);
 
         /**
+         * Adds the image base Google Storage URL to the summary of results.
+         *
+         * @param imageBaseGSUrl the image base Google Storage URL
+         */
+        void setImageBaseGSUrl(const char *imageBaseGSUrl);
+
+        /**
          * Returns the Expectation for this test.
          *
          * @param sourceName name of the source file that generated this result
@@ -217,6 +224,7 @@
         Json::Value fDescriptions;
         Json::Value fExpectedJsonRoot;
         Json::Value fExpectedResults;
+        Json::Value fImageBaseGSUrl;
     };
 
 } // namespace sk_tools
diff --git a/tools/render_pictures_main.cpp b/tools/render_pictures_main.cpp
index 595a78e..d508510 100644
--- a/tools/render_pictures_main.cpp
+++ b/tools/render_pictures_main.cpp
@@ -30,6 +30,7 @@
 DECLARE_bool(deferImageDecoding);
 DEFINE_string(descriptions, "", "one or more key=value pairs to add to the descriptions section "
               "of the JSON summary.");
+DEFINE_string(imageBaseGSUrl, "", "The Google Storage image base URL the images are stored in.");
 DEFINE_int32(maxComponentDiff, 256, "Maximum diff on a component, 0 - 256. Components that differ "
              "by more than this amount are considered errors, though all diffs are reported. "
              "Requires --validate.");
@@ -498,6 +499,9 @@
             SkASSERT(tokens.count() == 2);
             jsonSummary.addDescription(tokens[0].c_str(), tokens[1].c_str());
         }
+        if (FLAGS_imageBaseGSUrl.count() == 1) {
+          jsonSummary.setImageBaseGSUrl(FLAGS_imageBaseGSUrl[0]);
+        }
         jsonSummary.writeToFile(FLAGS_writeJsonSummaryPath[0]);
     }
     return 0;
diff --git a/tools/tests/render_pictures_test.py b/tools/tests/render_pictures_test.py
index 9ff3226..6daadd8 100755
--- a/tools/tests/render_pictures_test.py
+++ b/tools/tests/render_pictures_test.py
@@ -197,6 +197,7 @@
         '--writeWholeImage'])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -233,6 +234,7 @@
     modified_red_tiles[5]['comparisonResult'] = 'failure-ignored'
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -270,6 +272,7 @@
     modified_red_tiles[5]['comparisonResult'] = 'no-comparison'
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -315,6 +318,7 @@
     if expected_summary_dict == None:
       expected_summary_dict = {
           "header" : EXPECTED_HEADER_CONTENTS,
+          "image-base-gs-url" : None,
           "descriptions" : None,
           "actual-results" : {
               "red.skp": {
@@ -340,6 +344,7 @@
       pass
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -370,6 +375,7 @@
     ])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : {
             "builder": "builderName",
             "renderMode": "renderModeName",
@@ -421,6 +427,7 @@
         '--writeJsonSummaryPath', output_json_path])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -449,6 +456,7 @@
         '--writeJsonSummaryPath', output_json_path])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -484,6 +492,7 @@
         '--writeJsonSummaryPath', output_json_path])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {
@@ -515,6 +524,7 @@
                                '--writeJsonSummaryPath', output_json_path])
     expected_summary_dict = {
         "header" : EXPECTED_HEADER_CONTENTS,
+        "image-base-gs-url" : None,
         "descriptions" : None,
         "actual-results" : {
             "red.skp": {