1#!/usr/bin/python
2
3"""
4Copyright 2014 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
8
9ImagePair class (see class docstring for details)
10"""
11
12import posixpath
13
14
15# Keys used within ImagePair dictionary representations.
16# NOTE: Keep these in sync with static/constants.js
17KEY__IMAGEPAIRS__DIFFERENCES = 'differenceData'
18KEY__IMAGEPAIRS__EXPECTATIONS = 'expectations'
19KEY__IMAGEPAIRS__EXTRACOLUMNS = 'extraColumns'
20KEY__IMAGEPAIRS__IMAGE_A_URL = 'imageAUrl'
21KEY__IMAGEPAIRS__IMAGE_B_URL = 'imageBUrl'
22KEY__IMAGEPAIRS__IS_DIFFERENT = 'isDifferent'
23KEY__IMAGEPAIRS__SOURCE_JSON_FILE = 'sourceJsonFile'
24
25# If self._diff_record is set to this, we haven't asked ImageDiffDB for the
26# image diff details yet.
27_DIFF_RECORD_STILL_LOADING = 'still_loading'
28
29
30class ImagePair(object):
31  """Describes a pair of images, pixel difference info, and optional metadata.
32  """
33
34  def __init__(self, image_diff_db,
35               imageA_base_url, imageB_base_url,
36               imageA_relative_url, imageB_relative_url,
37               expectations=None, extra_columns=None, source_json_file=None,
38               download_all_images=False):
39    """
40    Args:
41      image_diff_db: ImageDiffDB instance we use to generate/store image diffs
42      imageA_base_url: string; base URL for image A
43      imageB_base_url: string; base URL for image B
44      imageA_relative_url: string; URL pointing at an image, relative to
45          imageA_base_url; or None, if this image is missing
46      imageB_relative_url: string; URL pointing at an image, relative to
47          imageB_base_url; or None, if this image is missing
48      expectations: optional dictionary containing expectations-specific
49          metadata (ignore-failure, bug numbers, etc.)
50      extra_columns: optional dictionary containing more metadata (test name,
51          builder name, etc.)
52      source_json_file: relative path of the JSON file where each image came
53          from; this will be the same for both imageA and imageB, within their
54          respective directories
55      download_all_images: if True, download any images associated with this
56          image pair, even if we don't need them to generate diffs
57          (imageA == imageB, or one of them is missing)
58    """
59    self._image_diff_db = image_diff_db
60    self.imageA_base_url = imageA_base_url
61    self.imageB_base_url = imageB_base_url
62    self.imageA_relative_url = imageA_relative_url
63    self.imageB_relative_url = imageB_relative_url
64    self.expectations_dict = expectations
65    self.extra_columns_dict = extra_columns
66    self.source_json_file = source_json_file
67    if not imageA_relative_url or not imageB_relative_url:
68      self._is_different = True
69      self._diff_record = None
70    elif imageA_relative_url == imageB_relative_url:
71      self._is_different = False
72      self._diff_record = None
73    else:
74      # Tell image_diff_db to add an entry for this diff asynchronously.
75      # Later on, we will call image_diff_db.get_diff_record() to find it.
76      self._is_different = True
77      self._diff_record = _DIFF_RECORD_STILL_LOADING
78
79    if self._diff_record != None or download_all_images:
80      image_diff_db.add_image_pair(
81          expected_image_locator=imageA_relative_url,
82          expected_image_url=self.posixpath_join(imageA_base_url,
83                                                 imageA_relative_url),
84          actual_image_locator=imageB_relative_url,
85          actual_image_url=self.posixpath_join(imageB_base_url,
86                                               imageB_relative_url))
87
88  def as_dict(self):
89    """Returns a dictionary describing this ImagePair.
90
91    Uses the KEY__IMAGEPAIRS__* constants as keys.
92    """
93    asdict = {
94        KEY__IMAGEPAIRS__IMAGE_A_URL: self.imageA_relative_url,
95        KEY__IMAGEPAIRS__IMAGE_B_URL: self.imageB_relative_url,
96    }
97    asdict[KEY__IMAGEPAIRS__IS_DIFFERENT] = self._is_different
98    if self.expectations_dict:
99      asdict[KEY__IMAGEPAIRS__EXPECTATIONS] = self.expectations_dict
100    if self.extra_columns_dict:
101      asdict[KEY__IMAGEPAIRS__EXTRACOLUMNS] = self.extra_columns_dict
102    if self.source_json_file:
103      asdict[KEY__IMAGEPAIRS__SOURCE_JSON_FILE] = self.source_json_file
104    if self._diff_record is _DIFF_RECORD_STILL_LOADING:
105      # We have waited as long as we can to ask ImageDiffDB for details of
106      # this image diff.  Now we must block until ImageDiffDB can provide
107      # those details.
108      #
109      # TODO(epoger): Is it wasteful for every imagepair to have its own
110      # reference to image_diff_db?  If so, we could pass an image_diff_db
111      # reference into this method call instead...
112      self._diff_record = self._image_diff_db.get_diff_record(
113          expected_image_locator=self.imageA_relative_url,
114          actual_image_locator=self.imageB_relative_url)
115    if self._diff_record != None:
116      asdict[KEY__IMAGEPAIRS__DIFFERENCES] = self._diff_record.as_dict()
117    return asdict
118
119  @staticmethod
120  def posixpath_join(*args):
121    """Wrapper around posixpath.join().
122
123    Returns posixpath.join(*args), or None if any arg is None.
124    """
125    for arg in args:
126      if arg == None:
127        return None
128    return posixpath.join(*args)
129