1a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# Copyright 2013 The Chromium Authors. All rights reserved.
2a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
3a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)# found in the LICENSE file.
45d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
55d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)"""
65d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)Bitmap is a basic wrapper for image pixels. It includes some basic processing
75d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)tools: crop, find bounding box of a color and compute histogram of color values.
85d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)"""
95d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import array
11a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import base64
12a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import cStringIO
13a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import collections
145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import struct
155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import subprocess
16a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
17a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)from telemetry.core import util
18cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)from telemetry.core import platform
190529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochfrom telemetry.util import support_binaries
20a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
21a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png')
22a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)import png  # pylint: disable=F0401
23a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
24a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
25a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)def HistogramDistance(hist1, hist2):
26a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  """Earth mover's distance.
27a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
28a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  http://en.wikipedia.org/wiki/Earth_mover's_distance
29a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  First, normalize the two histograms. Then, treat the two histograms as
30a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  piles of dirt, and calculate the cost of turning one pile into the other.
31a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
32a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  To do this, calculate the difference in one bucket between the two
33a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  histograms. Then carry it over in the calculation for the next bucket.
34a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  In this way, the difference is weighted by how far it has to move."""
35a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  if len(hist1) != len(hist2):
36a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    raise ValueError('Trying to compare histograms '
37a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      'of different sizes, %s != %s' % (len(hist1), len(hist2)))
38a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
39a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  n1 = sum(hist1)
40a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  n2 = sum(hist2)
41a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  if n1 == 0:
42a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    raise ValueError('First histogram has 0 pixels in it.')
43a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  if n2 == 0:
44a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    raise ValueError('Second histogram has 0 pixels in it.')
45a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
46a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  total = 0
47a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  remainder = 0
48a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  for value1, value2 in zip(hist1, hist2):
49a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    remainder += value1 * n2 - value2 * n1
50a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    total += abs(remainder)
51a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  assert remainder == 0, (
52a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      '%s pixel(s) left over after computing histogram distance.'
53a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      % abs(remainder))
54a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  return abs(float(total) / n1 / n2)
55a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
56a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
57a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)class ColorHistogram(
58a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    collections.namedtuple('ColorHistogram', ['r', 'g', 'b', 'default_color'])):
59a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  # pylint: disable=W0232
60a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  # pylint: disable=E1002
61a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
62a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  def __new__(cls, r, g, b, default_color=None):
63a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    return super(ColorHistogram, cls).__new__(cls, r, g, b, default_color)
64a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
65a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  def Distance(self, other):
66a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    total = 0
67a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    for i in xrange(3):
68a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      hist1 = self[i]
69a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      hist2 = other[i]
70a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
71a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      if sum(self[i]) == 0:
72a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if not self.default_color:
73a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          raise ValueError('Histogram has no data and no default color.')
74a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        hist1 = [0] * 256
75a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        hist1[self.default_color[i]] = 1
76a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      if sum(other[i]) == 0:
77a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if not other.default_color:
78a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          raise ValueError('Histogram has no data and no default color.')
79a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        hist2 = [0] * 256
80a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        hist2[other.default_color[i]] = 1
81a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
82a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      total += HistogramDistance(hist1, hist2)
83a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    return total
84a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
85a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
86a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)class RgbaColor(collections.namedtuple('RgbaColor', ['r', 'g', 'b', 'a'])):
87a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  """Encapsulates an RGBA color retreived from a Bitmap"""
88a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  # pylint: disable=W0232
89a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  # pylint: disable=E1002
90a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
91a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  def __new__(cls, r, g, b, a=255):
92a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    return super(RgbaColor, cls).__new__(cls, r, g, b, a)
93a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def __int__(self):
955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return (self.r << 16) | (self.g << 8) | self.b
965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
97a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def IsEqual(self, expected_color, tolerance=0):
98a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    """Verifies that the color is within a given tolerance of
99a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    the expected color"""
100a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    r_diff = abs(self.r - expected_color.r)
101a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    g_diff = abs(self.g - expected_color.g)
102a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    b_diff = abs(self.b - expected_color.b)
103a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    a_diff = abs(self.a - expected_color.a)
104a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return (r_diff <= tolerance and g_diff <= tolerance
105a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        and b_diff <= tolerance and a_diff <= tolerance)
106a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
107a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def AssertIsRGB(self, r, g, b, tolerance=0):
108a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert self.IsEqual(RgbaColor(r, g, b), tolerance)
109a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
110a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def AssertIsRGBA(self, r, g, b, a, tolerance=0):
111a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert self.IsEqual(RgbaColor(r, g, b, a), tolerance)
112a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
113a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
1145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100,  13)
1155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)WHITE =                RgbaColor(255, 255, 255)
1165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)class _BitmapTools(object):
1195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  """Wraps a child process of bitmaptools and allows for one command."""
1205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  CROP_PIXELS = 0
1215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  HISTOGRAM = 1
1225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  BOUNDING_BOX = 2
1235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def __init__(self, dimensions, pixels):
1250529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    binary = support_binaries.FindPath(
126cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)        'bitmaptools', platform.GetHostPlatform().GetOSName())
1275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    assert binary, 'You must build bitmaptools first!'
1285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._popen = subprocess.Popen([binary],
1305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                                   stdin=subprocess.PIPE,
1315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                                   stdout=subprocess.PIPE,
1325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                                   stderr=subprocess.PIPE)
1335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight
1355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    packed_dims = struct.pack('iiiiiii', *dimensions)
1365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._popen.stdin.write(packed_dims)
1375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    # If we got a list of ints, we need to convert it into a byte buffer.
1385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if type(pixels) is not bytearray:
1395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      pixels = bytearray(pixels)
1405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._popen.stdin.write(pixels)
1415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def _RunCommand(self, *command):
1435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    assert not self._popen.stdin.closed, (
1445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      'Exactly one command allowed per instance of tools.')
1455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    packed_command = struct.pack('i' * len(command), *command)
1465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._popen.stdin.write(packed_command)
1475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._popen.stdin.close()
1485d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    length_packed = self._popen.stdout.read(struct.calcsize('i'))
1495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if not length_packed:
1505d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      raise Exception(self._popen.stderr.read())
1515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    length = struct.unpack('i', length_packed)[0]
1525d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._popen.stdout.read(length)
1535d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1545d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def CropPixels(self):
1555d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._RunCommand(_BitmapTools.CROP_PIXELS)
1565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def Histogram(self, ignore_color, tolerance):
158a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    ignore_color_int = -1 if ignore_color is None else int(ignore_color)
159a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    response = self._RunCommand(_BitmapTools.HISTOGRAM,
160a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)                                ignore_color_int, tolerance)
1615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    out = array.array('i')
1625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    out.fromstring(response)
163a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    assert len(out) == 768, (
164a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        'The ColorHistogram has the wrong number of buckets: %s' % len(out))
165a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    return ColorHistogram(out[:256], out[256:512], out[512:], ignore_color)
1665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def BoundingBox(self, color, tolerance):
1685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color),
1695d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                                tolerance)
1705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    unpacked = struct.unpack('iiiii', response)
1715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    box, count = unpacked[:4], unpacked[-1]
1725d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if box[2] < 0 or box[3] < 0:
1735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      box = None
1745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return box, count
1755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
177a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)class Bitmap(object):
178a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  """Utilities for parsing and inspecting a bitmap."""
179a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
180a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def __init__(self, bpp, width, height, pixels, metadata=None):
181a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert bpp in [3, 4], 'Invalid bytes per pixel'
182a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert width > 0, 'Invalid width'
183a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert height > 0, 'Invalid height'
184a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert pixels, 'Must specify pixels'
185a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch'
186a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
187a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    self._bpp = bpp
188a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    self._width = width
189a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    self._height = height
190a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    self._pixels = pixels
1915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._metadata = metadata or {}
1925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._crop_box = None
1935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  @property
1955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def bpp(self):
1965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Bytes per pixel."""
1975d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._bpp
198a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
199a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  @property
200a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def width(self):
2015d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Width of the bitmap."""
2025d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._crop_box[2] if self._crop_box else self._width
203a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
204a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  @property
205a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def height(self):
2065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Height of the bitmap."""
2075d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._crop_box[3] if self._crop_box else self._height
2085d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2095d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def _PrepareTools(self):
2105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Prepares an instance of _BitmapTools which allows exactly one command.
2115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """
2125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    crop_box = self._crop_box or (0, 0, self._width, self._height)
2135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return _BitmapTools((self._bpp, self._width, self._height) + crop_box,
2145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                        self._pixels)
2155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  @property
2175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def pixels(self):
2185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Flat pixel array of the bitmap."""
2195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if self._crop_box:
2205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      self._pixels = self._PrepareTools().CropPixels()
2215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      _, _, self._width, self._height = self._crop_box
2225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      self._crop_box = None
2235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if type(self._pixels) is not bytearray:
2245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      self._pixels = bytearray(self._pixels)
2255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._pixels
2265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  @property
2285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def metadata(self):
2295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._metadata['size'] = (self.width, self.height)
2305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._metadata['alpha'] = self.bpp == 4
2315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._metadata['bitdepth'] = 8
2325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._metadata
233a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
234a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def GetPixelColor(self, x, y):
2355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Returns a RgbaColor for the pixel at (x, y)."""
2365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    pixels = self.pixels
237a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    base = self._bpp * (y * self._width + x)
238a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    if self._bpp == 4:
2395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      return RgbaColor(pixels[base + 0], pixels[base + 1],
2405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                       pixels[base + 2], pixels[base + 3])
2415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return RgbaColor(pixels[base + 0], pixels[base + 1],
2425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                     pixels[base + 2])
243a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
244a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def WritePngFile(self, path):
245a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    with open(path, "wb") as f:
2465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      png.Writer(**self.metadata).write_array(f, self.pixels)
247a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
248a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  @staticmethod
249a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def FromPng(png_data):
250a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    width, height, pixels, meta = png.Reader(bytes=png_data).read_flat()
251a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta)
252a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
253a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  @staticmethod
254a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def FromPngFile(path):
255a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    with open(path, "rb") as f:
256a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      return Bitmap.FromPng(f.read())
257a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
258a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  @staticmethod
259a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def FromBase64Png(base64_png):
260a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return Bitmap.FromPng(base64.b64decode(base64_png))
261a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
262a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def IsEqual(self, other, tolerance=0):
2635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Determines whether two Bitmaps are identical within a given tolerance."""
264a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
265a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # Dimensions must be equal
266a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    if self.width != other.width or self.height != other.height:
267a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      return False
268a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
269a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # Loop over each pixel and test for equality
2705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if tolerance or self.bpp != other.bpp:
271a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      for y in range(self.height):
272a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        for x in range(self.width):
273a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c0 = self.GetPixelColor(x, y)
274a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c1 = other.GetPixelColor(x, y)
275a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          if not c0.IsEqual(c1, tolerance):
276a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)            return False
277a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    else:
2785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      return self.pixels == other.pixels
279a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
280a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return True
281a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
282a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def Diff(self, other):
283a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    """Returns a new Bitmap that represents the difference between this image
284a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    and another Bitmap."""
285a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
286a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # Output dimensions will be the maximum of the two input dimensions
287a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    out_width = max(self.width, other.width)
288a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    out_height = max(self.height, other.height)
289a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
290a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)]
291a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
292a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # Loop over each pixel and write out the difference
293a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    for y in range(out_height):
294a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      for x in range(out_width):
295a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        if x < self.width and y < self.height:
296a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c0 = self.GetPixelColor(x, y)
297a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        else:
298a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c0 = RgbaColor(0, 0, 0, 0)
299a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
300a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        if x < other.width and y < other.height:
301a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c1 = other.GetPixelColor(x, y)
302a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        else:
303a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)          c1 = RgbaColor(0, 0, 0, 0)
304a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
305a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        offset = x * 3
306a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        diff[y][offset] = abs(c0.r - c1.r)
307a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        diff[y][offset+1] = abs(c0.g - c1.g)
308a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        diff[y][offset+2] = abs(c0.b - c1.b)
309a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
310a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # This particular method can only save to a file, so the result will be
311a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # written into an in-memory buffer and read back into a Bitmap
312a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    diff_img = png.from_array(diff, mode='RGB')
313a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    output = cStringIO.StringIO()
314a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    try:
315a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      diff_img.save(output)
316a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      diff = Bitmap.FromPng(output.getvalue())
317a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    finally:
318a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      output.close()
319a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
320a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return diff
321a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
3225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def GetBoundingBox(self, color, tolerance=0):
3235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Finds the minimum box surrounding all occurences of |color|.
3245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    Returns: (top, left, width, height), match_count
3255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    Ignores the alpha channel."""
3265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._PrepareTools().BoundingBox(color, tolerance)
3275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
328a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def Crop(self, left, top, width, height):
3295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Crops the current bitmap down to the specified box."""
3305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    cur_box = self._crop_box or (0, 0, self._width, self._height)
3315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    cur_left, cur_top, cur_width, cur_height = cur_box
332a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
333a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    if (left < 0 or top < 0 or
3345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)        (left + width) > cur_width or
3355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)        (top + height) > cur_height):
336a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)      raise ValueError('Invalid dimensions')
337a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
3385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    self._crop_box = cur_left + left, cur_top + top, width, height
3395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self
340a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
3415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  def ColorHistogram(self, ignore_color=None, tolerance=0):
3425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """Computes a histogram of the pixel colors in this Bitmap.
3435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    Args:
3445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      ignore_color: An RgbaColor to exclude from the bucket counts.
3455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      tolerance: A tolerance for the ignore_color.
346a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)
3475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    Returns:
348a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      A ColorHistogram namedtuple with 256 integers in each field: r, g, and b.
3495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    """
3505d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    return self._PrepareTools().Histogram(ignore_color, tolerance)
351