Chromium Code Reviews| 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 array | |
|
M-A Ruel
2014/01/17 14:01:37
insert an empty line
What's the purpose of this f
szym
2014/01/17 20:38:20
Done.
| |
| 4 import base64 | 5 import base64 |
| 5 import cStringIO | 6 import cStringIO |
| 7 import struct | |
| 8 import subprocess | |
| 9 import sys | |
| 6 | 10 |
| 7 from telemetry.core import util | 11 from telemetry.core import util |
| 8 | 12 |
| 9 util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') | 13 util.AddDirToPythonPath(util.GetTelemetryDir(), 'third_party', 'png') |
| 10 import png # pylint: disable=F0401 | 14 import png # pylint: disable=F0401 |
| 11 | 15 |
| 12 | 16 |
| 13 class RgbaColor(object): | 17 class RgbaColor(object): |
| 14 """Encapsulates an RGBA color retreived from a Bitmap""" | 18 """Encapsulates an RGBA color retreived from a Bitmap""" |
| 15 | 19 |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 36 assert self.IsEqual(RgbaColor(r, g, b), tolerance) | 40 assert self.IsEqual(RgbaColor(r, g, b), tolerance) |
| 37 | 41 |
| 38 def AssertIsRGBA(self, r, g, b, a, tolerance=0): | 42 def AssertIsRGBA(self, r, g, b, a, tolerance=0): |
| 39 assert self.IsEqual(RgbaColor(r, g, b, a), tolerance) | 43 assert self.IsEqual(RgbaColor(r, g, b, a), tolerance) |
| 40 | 44 |
| 41 | 45 |
| 42 WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) | 46 WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) |
| 43 WHITE = RgbaColor(255, 255, 255) | 47 WHITE = RgbaColor(255, 255, 255) |
| 44 | 48 |
| 45 | 49 |
| 50 class _BitmapTools(object): | |
| 51 CROP_PIXELS = 0 | |
| 52 HISTOGRAM = 1 | |
| 53 BOUNDING_BOX = 2 | |
| 54 | |
| 55 def __init__(self, bitmap): | |
| 56 suffix = '.exe' if sys.platform == 'win32' else '' | |
| 57 binary = util.FindSupportBinary('bitmaptools' + suffix) | |
| 58 assert binary, 'You must build bitmaptools first!' | |
| 59 | |
| 60 self.popen = subprocess.Popen([binary], | |
| 61 stdin=subprocess.PIPE, | |
| 62 stdout=subprocess.PIPE, | |
| 63 stderr=subprocess.PIPE) | |
| 64 dimensions, pixels = bitmap | |
| 65 # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight | |
| 66 packed_dims = struct.pack('iiiiiii', *dimensions) | |
| 67 self.popen.stdin.write(packed_dims) | |
| 68 self.popen.stdin.write(pixels) | |
|
bulach
2014/01/17 10:43:44
nit: perhaps a self.popen.flush() here?
M-A Ruel
2014/01/17 14:01:37
well, keep in mind this code could dead lock.
szym
2014/01/17 20:38:20
We don't read from popen.stdout until _RunCommand
| |
| 69 | |
| 70 def _RunCommand(self, *command): | |
| 71 packed_command = struct.pack('i' * len(command), *command) | |
| 72 self.popen.stdin.write(packed_command) | |
| 73 self.popen.stdin.close() | |
|
M-A Ruel
2014/01/17 14:01:37
So the object can only have one call done on it an
szym
2014/01/17 20:38:20
I want to reuse _RunCommand and __init__. I tried
| |
| 74 length_packed = self.popen.stdout.read(struct.calcsize('i')) | |
| 75 if not length_packed: | |
| 76 raise Exception(self.popen.stderr.read()) | |
| 77 length = struct.unpack('i', length_packed)[0] | |
| 78 return self.popen.stdout.read(length) | |
| 79 | |
| 80 def CropPixels(self): | |
| 81 return self._RunCommand(_BitmapTools.CROP_PIXELS) | |
| 82 | |
| 83 def Histogram(self, ignore_color, tolerance): | |
| 84 ignore_color = -1 if ignore_color is None else int(ignore_color) | |
| 85 response = self._RunCommand(_BitmapTools.HISTOGRAM, ignore_color, tolerance) | |
| 86 out = array.array('i') | |
| 87 out.fromstring(response) | |
| 88 return out | |
| 89 | |
| 90 def BoundingBox(self, color, tolerance): | |
| 91 response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color), | |
| 92 tolerance) | |
| 93 unpacked = struct.unpack('iiiii', response) | |
| 94 box, count = unpacked[:4], unpacked[-1] | |
| 95 if box[2] < 0 or box[3] < 0: | |
| 96 box = None | |
| 97 return box, count | |
| 98 | |
| 99 | |
| 46 class Bitmap(object): | 100 class Bitmap(object): |
| 47 """Utilities for parsing and inspecting a bitmap.""" | 101 """Utilities for parsing and inspecting a bitmap.""" |
| 48 | 102 |
| 49 def __init__(self, bpp, width, height, pixels, metadata=None): | 103 def __init__(self, bpp, width, height, pixels, metadata=None): |
| 50 assert bpp in [3, 4], 'Invalid bytes per pixel' | 104 assert bpp in [3, 4], 'Invalid bytes per pixel' |
| 51 assert width > 0, 'Invalid width' | 105 assert width > 0, 'Invalid width' |
| 52 assert height > 0, 'Invalid height' | 106 assert height > 0, 'Invalid height' |
| 53 assert pixels, 'Must specify pixels' | 107 assert pixels, 'Must specify pixels' |
| 54 assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' | 108 assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' |
| 55 | 109 |
| 56 self._bpp = bpp | 110 self._bpp = bpp |
| 57 self._width = width | 111 self._width = width |
| 58 self._height = height | 112 self._height = height |
| 59 self._pixels = pixels | 113 self._pixels = pixels |
| 60 self._metadata = metadata or {} | 114 self._metadata = metadata or {} |
| 115 self._crop_box = None | |
| 61 | 116 |
| 62 @property | 117 @property |
| 63 def bpp(self): | 118 def bpp(self): |
| 64 """Bytes per pixel.""" | 119 """Bytes per pixel.""" |
| 65 return self._bpp | 120 return self._bpp |
| 66 | 121 |
| 67 @property | 122 @property |
| 68 def width(self): | 123 def width(self): |
| 69 """Width of the bitmap.""" | 124 """Width of the bitmap.""" |
| 125 if self._crop_box: | |
|
M-A Ruel
2014/01/17 14:01:37
optionally, if you want to save space, you do this
szym
2014/01/17 20:38:20
Done.
| |
| 126 return self._crop_box[2] | |
| 70 return self._width | 127 return self._width |
| 71 | 128 |
| 72 @property | 129 @property |
| 73 def height(self): | 130 def height(self): |
| 74 """Height of the bitmap.""" | 131 """Height of the bitmap.""" |
| 132 if self._crop_box: | |
| 133 return self._crop_box[3] | |
| 75 return self._height | 134 return self._height |
| 76 | 135 |
| 77 @property | 136 @property |
| 137 def _packed(self): | |
| 138 # If we got a list of ints, we need to convert it into a byte buffer. | |
|
M-A Ruel
2014/01/17 14:01:37
As a docstring?
szym
2014/01/17 20:38:20
This part describes just the |pixels| conversion,
| |
| 139 pixels = self._pixels | |
| 140 if type(pixels) is not bytearray: | |
| 141 pixels = bytearray(pixels) | |
| 142 if type(pixels) is not bytes: | |
| 143 pixels = bytes(pixels) | |
| 144 crop_box = self._crop_box or (0, 0, self._width, self._height) | |
| 145 return (self._bpp, self._width, self._height) + crop_box, pixels | |
| 146 | |
| 147 @property | |
| 78 def pixels(self): | 148 def pixels(self): |
| 79 """Flat pixel array of the bitmap.""" | 149 """Flat pixel array of the bitmap.""" |
| 150 if self._crop_box: | |
| 151 self._pixels = _BitmapTools(self._packed).CropPixels() | |
| 152 _, _, self._width, self._height = self._crop_box | |
| 153 self._crop_box = None | |
| 80 if type(self._pixels) is not bytearray: | 154 if type(self._pixels) is not bytearray: |
| 81 self._pixels = bytearray(self._pixels) | 155 self._pixels = bytearray(self._pixels) |
| 82 return self._pixels | 156 return self._pixels |
| 83 | 157 |
| 84 @property | 158 @property |
| 85 def metadata(self): | 159 def metadata(self): |
| 86 self._metadata['size'] = (self.width, self.height) | 160 self._metadata['size'] = (self.width, self.height) |
| 87 self._metadata['alpha'] = self.bpp == 4 | 161 self._metadata['alpha'] = self.bpp == 4 |
| 88 self._metadata['bitdepth'] = 8 | 162 self._metadata['bitdepth'] = 8 |
| 89 return self._metadata | 163 return self._metadata |
| 90 | 164 |
| 91 def GetPixelColor(self, x, y): | 165 def GetPixelColor(self, x, y): |
| 92 """Returns a RgbaColor for the pixel at (x, y).""" | 166 """Returns a RgbaColor for the pixel at (x, y).""" |
| 167 pixels = self.pixels | |
| 93 base = self._bpp * (y * self._width + x) | 168 base = self._bpp * (y * self._width + x) |
| 94 if self._bpp == 4: | 169 if self._bpp == 4: |
| 95 return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], | 170 return RgbaColor(pixels[base + 0], pixels[base + 1], |
| 96 self._pixels[base + 2], self._pixels[base + 3]) | 171 pixels[base + 2], pixels[base + 3]) |
| 97 return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], | 172 return RgbaColor(pixels[base + 0], pixels[base + 1], |
| 98 self._pixels[base + 2]) | 173 pixels[base + 2]) |
| 99 | 174 |
| 100 def WritePngFile(self, path): | 175 def WritePngFile(self, path): |
| 101 with open(path, "wb") as f: | 176 with open(path, "wb") as f: |
| 102 png.Writer(**self.metadata).write_array(f, self.pixels) | 177 png.Writer(**self.metadata).write_array(f, self.pixels) |
| 103 | 178 |
| 104 @staticmethod | 179 @staticmethod |
| 105 def FromPng(png_data): | 180 def FromPng(png_data): |
| 106 width, height, pixels, meta = png.Reader(bytes=png_data).read_flat() | 181 width, height, pixels, meta = png.Reader(bytes=png_data).read_flat() |
| 107 return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta) | 182 return Bitmap(4 if meta['alpha'] else 3, width, height, pixels, meta) |
| 108 | 183 |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 172 diff = Bitmap.FromPng(output.getvalue()) | 247 diff = Bitmap.FromPng(output.getvalue()) |
| 173 finally: | 248 finally: |
| 174 output.close() | 249 output.close() |
| 175 | 250 |
| 176 return diff | 251 return diff |
| 177 | 252 |
| 178 def GetBoundingBox(self, color, tolerance=0): | 253 def GetBoundingBox(self, color, tolerance=0): |
| 179 """Finds the minimum box surrounding all occurences of |color|. | 254 """Finds the minimum box surrounding all occurences of |color|. |
| 180 Returns: (top, left, width, height), match_count | 255 Returns: (top, left, width, height), match_count |
| 181 Ignores the alpha channel.""" | 256 Ignores the alpha channel.""" |
| 182 # TODO(szym): Implement this. | 257 return _BitmapTools(self._packed).BoundingBox(color, tolerance) |
| 183 raise NotImplementedError("GetBoundingBox not yet implemented.") | |
| 184 | 258 |
| 185 def Crop(self, left, top, width, height): | 259 def Crop(self, left, top, width, height): |
| 186 """Crops the current bitmap down to the specified box. | 260 """Crops the current bitmap down to the specified box.""" |
| 187 TODO(szym): Make this O(1). | 261 cur_box = self._crop_box or (0, 0, self._width, self._height) |
| 188 """ | 262 cur_left, cur_top, cur_width, cur_height = cur_box |
| 263 | |
| 189 if (left < 0 or top < 0 or | 264 if (left < 0 or top < 0 or |
| 190 (left + width) > self.width or | 265 (left + width) > cur_width or |
| 191 (top + height) > self.height): | 266 (top + height) > cur_height): |
| 192 raise ValueError('Invalid dimensions') | 267 raise ValueError('Invalid dimensions') |
| 193 | 268 |
| 194 img_data = [[0 for x in xrange(width * self.bpp)] | 269 self._crop_box = cur_left + left, cur_top + top, width, height |
| 195 for y in xrange(height)] | |
| 196 | |
| 197 # Copy each pixel in the sub-rect. | |
| 198 # TODO(tonyg): Make this faster by avoiding the copy and artificially | |
| 199 # restricting the dimensions. | |
| 200 for y in range(height): | |
| 201 for x in range(width): | |
| 202 c = self.GetPixelColor(x + left, y + top) | |
| 203 offset = x * self.bpp | |
| 204 img_data[y][offset] = c.r | |
| 205 img_data[y][offset + 1] = c.g | |
| 206 img_data[y][offset + 2] = c.b | |
| 207 if self.bpp == 4: | |
| 208 img_data[y][offset + 3] = c.a | |
| 209 | |
| 210 # This particular method can only save to a file, so the result will be | |
| 211 # written into an in-memory buffer and read back into a Bitmap | |
| 212 crop_img = png.from_array(img_data, mode='RGBA' if self.bpp == 4 else 'RGB') | |
| 213 output = cStringIO.StringIO() | |
| 214 try: | |
| 215 crop_img.save(output) | |
| 216 width, height, pixels, meta = png.Reader( | |
| 217 bytes=output.getvalue()).read_flat() | |
| 218 self._width = width | |
| 219 self._height = height | |
| 220 self._pixels = pixels | |
| 221 self._metadata = meta | |
| 222 finally: | |
| 223 output.close() | |
| 224 | |
| 225 return self | 270 return self |
| 226 | 271 |
| 227 def ColorHistogram(self, ignore_color=None, tolerance=0): | 272 def ColorHistogram(self, ignore_color=None, tolerance=0): |
| 228 """Computes a histogram of the pixel colors in this Bitmap. | 273 """Computes a histogram of the pixel colors in this Bitmap. |
| 229 Args: | 274 Args: |
| 230 ignore_color: An RgbaColor to exclude from the bucket counts. | 275 ignore_color: An RgbaColor to exclude from the bucket counts. |
| 231 tolerance: A tolerance for the ignore_color. | 276 tolerance: A tolerance for the ignore_color. |
| 232 | 277 |
| 233 Returns: | 278 Returns: |
| 234 A list of 3x256 integers formatted as | 279 A list of 3x256 integers formatted as |
| 235 [r0, r1, ..., g0, g1, ..., b0, b1, ...]. | 280 [r0, r1, ..., g0, g1, ..., b0, b1, ...]. |
| 236 """ | 281 """ |
| 237 # TODO(szym): Implement this. | 282 return _BitmapTools(self._packed).Histogram(ignore_color, tolerance) |
| 238 raise NotImplementedError("ColorHistogram not yet implemented.") | |
| OLD | NEW |