summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorszym@chromium.org <szym@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-01-22 17:49:55 +0000
committerszym@chromium.org <szym@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-01-22 17:49:55 +0000
commit90f69904f78bd1584d8f36a140cb2f4a728cc39d (patch)
tree8e74b331db5d71dfcce84e921a66e9575cdbd0e6
parent62f989801281c55025514a69b606b95e6ea48aa0 (diff)
downloadchromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.zip
chromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.tar.gz
chromium_src-90f69904f78bd1584d8f36a140cb2f4a728cc39d.tar.bz2
[telemetry] bitmaptools as a standalone executable
The C++ binary implements simple per-pixel algorithms for SpeedIndex computation. This allows us to achieve near real-time processing without bringing external dependencies. The bitmaptools binary needs to be built before it can be used. The overhead of spawning a child process for each frame is about 3ms. BUG=323813 TEST=telemetry bitmap_unittest Original review: https://codereview.chromium.org/136793022 TBR=tonyg,maruel Review URL: https://codereview.chromium.org/131563009 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@246361 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--PRESUBMIT.py2
-rw-r--r--build/all.gyp5
-rw-r--r--build/all_android.gyp1
-rw-r--r--chrome/telemetry.isolate1
-rw-r--r--tools/android/android_tools.gyp1
-rw-r--r--tools/telemetry/telemetry.gyp16
-rw-r--r--tools/telemetry/telemetry/core/bitmap.py140
-rw-r--r--tools/telemetry/telemetry/core/bitmap_unittest.py12
-rw-r--r--tools/telemetry/telemetry/core/bitmaptools.cc264
9 files changed, 391 insertions, 51 deletions
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 9407609..8f024e9 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -901,6 +901,8 @@ def _CheckSpamLogging(input_api, output_api):
r"^remoting[\\\/]base[\\\/]logging\.h$",
r"^remoting[\\\/]host[\\\/].*",
r"^sandbox[\\\/]linux[\\\/].*",
+ r"^tools[\\\/]telemetry[\\\/]telemetry[\\\/]core[\\\/]"
+ r"bitmaptools.cc$",
r"^ui[\\\/]aura[\\\/]bench[\\\/]bench_main\.cc$",))
source_file_filter = lambda x: input_api.FilterSourceFile(
x, white_list=(file_inclusion_pattern,), black_list=black_list)
diff --git a/build/all.gyp b/build/all.gyp
index 22d8615..1bf93d72 100644
--- a/build/all.gyp
+++ b/build/all.gyp
@@ -69,6 +69,7 @@
'../third_party/qcms/qcms.gyp:*',
'../tools/gn/gn.gyp:*',
'../tools/perf/clear_system_cache/clear_system_cache.gyp:*',
+ '../tools/telemetry/telemetry.gyp:*',
'../v8/tools/gyp/v8.gyp:*',
'../webkit/glue/webkit_glue.gyp:*',
'../webkit/renderer/compositor_bindings/compositor_bindings_tests.gyp:*',
@@ -265,6 +266,7 @@
'../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_unittests',
'../third_party/libaddressinput/libaddressinput.gyp:libaddressinput_unittests',
'../third_party/libphonenumber/libphonenumber.gyp:libphonenumber_unittests',
+ '../tools/telemetry/telemetry.gyp:*',
'../webkit/renderer/compositor_bindings/compositor_bindings_tests.gyp:webkit_compositor_bindings_unittests',
],
}],
@@ -436,6 +438,7 @@
'../chrome/chrome.gyp:sync_performance_tests',
'../media/media.gyp:media_perftests',
'../tools/perf/clear_system_cache/clear_system_cache.gyp:*',
+ '../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
@@ -472,6 +475,7 @@
'../gpu/gles2_conform_support/gles2_conform_test.gyp:gles2_conform_test',
'../gpu/gpu.gyp:gl_tests',
'../gpu/gpu.gyp:angle_unittests',
+ '../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
@@ -506,6 +510,7 @@
'../gpu/gles2_conform_support/gles2_conform_test.gyp:gles2_conform_test',
'../gpu/gpu.gyp:gl_tests',
'../gpu/gpu.gyp:angle_unittests',
+ '../tools/telemetry/telemetry.gyp:*',
],
'conditions': [
['OS!="ios" and OS!="win"', {
diff --git a/build/all_android.gyp b/build/all_android.gyp
index aa58d65..e60c14e 100644
--- a/build/all_android.gyp
+++ b/build/all_android.gyp
@@ -28,6 +28,7 @@
# the sync-related code for Android has been upstreamed.
# See http://crbug.com/159203
'../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_javalib',
+ '../tools/telemetry/telemetry.gyp:*#host',
],
}, # target_name: All
{
diff --git a/chrome/telemetry.isolate b/chrome/telemetry.isolate
index 7a98160..1c09db8 100644
--- a/chrome/telemetry.isolate
+++ b/chrome/telemetry.isolate
@@ -25,6 +25,7 @@
'../third_party/WebKit/PerformanceTests/resources/statistics.js',
'../third_party/flot/jquery.flot.min.js',
'../tools/perf/unit-info.json',
+ '<(PRODUCT_DIR)/bitmaptools<(EXECUTABLE_SUFFIX)',
],
'isolate_dependency_untracked': [
'../build/android/pylib/',
diff --git a/tools/android/android_tools.gyp b/tools/android/android_tools.gyp
index ed5e905..b780428 100644
--- a/tools/android/android_tools.gyp
+++ b/tools/android/android_tools.gyp
@@ -14,6 +14,7 @@
'forwarder2/forwarder.gyp:forwarder2',
'md5sum/md5sum.gyp:md5sum',
'purge_ashmem/purge_ashmem.gyp:purge_ashmem',
+ '../../tools/telemetry/telemetry.gyp:*#host',
],
},
{
diff --git a/tools/telemetry/telemetry.gyp b/tools/telemetry/telemetry.gyp
new file mode 100644
index 0000000..150f76c
--- /dev/null
+++ b/tools/telemetry/telemetry.gyp
@@ -0,0 +1,16 @@
+# Copyright 2014 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.
+
+{
+ 'targets': [
+ {
+ 'target_name': 'bitmaptools',
+ 'type': 'executable',
+ 'sources': [
+ 'telemetry/core/bitmaptools.cc',
+ ],
+ 'toolsets': ['host'],
+ },
+ ],
+}
diff --git a/tools/telemetry/telemetry/core/bitmap.py b/tools/telemetry/telemetry/core/bitmap.py
index 8c52d3b..9b4c163 100644
--- a/tools/telemetry/telemetry/core/bitmap.py
+++ b/tools/telemetry/telemetry/core/bitmap.py
@@ -1,8 +1,18 @@
# 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.
+
+"""
+Bitmap is a basic wrapper for image pixels. It includes some basic processing
+tools: crop, find bounding box of a color and compute histogram of color values.
+"""
+
+import array
import base64
import cStringIO
+import struct
+import subprocess
+import sys
from telemetry.core import util
@@ -43,6 +53,62 @@ WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13)
WHITE = RgbaColor(255, 255, 255)
+class _BitmapTools(object):
+ """Wraps a child process of bitmaptools and allows for one command."""
+ CROP_PIXELS = 0
+ HISTOGRAM = 1
+ BOUNDING_BOX = 2
+
+ def __init__(self, dimensions, pixels):
+ 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 are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight
+ packed_dims = struct.pack('iiiiiii', *dimensions)
+ self._popen.stdin.write(packed_dims)
+ # If we got a list of ints, we need to convert it into a byte buffer.
+ if type(pixels) is not bytearray:
+ pixels = bytearray(pixels)
+ self._popen.stdin.write(pixels)
+
+ def _RunCommand(self, *command):
+ assert not self._popen.stdin.closed, (
+ 'Exactly one command allowed per instance of tools.')
+ packed_command = struct.pack('i' * len(command), *command)
+ self._popen.stdin.write(packed_command)
+ self._popen.stdin.close()
+ 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 +124,7 @@ class Bitmap(object):
self._height = height
self._pixels = pixels
self._metadata = metadata or {}
+ self._crop_box = None
@property
def bpp(self):
@@ -67,16 +134,27 @@ class Bitmap(object):
@property
def width(self):
"""Width of the bitmap."""
- return self._width
+ return self._crop_box[2] if self._crop_box else self._width
@property
def height(self):
"""Height of the bitmap."""
- return self._height
+ return self._crop_box[3] if self._crop_box else self._height
+
+ def _PrepareTools(self):
+ """Prepares an instance of _BitmapTools which allows exactly one command.
+ """
+ crop_box = self._crop_box or (0, 0, self._width, self._height)
+ return _BitmapTools((self._bpp, self._width, self._height) + crop_box,
+ self._pixels)
@property
def pixels(self):
"""Flat pixel array of the bitmap."""
+ if self._crop_box:
+ self._pixels = self._PrepareTools().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 +168,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 +258,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 self._PrepareTools().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 +283,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 self._PrepareTools().Histogram(ignore_color, tolerance)
diff --git a/tools/telemetry/telemetry/core/bitmap_unittest.py b/tools/telemetry/telemetry/core/bitmap_unittest.py
index b0b01f5..095cc9a 100644
--- a/tools/telemetry/telemetry/core/bitmap_unittest.py
+++ b/tools/telemetry/telemetry/core/bitmap_unittest.py
@@ -8,7 +8,7 @@ import unittest
from telemetry.core import bitmap
from telemetry.core import util
-from telemetry.unittest import DisabledTest
+from telemetry.unittest import DisabledTestOnCrOS
# This is a simple base64 encoded 2x2 PNG which contains, in order, a single
# Red, Yellow, Blue, and Green pixel.
@@ -57,6 +57,7 @@ class BitmapTest(unittest.TestCase):
new_file = bitmap.Bitmap.FromPngFile(temp_file)
self.assertTrue(orig.IsEqual(new_file))
+ @DisabledTestOnCrOS
def testWriteCroppedBmpToPngFile(self):
pixels = [255,0,0, 255,255,0, 0,0,0,
255,255,0, 0,255,0, 0,0,0]
@@ -102,7 +103,7 @@ class BitmapTest(unittest.TestCase):
diff_bmp.GetPixelColor(2, 1).AssertIsRGB(255, 255, 255)
diff_bmp.GetPixelColor(2, 2).AssertIsRGB(255, 255, 255)
- @DisabledTest
+ @DisabledTestOnCrOS
def testGetBoundingBox(self):
pixels = [0,0,0, 0,0,0, 0,0,0, 0,0,0,
0,0,0, 1,0,0, 1,0,0, 0,0,0,
@@ -116,6 +117,7 @@ class BitmapTest(unittest.TestCase):
self.assertEquals(box, None)
self.assertEquals(count, 0)
+ @DisabledTestOnCrOS
def testCrop(self):
pixels = [0,0,0, 1,0,0, 2,0,0, 3,0,0,
0,1,0, 1,1,0, 2,1,0, 3,1,0,
@@ -129,7 +131,7 @@ class BitmapTest(unittest.TestCase):
bmp.GetPixelColor(1, 0).AssertIsRGB(2, 2, 0)
self.assertEquals(bmp.pixels, bytearray([1,2,0, 2,2,0]))
- @DisabledTest
+ @DisabledTestOnCrOS
def testHistogram(self):
pixels = [1,2,3, 1,2,3, 1,2,3, 1,2,3,
1,2,3, 8,7,6, 5,4,6, 1,2,3,
@@ -148,7 +150,7 @@ class BitmapTest(unittest.TestCase):
self.assertEquals(histogram[3 + 512], 0)
self.assertEquals(histogram[6 + 512], 4)
- @DisabledTest
+ @DisabledTestOnCrOS
def testHistogramIgnoreColor(self):
pixels = [1,2,3, 1,2,3, 1,2,3, 1,2,3,
1,2,3, 8,7,6, 5,4,6, 1,2,3,
@@ -165,7 +167,7 @@ class BitmapTest(unittest.TestCase):
self.assertEquals(histogram[3 + 512], 0)
self.assertEquals(histogram[6 + 512], 4)
- @DisabledTest
+ @DisabledTestOnCrOS
def testHistogramIgnoreColorTolerance(self):
pixels = [1,2,3, 4,5,6,
7,8,9, 8,7,6]
diff --git a/tools/telemetry/telemetry/core/bitmaptools.cc b/tools/telemetry/telemetry/core/bitmaptools.cc
new file mode 100644
index 0000000..ba4a8a5
--- /dev/null
+++ b/tools/telemetry/telemetry/core/bitmaptools.cc
@@ -0,0 +1,264 @@
+// Copyright 2014 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.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(WIN32)
+#include <fcntl.h>
+#include <io.h>
+#endif
+
+enum Commands {
+ CROP_PIXELS = 0,
+ HISTOGRAM = 1,
+ BOUNDING_BOX = 2
+};
+
+bool ReadInt(int* out) {
+ return fread(out, sizeof(*out), 1, stdin) == 1;
+}
+
+void WriteResponse(void* data, int size) {
+ fwrite(&size, sizeof(size), 1, stdout);
+ fwrite(data, size, 1, stdout);
+ fflush(stdout);
+}
+
+struct Box {
+ Box() : left(), top(), right(), bottom() {}
+
+ // Expected input is:
+ // left, top, width, height
+ bool Read() {
+ int width;
+ int height;
+ if (!(ReadInt(&left) && ReadInt(&top) &&
+ ReadInt(&width) && ReadInt(&height))) {
+ fprintf(stderr, "Could not parse Box.\n");
+ return false;
+ }
+ if (left < 0 || top < 0 || width < 0 || height < 0) {
+ fprintf(stderr, "Box dimensions must be non-negative.\n");
+ return false;
+ }
+ right = left + width;
+ bottom = top + height;
+ return true;
+ }
+
+ void Union(int x, int y) {
+ if (left > x) left = x;
+ if (right <= x) right = x + 1;
+ if (top > y) top = y;
+ if (bottom <= y) bottom = y + 1;
+ }
+
+ int width() const { return right - left; }
+ int height() const { return bottom - top; }
+
+ int left;
+ int top;
+ int right;
+ int bottom;
+};
+
+
+// Represents a bitmap buffer with a crop box.
+struct Bitmap {
+ Bitmap() : pixels(NULL) {}
+
+ ~Bitmap() {
+ if (pixels)
+ delete[] pixels;
+ }
+
+ // Expected input is:
+ // bpp, width, height, box, pixels
+ bool Read() {
+ int bpp;
+ int width;
+ int height;
+ if (!(ReadInt(&bpp) && ReadInt(&width) && ReadInt(&height))) {
+ fprintf(stderr, "Could not parse Bitmap initializer.\n");
+ return false;
+ }
+ if (bpp <= 0 || width <= 0 || height <= 0) {
+ fprintf(stderr, "Dimensions must be positive.\n");
+ return false;
+ }
+
+ int size = width * height * bpp;
+
+ row_stride = width * bpp;
+ pixel_stride = bpp;
+ total_size = size;
+ row_size = row_stride;
+
+ if (!box.Read()) {
+ fprintf(stderr, "Expected crop box argument not found.\n");
+ return false;
+ }
+
+ if (box.bottom * row_stride > total_size ||
+ box.right * pixel_stride > row_size) {
+ fprintf(stderr, "Crop box overflows the bitmap.\n");
+ return false;
+ }
+
+ pixels = new unsigned char[size];
+ if (fread(pixels, sizeof(pixels[0]), size, stdin) <
+ static_cast<size_t>(size)) {
+ fprintf(stderr, "Not enough pixels found,\n");
+ return false;
+ }
+
+ total_size = (box.bottom - box.top) * row_stride;
+ row_size = (box.right - box.left) * pixel_stride;
+ data = pixels + box.top * row_stride + box.left * pixel_stride;
+ return true;
+ }
+
+ void WriteCroppedPixels() const {
+ int out_size = row_size * box.height();
+ unsigned char* out = new unsigned char[out_size];
+ unsigned char* dst = out;
+ for (const unsigned char* row = data;
+ row < data + total_size;
+ row += row_stride, dst += row_size) {
+ // No change in pixel_stride, so we can copy whole rows.
+ memcpy(dst, row, row_size);
+ }
+
+ WriteResponse(out, out_size);
+ delete[] out;
+ }
+
+ unsigned char* pixels;
+ Box box;
+ // Points at the top-left pixel in |pixels|.
+ const unsigned char* data;
+ // These counts are in bytes.
+ int row_stride;
+ int pixel_stride;
+ int total_size;
+ int row_size;
+};
+
+
+static inline
+bool PixelsEqual(const unsigned char* pixel1, const unsigned char* pixel2,
+ int tolerance) {
+ // Note: this works for both RGB and RGBA. Alpha channel is ignored.
+ return (abs(pixel1[0] - pixel2[0]) <= tolerance) &&
+ (abs(pixel1[1] - pixel2[1]) <= tolerance) &&
+ (abs(pixel1[2] - pixel2[2]) <= tolerance);
+}
+
+
+static inline
+bool PixelsEqual(const unsigned char* pixel, int color, int tolerance) {
+ unsigned char pixel2[3] = { color >> 16, color >> 8, color };
+ return PixelsEqual(pixel, pixel2, tolerance);
+}
+
+
+static
+bool Histogram(const Bitmap& bmp) {
+ int ignore_color;
+ int tolerance;
+ if (!(ReadInt(&ignore_color) && ReadInt(&tolerance))) {
+ fprintf(stderr, "Could not parse HISTOGRAM command.\n");
+ return false;
+ }
+
+ const int kLength = 3 * 256;
+ int counts[kLength] = {};
+
+ for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size;
+ row += bmp.row_stride) {
+ for (const unsigned char* pixel = row; pixel < row + bmp.row_size;
+ pixel += bmp.pixel_stride) {
+ if (ignore_color >= 0 && PixelsEqual(pixel, ignore_color, tolerance))
+ continue;
+ ++(counts[256 * 0 + pixel[0]]);
+ ++(counts[256 * 1 + pixel[1]]);
+ ++(counts[256 * 2 + pixel[2]]);
+ }
+ }
+
+ WriteResponse(counts, sizeof(counts));
+ return true;
+}
+
+
+static
+bool BoundingBox(const Bitmap& bmp) {
+ int color;
+ int tolerance;
+ if (!(ReadInt(&color) && ReadInt(&tolerance))) {
+ fprintf(stderr, "Could not parse BOUNDING_BOX command.\n");
+ return false;
+ }
+
+ Box box;
+ box.left = bmp.total_size;
+ box.top = bmp.total_size;
+ box.right = 0;
+ box.bottom = 0;
+
+ int count = 0;
+ int y = 0;
+ for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size;
+ row += bmp.row_stride, ++y) {
+ int x = 0;
+ for (const unsigned char* pixel = row; pixel < row + bmp.row_size;
+ pixel += bmp.pixel_stride, ++x) {
+ if (!PixelsEqual(pixel, color, tolerance))
+ continue;
+ box.Union(x, y);
+ ++count;
+ }
+ }
+
+ int response[] = { box.left, box.top, box.width(), box.height(), count };
+ WriteResponse(response, sizeof(response));
+ return true;
+}
+
+
+int main() {
+ Bitmap bmp;
+ int command;
+
+#if defined(WIN32)
+ _setmode(_fileno(stdin), _O_BINARY);
+ _setmode(_fileno(stdout), _O_BINARY);
+#else
+ FILE* unused_stdin = freopen(NULL, "rb", stdin);
+ FILE* unused_stdout = freopen(NULL, "wb", stdout);
+#endif
+
+ if (!bmp.Read()) return -1;
+ if (!ReadInt(&command)) {
+ fprintf(stderr, "Expected command.\n");
+ return -1;
+ }
+ switch (command) {
+ case CROP_PIXELS:
+ bmp.WriteCroppedPixels();
+ break;
+ case BOUNDING_BOX:
+ if (!BoundingBox(bmp)) return -1;
+ break;
+ case HISTOGRAM:
+ if (!Histogram(bmp)) return -1;
+ break;
+ default:
+ fprintf(stderr, "Unrecognized command\n");
+ return -1;
+ }
+ return 0;
+}