Chromium Code Reviews| Index: tools/telemetry/telemetry/core/bitmap.py |
| diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py |
| index 8c52d3b2848649b2a279c24f7c472ce14ea90711..72b1a59ba62abdaaf8ffeb4c8cb16060f6e4e343 100644 |
| --- a/tools/telemetry/telemetry/core/bitmap.py |
| +++ b/tools/telemetry/telemetry/core/bitmap.py |
| @@ -1,8 +1,12 @@ |
| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| +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.
|
| import base64 |
| import cStringIO |
| +import struct |
| +import subprocess |
| +import sys |
| from telemetry.core import util |
| @@ -43,6 +47,56 @@ WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) |
| WHITE = RgbaColor(255, 255, 255) |
| +class _BitmapTools(object): |
| + CROP_PIXELS = 0 |
| + HISTOGRAM = 1 |
| + BOUNDING_BOX = 2 |
| + |
| + def __init__(self, bitmap): |
| + suffix = '.exe' if sys.platform == 'win32' else '' |
| + binary = util.FindSupportBinary('bitmaptools' + suffix) |
| + assert binary, 'You must build bitmaptools first!' |
| + |
| + self.popen = subprocess.Popen([binary], |
| + stdin=subprocess.PIPE, |
| + stdout=subprocess.PIPE, |
| + stderr=subprocess.PIPE) |
| + dimensions, pixels = bitmap |
| + # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight |
| + packed_dims = struct.pack('iiiiiii', *dimensions) |
| + self.popen.stdin.write(packed_dims) |
| + 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
|
| + |
| + def _RunCommand(self, *command): |
| + packed_command = struct.pack('i' * len(command), *command) |
| + self.popen.stdin.write(packed_command) |
| + 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
|
| + length_packed = self.popen.stdout.read(struct.calcsize('i')) |
| + if not length_packed: |
| + raise Exception(self.popen.stderr.read()) |
| + length = struct.unpack('i', length_packed)[0] |
| + return self.popen.stdout.read(length) |
| + |
| + def CropPixels(self): |
| + return self._RunCommand(_BitmapTools.CROP_PIXELS) |
| + |
| + def Histogram(self, ignore_color, tolerance): |
| + ignore_color = -1 if ignore_color is None else int(ignore_color) |
| + response = self._RunCommand(_BitmapTools.HISTOGRAM, ignore_color, tolerance) |
| + out = array.array('i') |
| + out.fromstring(response) |
| + return out |
| + |
| + def BoundingBox(self, color, tolerance): |
| + response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color), |
| + tolerance) |
| + unpacked = struct.unpack('iiiii', response) |
| + box, count = unpacked[:4], unpacked[-1] |
| + if box[2] < 0 or box[3] < 0: |
| + box = None |
| + return box, count |
| + |
| + |
| class Bitmap(object): |
| """Utilities for parsing and inspecting a bitmap.""" |
| @@ -58,6 +112,7 @@ class Bitmap(object): |
| self._height = height |
| self._pixels = pixels |
| self._metadata = metadata or {} |
| + self._crop_box = None |
| @property |
| def bpp(self): |
| @@ -67,16 +122,35 @@ class Bitmap(object): |
| @property |
| def width(self): |
| """Width of the bitmap.""" |
| + 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.
|
| + return self._crop_box[2] |
| return self._width |
| @property |
| def height(self): |
| """Height of the bitmap.""" |
| + if self._crop_box: |
| + return self._crop_box[3] |
| return self._height |
| @property |
| + def _packed(self): |
| + # 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,
|
| + pixels = self._pixels |
| + if type(pixels) is not bytearray: |
| + pixels = bytearray(pixels) |
| + if type(pixels) is not bytes: |
| + pixels = bytes(pixels) |
| + crop_box = self._crop_box or (0, 0, self._width, self._height) |
| + return (self._bpp, self._width, self._height) + crop_box, pixels |
| + |
| + @property |
| def pixels(self): |
| """Flat pixel array of the bitmap.""" |
| + if self._crop_box: |
| + self._pixels = _BitmapTools(self._packed).CropPixels() |
| + _, _, self._width, self._height = self._crop_box |
| + self._crop_box = None |
| if type(self._pixels) is not bytearray: |
| self._pixels = bytearray(self._pixels) |
| return self._pixels |
| @@ -90,12 +164,13 @@ class Bitmap(object): |
| def GetPixelColor(self, x, y): |
| """Returns a RgbaColor for the pixel at (x, y).""" |
| + pixels = self.pixels |
| base = self._bpp * (y * self._width + x) |
| if self._bpp == 4: |
| - return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
| - self._pixels[base + 2], self._pixels[base + 3]) |
| - return RgbaColor(self._pixels[base + 0], self._pixels[base + 1], |
| - self._pixels[base + 2]) |
| + return RgbaColor(pixels[base + 0], pixels[base + 1], |
| + pixels[base + 2], pixels[base + 3]) |
| + return RgbaColor(pixels[base + 0], pixels[base + 1], |
| + pixels[base + 2]) |
| def WritePngFile(self, path): |
| with open(path, "wb") as f: |
| @@ -179,49 +254,19 @@ class Bitmap(object): |
| """Finds the minimum box surrounding all occurences of |color|. |
| Returns: (top, left, width, height), match_count |
| Ignores the alpha channel.""" |
| - # TODO(szym): Implement this. |
| - raise NotImplementedError("GetBoundingBox not yet implemented.") |
| + return _BitmapTools(self._packed).BoundingBox(color, tolerance) |
| def Crop(self, left, top, width, height): |
| - """Crops the current bitmap down to the specified box. |
| - TODO(szym): Make this O(1). |
| - """ |
| + """Crops the current bitmap down to the specified box.""" |
| + cur_box = self._crop_box or (0, 0, self._width, self._height) |
| + cur_left, cur_top, cur_width, cur_height = cur_box |
| + |
| if (left < 0 or top < 0 or |
| - (left + width) > self.width or |
| - (top + height) > self.height): |
| + (left + width) > cur_width or |
| + (top + height) > cur_height): |
| raise ValueError('Invalid dimensions') |
| - img_data = [[0 for x in xrange(width * self.bpp)] |
| - for y in xrange(height)] |
| - |
| - # Copy each pixel in the sub-rect. |
| - # TODO(tonyg): Make this faster by avoiding the copy and artificially |
| - # restricting the dimensions. |
| - for y in range(height): |
| - for x in range(width): |
| - c = self.GetPixelColor(x + left, y + top) |
| - offset = x * self.bpp |
| - img_data[y][offset] = c.r |
| - img_data[y][offset + 1] = c.g |
| - img_data[y][offset + 2] = c.b |
| - if self.bpp == 4: |
| - img_data[y][offset + 3] = c.a |
| - |
| - # This particular method can only save to a file, so the result will be |
| - # written into an in-memory buffer and read back into a Bitmap |
| - crop_img = png.from_array(img_data, mode='RGBA' if self.bpp == 4 else 'RGB') |
| - output = cStringIO.StringIO() |
| - try: |
| - crop_img.save(output) |
| - width, height, pixels, meta = png.Reader( |
| - bytes=output.getvalue()).read_flat() |
| - self._width = width |
| - self._height = height |
| - self._pixels = pixels |
| - self._metadata = meta |
| - finally: |
| - output.close() |
| - |
| + self._crop_box = cur_left + left, cur_top + top, width, height |
| return self |
| def ColorHistogram(self, ignore_color=None, tolerance=0): |
| @@ -234,5 +279,4 @@ class Bitmap(object): |
| A list of 3x256 integers formatted as |
| [r0, r1, ..., g0, g1, ..., b0, b1, ...]. |
| """ |
| - # TODO(szym): Implement this. |
| - raise NotImplementedError("ColorHistogram not yet implemented.") |
| + return _BitmapTools(self._packed).Histogram(ignore_color, tolerance) |