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 |