OLD | NEW |
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 import base64 | 4 import base64 |
5 import cStringIO | 5 import cStringIO |
6 | 6 |
7 from telemetry.core import bitmaptools | |
8 from telemetry.core import util | 7 from telemetry.core import util |
9 | 8 |
10 util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') | 9 util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') |
11 import png # pylint: disable=F0401 | 10 import png # pylint: disable=F0401 |
12 | 11 |
13 | 12 |
14 class RgbaColor(object): | 13 class RgbaColor(object): |
15 """Encapsulates an RGBA color retreived from a Bitmap""" | 14 """Encapsulates an RGBA color retreived from a Bitmap""" |
16 | 15 |
17 def __init__(self, r, g, b, a=255): | 16 def __init__(self, r, g, b, a=255): |
(...skipping 27 matching lines...) Expand all Loading... |
45 assert width > 0, 'Invalid width' | 44 assert width > 0, 'Invalid width' |
46 assert height > 0, 'Invalid height' | 45 assert height > 0, 'Invalid height' |
47 assert pixels, 'Must specify pixels' | 46 assert pixels, 'Must specify pixels' |
48 assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' | 47 assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' |
49 | 48 |
50 self._bpp = bpp | 49 self._bpp = bpp |
51 self._width = width | 50 self._width = width |
52 self._height = height | 51 self._height = height |
53 self._pixels = pixels | 52 self._pixels = pixels |
54 self._metadata = metadata or {} | 53 self._metadata = metadata or {} |
55 self._crop_box = None | |
56 | 54 |
57 @property | 55 @property |
58 def bpp(self): | 56 def bpp(self): |
59 """Bytes per pixel.""" | 57 """Bytes per pixel.""" |
60 return self._bpp | 58 return self._bpp |
61 | 59 |
62 @property | 60 @property |
63 def width(self): | 61 def width(self): |
64 """Width of the bitmap.""" | 62 """Width of the bitmap.""" |
65 if self._crop_box: | |
66 return self._crop_box[2] | |
67 return self._width | 63 return self._width |
68 | 64 |
69 @property | 65 @property |
70 def height(self): | 66 def height(self): |
71 """Height of the bitmap.""" | 67 """Height of the bitmap.""" |
72 if self._crop_box: | |
73 return self._crop_box[3] | |
74 return self._height | 68 return self._height |
75 | 69 |
76 @property | 70 @property |
77 def _as_tuple(self): | |
78 # If we got a list of ints, we need to convert it into a byte buffer. | |
79 pixels = self._pixels | |
80 if type(pixels) is not bytearray: | |
81 pixels = bytearray(pixels) | |
82 if type(pixels) is not bytes: | |
83 pixels = bytes(pixels) | |
84 crop_box = self._crop_box or (0, 0, self._width, self._height) | |
85 return pixels, self._width, self._bpp, crop_box | |
86 | |
87 @property | |
88 def pixels(self): | 71 def pixels(self): |
89 """Flat pixel array of the bitmap.""" | 72 """Flat pixel array of the bitmap.""" |
90 if self._crop_box: | |
91 self._pixels = bitmaptools.Crop(self._as_tuple) | |
92 _, _, self._width, self._height = self._crop_box | |
93 self._crop_box = None | |
94 if type(self._pixels) is not bytearray: | 73 if type(self._pixels) is not bytearray: |
95 self._pixels = bytearray(self._pixels) | 74 self._pixels = bytearray(self._pixels) |
96 return self._pixels | 75 return self._pixels |
97 | 76 |
98 @property | 77 @property |
99 def metadata(self): | 78 def metadata(self): |
100 self._metadata['size'] = (self.width, self.height) | 79 self._metadata['size'] = (self.width, self.height) |
101 self._metadata['alpha'] = self.bpp == 4 | 80 self._metadata['alpha'] = self.bpp == 4 |
102 self._metadata['bitdepth'] = 8 | 81 self._metadata['bitdepth'] = 8 |
103 return self._metadata | 82 return self._metadata |
104 | 83 |
105 def GetPixelColor(self, x, y): | 84 def GetPixelColor(self, x, y): |
106 """Returns a RgbaColor for the pixel at (x, y).""" | 85 """Returns a RgbaColor for the pixel at (x, y).""" |
107 pixels = self.pixels | |
108 base = self._bpp * (y * self._width + x) | 86 base = self._bpp * (y * self._width + x) |
109 if self._bpp == 4: | 87 if self._bpp == 4: |
110 return RgbaColor(pixels[base + 0], pixels[base + 1], | 88 return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
111 pixels[base + 2], pixels[base + 3]) | 89 self._pixels[base + 2], self._pixels[base + 3]) |
112 return RgbaColor(pixels[base + 0], pixels[base + 1], | 90 return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
113 pixels[base + 2]) | 91 self._pixels[base + 2]) |
114 | 92 |
115 def WritePngFile(self, path): | 93 def WritePngFile(self, path): |
116 with open(path, "wb") as f: | 94 with open(path, "wb") as f: |
117 png.Writer(**self.metadata).write_array(f, self.pixels) | 95 png.Writer(**self.metadata).write_array(f, self.pixels) |
118 | 96 |
119 @staticmethod | 97 @staticmethod |
120 def FromPng(png_data): | 98 def FromPng(png_data): |
121 width, height, pixels, meta = png.Reader(bytes=png_data).read_flat() | 99 width, height, pixels, meta = png.Reader(bytes=png_data).read_flat() |
122 return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta) | 100 return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta) |
123 | 101 |
124 @staticmethod | 102 @staticmethod |
125 def FromPngFile(path): | 103 def FromPngFile(path): |
126 with open(path, "rb") as f: | 104 with open(path, "rb") as f: |
127 return Bitmap.FromPng(f.read()) | 105 return Bitmap.FromPng(f.read()) |
128 | 106 |
129 @staticmethod | 107 @staticmethod |
130 def FromBase64Png(base64_png): | 108 def FromBase64Png(base64_png): |
131 return Bitmap.FromPng(base64.b64decode(base64_png)) | 109 return Bitmap.FromPng(base64.b64decode(base64_png)) |
132 | 110 |
133 def IsEqual(self, other, tolerance=0): | 111 def IsEqual(self, other, tolerance=0): |
134 """Determines whether two Bitmaps are identical within a given tolerance. | 112 """Determines whether two Bitmaps are identical within a given tolerance.""" |
135 Ignores alpha channel.""" | 113 |
136 # pylint: disable=W0212 | 114 # Dimensions must be equal |
137 return bitmaptools.Equal(self._as_tuple, other._as_tuple, tolerance) | 115 if self.width != other.width or self.height != other.height: |
| 116 return False |
| 117 |
| 118 # Loop over each pixel and test for equality |
| 119 if tolerance or self.bpp != other.bpp: |
| 120 for y in range(self.height): |
| 121 for x in range(self.width): |
| 122 c0 = self.GetPixelColor(x, y) |
| 123 c1 = other.GetPixelColor(x, y) |
| 124 if not c0.IsEqual(c1, tolerance): |
| 125 return False |
| 126 else: |
| 127 return self.pixels == other.pixels |
| 128 |
| 129 return True |
138 | 130 |
139 def Diff(self, other): | 131 def Diff(self, other): |
140 """Returns a new Bitmap that represents the difference between this image | 132 """Returns a new Bitmap that represents the difference between this image |
141 and another Bitmap.""" | 133 and another Bitmap.""" |
142 | 134 |
143 # Output dimensions will be the maximum of the two input dimensions | 135 # Output dimensions will be the maximum of the two input dimensions |
144 out_width = max(self.width, other.width) | 136 out_width = max(self.width, other.width) |
145 out_height = max(self.height, other.height) | 137 out_height = max(self.height, other.height) |
146 | 138 |
147 diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)] | 139 diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)] |
(...skipping 22 matching lines...) Expand all Loading... |
170 output = cStringIO.StringIO() | 162 output = cStringIO.StringIO() |
171 try: | 163 try: |
172 diff_img.save(output) | 164 diff_img.save(output) |
173 diff = Bitmap.FromPng(output.getvalue()) | 165 diff = Bitmap.FromPng(output.getvalue()) |
174 finally: | 166 finally: |
175 output.close() | 167 output.close() |
176 | 168 |
177 return diff | 169 return diff |
178 | 170 |
179 def GetBoundingBox(self, color, tolerance=0): | 171 def GetBoundingBox(self, color, tolerance=0): |
180 """Finds the minimum box surrounding all occurences of |color|. | 172 """Returns a (top, left, width, height) tuple of the minimum box |
181 Returns: (top, left, width, height), match_count | 173 surrounding all occurences of |color|.""" |
182 Ignores the alpha channel.""" | 174 # TODO(szym): Implement this. |
183 int_color = (color.r << 16) | (color.g << 8) | color.b | 175 raise NotImplementedError("GetBoundingBox not yet implemented.") |
184 return bitmaptools.BoundingBox(self._as_tuple, int_color, tolerance) | |
185 | 176 |
186 def Crop(self, top, left, width, height): | 177 def Crop(self, top, left, width, height): |
187 """Crops the current bitmap down to the specified box.""" | 178 """Crops the current bitmap down to the specified box. |
188 cur_box = self._crop_box or (0, 0, self._width, self._height) | |
189 cur_left, cur_top, cur_width, cur_height = cur_box | |
190 | 179 |
| 180 TODO(szym): Make this O(1). |
| 181 """ |
191 if (left < 0 or top < 0 or | 182 if (left < 0 or top < 0 or |
192 (left + width) > cur_width or | 183 (left + width) > self.width or |
193 (top + height) > cur_height): | 184 (top + height) > self.height): |
194 raise ValueError('Invalid dimensions') | 185 raise ValueError('Invalid dimensions') |
195 | 186 |
196 self._crop_box = cur_left + left, cur_top + top, width, height | 187 img_data = [[0 for x in xrange(width * self.bpp)] |
| 188 for y in xrange(height)] |
| 189 |
| 190 # Copy each pixel in the sub-rect. |
| 191 # TODO(tonyg): Make this faster by avoiding the copy and artificially |
| 192 # restricting the dimensions. |
| 193 for y in range(height): |
| 194 for x in range(width): |
| 195 c = self.GetPixelColor(x + left, y + top) |
| 196 offset = x * self.bpp |
| 197 img_data[y][offset] = c.r |
| 198 img_data[y][offset + 1] = c.g |
| 199 img_data[y][offset + 2] = c.b |
| 200 if self.bpp == 4: |
| 201 img_data[y][offset + 3] = c.a |
| 202 |
| 203 # This particular method can only save to a file, so the result will be |
| 204 # written into an in-memory buffer and read back into a Bitmap |
| 205 crop_img = png.from_array(img_data, mode='RGBA' if self.bpp == 4 else 'RGB') |
| 206 output = cStringIO.StringIO() |
| 207 try: |
| 208 crop_img.save(output) |
| 209 width, height, pixels, meta = png.Reader( |
| 210 bytes=output.getvalue()).read_flat() |
| 211 self._width = width |
| 212 self._height = height |
| 213 self._pixels = pixels |
| 214 self._metadata = meta |
| 215 finally: |
| 216 output.close() |
| 217 |
197 return self | 218 return self |
198 | 219 |
199 def ColorHistogram(self): | 220 def ColorHistogram(self): |
200 """Computes a histogram of the pixel colors in this Bitmap. | 221 """Returns a histogram of the pixel colors in this Bitmap.""" |
201 Returns a list of 3x256 integers.""" | 222 # TODO(szym): Implement this. |
202 return bitmaptools.Histogram(self._as_tuple) | 223 raise NotImplementedError("ColorHistogram not yet implemented.") |
OLD | NEW |