├── .gitignore ├── LICENSE ├── README.md ├── android_app ├── AndroidManifest.xml └── src │ └── com │ └── google │ └── android │ └── apps │ └── appspeedindex │ └── BoundingBox.java ├── bitmap.py ├── bitmaptools.cc ├── html_graph.py ├── json_combiner.py ├── png.py ├── run_all.py ├── speed_index.py └── video.py /.gitignore: -------------------------------------------------------------------------------- 1 | android_app/bin/ 2 | android_app/build.xml 3 | android_app/gen/ 4 | android_app/local.properties 5 | android_app/proguard-project.txt 6 | android_app/project.properties 7 | *.pyc 8 | bitmaptools 9 | speed_index.mp4 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Chromium Authors. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | """ 2 | This repository is based on chrome's speed index: 3 | https://code.google.com/p/chromium/codesearch#chromium/src/tools/perf/metrics/speedindex.py 4 | 5 | For more details, see: 6 | https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index 7 | 8 | However, instead of measuring web pages, it calculates the "visual completeness" 9 | for android apps. 10 | This can be specially useful for black-box testing of apps startup. 11 | It doesn't need any special instrumentation on the application under test. 12 | As such, it provides fairly coarse grained information, but that can still 13 | be useful as a first approach to measure startup performance. 14 | """ 15 | 16 | # 1) Build the "BoundingBox" helper app. 17 | sdk/tools/android update project --name BoundingBox -p . --target android-19 18 | ant clean debug 19 | ant debug install 20 | 21 | # 2) Build the "bitmap" python module: 22 | g++ bitmaptools.cc -o bitmaptools 23 | -------------------------------------------------------------------------------- /android_app/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android_app/src/com/google/android/apps/appspeedindex/BoundingBox.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package com.google.android.apps.appspeedindex; 6 | 7 | import android.app.Activity; 8 | import android.graphics.Canvas; 9 | import android.graphics.Paint; 10 | import android.os.Bundle; 11 | import android.os.Handler; 12 | import android.util.Log; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | 16 | import java.lang.Runnable; 17 | 18 | public class BoundingBox extends Activity { 19 | final Paint paint = new Paint(); 20 | final Handler handler = new Handler(); 21 | 22 | @Override 23 | public void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | paint.setStyle(Paint.Style.FILL); 26 | final View view = new View(this) { 27 | @Override 28 | public void onDraw(Canvas canvas) { 29 | canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 30 | } 31 | }; 32 | 33 | view.setLayoutParams(new ViewGroup.LayoutParams( 34 | ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); 35 | setContentView(view); 36 | 37 | paintDelayed(view, 0, 255, 0, 0); 38 | paintDelayed(view, 0, 0, 255, 640); 39 | 40 | } 41 | 42 | private void paintDelayed( 43 | final View view, final int r, final int g, final int b, long delay) { 44 | handler.postDelayed(new Runnable() { 45 | @Override 46 | public void run() { 47 | paint.setARGB(255, r, g, b); 48 | view.invalidate(); 49 | } 50 | }, delay); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bitmap.py: -------------------------------------------------------------------------------- 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 3 | # found in the LICENSE file. 4 | 5 | """ 6 | Bitmap is a basic wrapper for image pixels. It includes some basic processing 7 | tools: crop, find bounding box of a color and compute histogram of color values. 8 | """ 9 | 10 | import array 11 | import base64 12 | import cStringIO 13 | import collections 14 | import os 15 | import struct 16 | import subprocess 17 | 18 | 19 | def HistogramDistance(hist1, hist2): 20 | """Earth mover's distance. 21 | 22 | http://en.wikipedia.org/wiki/Earth_mover's_distance 23 | First, normalize the two histograms. Then, treat the two histograms as 24 | piles of dirt, and calculate the cost of turning one pile into the other. 25 | 26 | To do this, calculate the difference in one bucket between the two 27 | histograms. Then carry it over in the calculation for the next bucket. 28 | In this way, the difference is weighted by how far it has to move.""" 29 | if len(hist1) != len(hist2): 30 | raise ValueError('Trying to compare histograms ' 31 | 'of different sizes, %s != %s' % (len(hist1), len(hist2))) 32 | 33 | n1 = sum(hist1) 34 | n2 = sum(hist2) 35 | if n1 == 0: 36 | raise ValueError('First histogram has 0 pixels in it.') 37 | if n2 == 0: 38 | raise ValueError('Second histogram has 0 pixels in it.') 39 | 40 | total = 0 41 | remainder = 0 42 | for value1, value2 in zip(hist1, hist2): 43 | remainder += value1 * n2 - value2 * n1 44 | total += abs(remainder) 45 | assert remainder == 0, ( 46 | '%s pixel(s) left over after computing histogram distance.' 47 | % abs(remainder)) 48 | return abs(float(total) / n1 / n2) 49 | 50 | 51 | class ColorHistogram( 52 | collections.namedtuple('ColorHistogram', ['r', 'g', 'b', 'default_color'])): 53 | # pylint: disable=W0232 54 | # pylint: disable=E1002 55 | 56 | def __new__(cls, r, g, b, default_color=None): 57 | return super(ColorHistogram, cls).__new__(cls, r, g, b, default_color) 58 | 59 | def Distance(self, other): 60 | total = 0 61 | for i in xrange(3): 62 | hist1 = self[i] 63 | hist2 = other[i] 64 | 65 | if sum(self[i]) == 0: 66 | if not self.default_color: 67 | raise ValueError('Histogram has no data and no default color.') 68 | hist1 = [0] * 256 69 | hist1[self.default_color[i]] = 1 70 | if sum(other[i]) == 0: 71 | if not other.default_color: 72 | raise ValueError('Histogram has no data and no default color.') 73 | hist2 = [0] * 256 74 | hist2[other.default_color[i]] = 1 75 | 76 | total += HistogramDistance(hist1, hist2) 77 | return total 78 | 79 | 80 | class RgbaColor(collections.namedtuple('RgbaColor', ['r', 'g', 'b', 'a'])): 81 | """Encapsulates an RGBA color retreived from a Bitmap""" 82 | # pylint: disable=W0232 83 | # pylint: disable=E1002 84 | 85 | def __new__(cls, r, g, b, a=255): 86 | return super(RgbaColor, cls).__new__(cls, r, g, b, a) 87 | 88 | def __int__(self): 89 | return (self.r << 16) | (self.g << 8) | self.b 90 | 91 | def IsEqual(self, expected_color, tolerance=0): 92 | """Verifies that the color is within a given tolerance of 93 | the expected color""" 94 | r_diff = abs(self.r - expected_color.r) 95 | g_diff = abs(self.g - expected_color.g) 96 | b_diff = abs(self.b - expected_color.b) 97 | a_diff = abs(self.a - expected_color.a) 98 | return (r_diff <= tolerance and g_diff <= tolerance 99 | and b_diff <= tolerance and a_diff <= tolerance) 100 | 101 | def AssertIsRGB(self, r, g, b, tolerance=0): 102 | assert self.IsEqual(RgbaColor(r, g, b), tolerance) 103 | 104 | def AssertIsRGBA(self, r, g, b, a, tolerance=0): 105 | assert self.IsEqual(RgbaColor(r, g, b, a), tolerance) 106 | 107 | 108 | WEB_PAGE_TEST_ORANGE = RgbaColor(222, 100, 13) 109 | WHITE = RgbaColor(255, 255, 255) 110 | 111 | 112 | class _BitmapTools(object): 113 | """Wraps a child process of bitmaptools and allows for one command.""" 114 | CROP_PIXELS = 0 115 | HISTOGRAM = 1 116 | BOUNDING_BOX = 2 117 | 118 | def __init__(self, dimensions, pixels): 119 | binary = './bitmaptools' 120 | assert os.path.exists(binary), 'You must build bitmaptools first!' 121 | 122 | self._popen = subprocess.Popen([binary], 123 | stdin=subprocess.PIPE, 124 | stdout=subprocess.PIPE, 125 | stderr=subprocess.PIPE) 126 | 127 | # dimensions are: bpp, width, height, boxleft, boxtop, boxwidth, boxheight 128 | packed_dims = struct.pack('iiiiiii', *dimensions) 129 | self._popen.stdin.write(packed_dims) 130 | # If we got a list of ints, we need to convert it into a byte buffer. 131 | if type(pixels) is not bytearray: 132 | pixels = bytearray(pixels) 133 | self._popen.stdin.write(pixels) 134 | 135 | def _RunCommand(self, *command): 136 | assert not self._popen.stdin.closed, ( 137 | 'Exactly one command allowed per instance of tools.') 138 | packed_command = struct.pack('i' * len(command), *command) 139 | self._popen.stdin.write(packed_command) 140 | self._popen.stdin.close() 141 | length_packed = self._popen.stdout.read(struct.calcsize('i')) 142 | if not length_packed: 143 | raise Exception(self._popen.stderr.read()) 144 | length = struct.unpack('i', length_packed)[0] 145 | return self._popen.stdout.read(length) 146 | 147 | def CropPixels(self): 148 | return self._RunCommand(_BitmapTools.CROP_PIXELS) 149 | 150 | def Histogram(self, ignore_color, tolerance): 151 | ignore_color_int = -1 if ignore_color is None else int(ignore_color) 152 | response = self._RunCommand(_BitmapTools.HISTOGRAM, 153 | ignore_color_int, tolerance) 154 | out = array.array('i') 155 | out.fromstring(response) 156 | assert len(out) == 768, ( 157 | 'The ColorHistogram has the wrong number of buckets: %s' % len(out)) 158 | return ColorHistogram(out[:256], out[256:512], out[512:], ignore_color) 159 | 160 | def BoundingBox(self, color, tolerance): 161 | response = self._RunCommand(_BitmapTools.BOUNDING_BOX, int(color), 162 | tolerance) 163 | unpacked = struct.unpack('iiiii', response) 164 | box, count = unpacked[:4], unpacked[-1] 165 | if box[2] < 0 or box[3] < 0: 166 | box = None 167 | return box, count 168 | 169 | 170 | class Bitmap(object): 171 | """Utilities for parsing and inspecting a bitmap.""" 172 | 173 | def __init__(self, bpp, width, height, pixels, metadata=None): 174 | assert bpp in [3, 4], 'Invalid bytes per pixel' 175 | assert width > 0, 'Invalid width' 176 | assert height > 0, 'Invalid height' 177 | assert pixels, 'Must specify pixels' 178 | assert bpp * width * height == len(pixels), 'Dimensions and pixels mismatch' 179 | 180 | self._bpp = bpp 181 | self._width = width 182 | self._height = height 183 | self._pixels = pixels 184 | self._metadata = metadata or {} 185 | self._crop_box = None 186 | 187 | @property 188 | def bpp(self): 189 | """Bytes per pixel.""" 190 | return self._bpp 191 | 192 | @property 193 | def width(self): 194 | """Width of the bitmap.""" 195 | return self._crop_box[2] if self._crop_box else self._width 196 | 197 | @property 198 | def height(self): 199 | """Height of the bitmap.""" 200 | return self._crop_box[3] if self._crop_box else self._height 201 | 202 | def _PrepareTools(self): 203 | """Prepares an instance of _BitmapTools which allows exactly one command. 204 | """ 205 | crop_box = self._crop_box or (0, 0, self._width, self._height) 206 | return _BitmapTools((self._bpp, self._width, self._height) + crop_box, 207 | self._pixels) 208 | 209 | @property 210 | def pixels(self): 211 | """Flat pixel array of the bitmap.""" 212 | if self._crop_box: 213 | self._pixels = self._PrepareTools().CropPixels() 214 | _, _, self._width, self._height = self._crop_box 215 | self._crop_box = None 216 | if type(self._pixels) is not bytearray: 217 | self._pixels = bytearray(self._pixels) 218 | return self._pixels 219 | 220 | @property 221 | def metadata(self): 222 | self._metadata['size'] = (self.width, self.height) 223 | self._metadata['alpha'] = self.bpp == 4 224 | self._metadata['bitdepth'] = 8 225 | return self._metadata 226 | 227 | def GetPixelColor(self, x, y): 228 | """Returns a RgbaColor for the pixel at (x, y).""" 229 | pixels = self.pixels 230 | base = self._bpp * (y * self._width + x) 231 | if self._bpp == 4: 232 | return RgbaColor(pixels[base + 0], pixels[base + 1], 233 | pixels[base + 2], pixels[base + 3]) 234 | return RgbaColor(pixels[base + 0], pixels[base + 1], 235 | pixels[base + 2]) 236 | 237 | def IsEqual(self, other, tolerance=0): 238 | """Determines whether two Bitmaps are identical within a given tolerance.""" 239 | 240 | # Dimensions must be equal 241 | if self.width != other.width or self.height != other.height: 242 | return False 243 | 244 | # Loop over each pixel and test for equality 245 | if tolerance or self.bpp != other.bpp: 246 | for y in range(self.height): 247 | for x in range(self.width): 248 | c0 = self.GetPixelColor(x, y) 249 | c1 = other.GetPixelColor(x, y) 250 | if not c0.IsEqual(c1, tolerance): 251 | return False 252 | else: 253 | return self.pixels == other.pixels 254 | 255 | return True 256 | 257 | def Diff(self, other): 258 | """Returns a new Bitmap that represents the difference between this image 259 | and another Bitmap.""" 260 | 261 | # Output dimensions will be the maximum of the two input dimensions 262 | out_width = max(self.width, other.width) 263 | out_height = max(self.height, other.height) 264 | 265 | diff = [[0 for x in xrange(out_width * 3)] for x in xrange(out_height)] 266 | 267 | # Loop over each pixel and write out the difference 268 | for y in range(out_height): 269 | for x in range(out_width): 270 | if x < self.width and y < self.height: 271 | c0 = self.GetPixelColor(x, y) 272 | else: 273 | c0 = RgbaColor(0, 0, 0, 0) 274 | 275 | if x < other.width and y < other.height: 276 | c1 = other.GetPixelColor(x, y) 277 | else: 278 | c1 = RgbaColor(0, 0, 0, 0) 279 | 280 | offset = x * 3 281 | diff[y][offset] = abs(c0.r - c1.r) 282 | diff[y][offset+1] = abs(c0.g - c1.g) 283 | diff[y][offset+2] = abs(c0.b - c1.b) 284 | 285 | # This particular method can only save to a file, so the result will be 286 | # written into an in-memory buffer and read back into a Bitmap 287 | diff_img = png.from_array(diff, mode='RGB') 288 | output = cStringIO.StringIO() 289 | try: 290 | diff_img.save(output) 291 | diff = Bitmap.FromPng(output.getvalue()) 292 | finally: 293 | output.close() 294 | 295 | return diff 296 | 297 | def GetBoundingBox(self, color, tolerance=0): 298 | """Finds the minimum box surrounding all occurences of |color|. 299 | Returns: (top, left, width, height), match_count 300 | Ignores the alpha channel.""" 301 | return self._PrepareTools().BoundingBox(color, tolerance) 302 | 303 | def Crop(self, left, top, width, height): 304 | """Crops the current bitmap down to the specified box.""" 305 | cur_box = self._crop_box or (0, 0, self._width, self._height) 306 | cur_left, cur_top, cur_width, cur_height = cur_box 307 | 308 | if (left < 0 or top < 0 or 309 | (left + width) > cur_width or 310 | (top + height) > cur_height): 311 | raise ValueError('Invalid dimensions') 312 | 313 | self._crop_box = cur_left + left, cur_top + top, width, height 314 | return self 315 | 316 | def ColorHistogram(self, ignore_color=None, tolerance=0): 317 | """Computes a histogram of the pixel colors in this Bitmap. 318 | Args: 319 | ignore_color: An RgbaColor to exclude from the bucket counts. 320 | tolerance: A tolerance for the ignore_color. 321 | 322 | Returns: 323 | A ColorHistogram namedtuple with 256 integers in each field: r, g, and b. 324 | """ 325 | return self._PrepareTools().Histogram(ignore_color, tolerance) 326 | -------------------------------------------------------------------------------- /bitmaptools.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #if defined(WIN32) 10 | #include 11 | #include 12 | #endif 13 | 14 | enum Commands { 15 | CROP_PIXELS = 0, 16 | HISTOGRAM = 1, 17 | BOUNDING_BOX = 2 18 | }; 19 | 20 | bool ReadInt(int* out) { 21 | return fread(out, sizeof(*out), 1, stdin) == 1; 22 | } 23 | 24 | void WriteResponse(void* data, int size) { 25 | fwrite(&size, sizeof(size), 1, stdout); 26 | fwrite(data, size, 1, stdout); 27 | fflush(stdout); 28 | } 29 | 30 | struct Box { 31 | Box() : left(), top(), right(), bottom() {} 32 | 33 | // Expected input is: 34 | // left, top, width, height 35 | bool Read() { 36 | int width; 37 | int height; 38 | if (!(ReadInt(&left) && ReadInt(&top) && 39 | ReadInt(&width) && ReadInt(&height))) { 40 | fprintf(stderr, "Could not parse Box.\n"); 41 | return false; 42 | } 43 | if (left < 0 || top < 0 || width < 0 || height < 0) { 44 | fprintf(stderr, "Box dimensions must be non-negative.\n"); 45 | return false; 46 | } 47 | right = left + width; 48 | bottom = top + height; 49 | return true; 50 | } 51 | 52 | void Union(int x, int y) { 53 | if (left > x) left = x; 54 | if (right <= x) right = x + 1; 55 | if (top > y) top = y; 56 | if (bottom <= y) bottom = y + 1; 57 | } 58 | 59 | int width() const { return right - left; } 60 | int height() const { return bottom - top; } 61 | 62 | int left; 63 | int top; 64 | int right; 65 | int bottom; 66 | }; 67 | 68 | 69 | // Represents a bitmap buffer with a crop box. 70 | struct Bitmap { 71 | Bitmap() : pixels(NULL) {} 72 | 73 | ~Bitmap() { 74 | if (pixels) 75 | delete[] pixels; 76 | } 77 | 78 | // Expected input is: 79 | // bpp, width, height, box, pixels 80 | bool Read() { 81 | int bpp; 82 | int width; 83 | int height; 84 | if (!(ReadInt(&bpp) && ReadInt(&width) && ReadInt(&height))) { 85 | fprintf(stderr, "Could not parse Bitmap initializer.\n"); 86 | return false; 87 | } 88 | if (bpp <= 0 || width <= 0 || height <= 0) { 89 | fprintf(stderr, "Dimensions must be positive.\n"); 90 | return false; 91 | } 92 | 93 | int size = width * height * bpp; 94 | 95 | row_stride = width * bpp; 96 | pixel_stride = bpp; 97 | total_size = size; 98 | row_size = row_stride; 99 | 100 | if (!box.Read()) { 101 | fprintf(stderr, "Expected crop box argument not found.\n"); 102 | return false; 103 | } 104 | 105 | if (box.bottom * row_stride > total_size || 106 | box.right * pixel_stride > row_size) { 107 | fprintf(stderr, "Crop box overflows the bitmap.\n"); 108 | return false; 109 | } 110 | 111 | pixels = new unsigned char[size]; 112 | if (fread(pixels, sizeof(pixels[0]), size, stdin) < 113 | static_cast(size)) { 114 | fprintf(stderr, "Not enough pixels found,\n"); 115 | return false; 116 | } 117 | 118 | total_size = (box.bottom - box.top) * row_stride; 119 | row_size = (box.right - box.left) * pixel_stride; 120 | data = pixels + box.top * row_stride + box.left * pixel_stride; 121 | return true; 122 | } 123 | 124 | void WriteCroppedPixels() const { 125 | int out_size = row_size * box.height(); 126 | unsigned char* out = new unsigned char[out_size]; 127 | unsigned char* dst = out; 128 | for (const unsigned char* row = data; 129 | row < data + total_size; 130 | row += row_stride, dst += row_size) { 131 | // No change in pixel_stride, so we can copy whole rows. 132 | memcpy(dst, row, row_size); 133 | } 134 | 135 | WriteResponse(out, out_size); 136 | delete[] out; 137 | } 138 | 139 | unsigned char* pixels; 140 | Box box; 141 | // Points at the top-left pixel in |pixels|. 142 | const unsigned char* data; 143 | // These counts are in bytes. 144 | int row_stride; 145 | int pixel_stride; 146 | int total_size; 147 | int row_size; 148 | }; 149 | 150 | 151 | static inline 152 | bool PixelsEqual(const unsigned char* pixel1, const unsigned char* pixel2, 153 | int tolerance) { 154 | // Note: this works for both RGB and RGBA. Alpha channel is ignored. 155 | return (abs(pixel1[0] - pixel2[0]) <= tolerance) && 156 | (abs(pixel1[1] - pixel2[1]) <= tolerance) && 157 | (abs(pixel1[2] - pixel2[2]) <= tolerance); 158 | } 159 | 160 | 161 | static inline 162 | bool PixelsEqual(const unsigned char* pixel, int color, int tolerance) { 163 | unsigned char pixel2[3] = { color >> 16, color >> 8, color }; 164 | return PixelsEqual(pixel, pixel2, tolerance); 165 | } 166 | 167 | 168 | static 169 | bool Histogram(const Bitmap& bmp) { 170 | int ignore_color; 171 | int tolerance; 172 | if (!(ReadInt(&ignore_color) && ReadInt(&tolerance))) { 173 | fprintf(stderr, "Could not parse HISTOGRAM command.\n"); 174 | return false; 175 | } 176 | 177 | const int kLength = 3 * 256; 178 | int counts[kLength] = {}; 179 | 180 | for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; 181 | row += bmp.row_stride) { 182 | for (const unsigned char* pixel = row; pixel < row + bmp.row_size; 183 | pixel += bmp.pixel_stride) { 184 | if (ignore_color >= 0 && PixelsEqual(pixel, ignore_color, tolerance)) 185 | continue; 186 | ++(counts[256 * 0 + pixel[0]]); 187 | ++(counts[256 * 1 + pixel[1]]); 188 | ++(counts[256 * 2 + pixel[2]]); 189 | } 190 | } 191 | 192 | WriteResponse(counts, sizeof(counts)); 193 | return true; 194 | } 195 | 196 | 197 | static 198 | bool BoundingBox(const Bitmap& bmp) { 199 | int color; 200 | int tolerance; 201 | if (!(ReadInt(&color) && ReadInt(&tolerance))) { 202 | fprintf(stderr, "Could not parse BOUNDING_BOX command.\n"); 203 | return false; 204 | } 205 | 206 | Box box; 207 | box.left = bmp.total_size; 208 | box.top = bmp.total_size; 209 | box.right = 0; 210 | box.bottom = 0; 211 | 212 | int count = 0; 213 | int y = 0; 214 | for (const unsigned char* row = bmp.data; row < bmp.data + bmp.total_size; 215 | row += bmp.row_stride, ++y) { 216 | int x = 0; 217 | for (const unsigned char* pixel = row; pixel < row + bmp.row_size; 218 | pixel += bmp.pixel_stride, ++x) { 219 | if (!PixelsEqual(pixel, color, tolerance)) 220 | continue; 221 | box.Union(x, y); 222 | ++count; 223 | } 224 | } 225 | 226 | int response[] = { box.left, box.top, box.width(), box.height(), count }; 227 | WriteResponse(response, sizeof(response)); 228 | return true; 229 | } 230 | 231 | 232 | int main() { 233 | Bitmap bmp; 234 | int command; 235 | 236 | #if defined(WIN32) 237 | _setmode(_fileno(stdin), _O_BINARY); 238 | _setmode(_fileno(stdout), _O_BINARY); 239 | #else 240 | FILE* unused_stdin = freopen(NULL, "rb", stdin); 241 | FILE* unused_stdout = freopen(NULL, "wb", stdout); 242 | #endif 243 | 244 | if (!bmp.Read()) return -1; 245 | if (!ReadInt(&command)) { 246 | fprintf(stderr, "Expected command.\n"); 247 | return -1; 248 | } 249 | switch (command) { 250 | case CROP_PIXELS: 251 | bmp.WriteCroppedPixels(); 252 | break; 253 | case BOUNDING_BOX: 254 | if (!BoundingBox(bmp)) return -1; 255 | break; 256 | case HISTOGRAM: 257 | if (!Histogram(bmp)) return -1; 258 | break; 259 | default: 260 | fprintf(stderr, "Unrecognized command\n"); 261 | return -1; 262 | } 263 | return 0; 264 | } 265 | -------------------------------------------------------------------------------- /html_graph.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 The Chromium Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | import json 6 | from string import Template 7 | 8 | 9 | class HTMLGraph(object): 10 | _HTML_TEMPLATE = """ 11 | 12 | 30 | Speed Index for $COMMAND 31 |
32 |
33 | """ 34 | 35 | def GenerateGraph(self, output, command, headers, all_data): 36 | html_report = Template(self._HTML_TEMPLATE).safe_substitute( 37 | { 38 | 'COMMAND': command, 39 | 'ALL_DATA': json.dumps([headers] + all_data) 40 | }) 41 | with file(output + '.html', 'w') as w: 42 | w.write(html_report) 43 | with file(output + '.json', 'w') as w: 44 | w.write(json.dumps(all_data)) 45 | -------------------------------------------------------------------------------- /json_combiner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2014 The Chromium Authors. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | import logging 8 | import optparse 9 | import bisect 10 | import os 11 | import json 12 | import sys 13 | 14 | import html_graph 15 | 16 | class JSONCombiner(object): 17 | def __init__(self, output, json_list): 18 | self._data = {} 19 | self._output = output 20 | self._json_list = json_list 21 | 22 | def _GetClosest(self, key, timestamp): 23 | if timestamp in self._data[key]: 24 | return self._data[key][timestamp] 25 | sorted_ts = sorted(self._data[key].keys()) 26 | closest = bisect.bisect_right(sorted_ts, timestamp) 27 | if closest: 28 | return self._data[key][sorted_ts[closest - 1]] 29 | return self.data[key][sorted_ts[-1]] 30 | 31 | def Generate(self): 32 | all_timestamps = set() 33 | for json_name in self._json_list: 34 | with file(json_name) as f: 35 | data = json.loads(f.read()) 36 | for d in data: 37 | all_timestamps.add(d[0]) 38 | if json_name not in self._data: 39 | self._data[json_name] = {} 40 | self._data[json_name][d[0]] = d[1] 41 | 42 | all_timestamps = sorted(all_timestamps) 43 | headers = ['Time'] 44 | for k in sorted(self._data.keys()): 45 | headers += ['%s (%%Visual Complete)' % 46 | os.path.basename(os.path.splitext(k)[0])] 47 | 48 | time_series = [] 49 | for ts in all_timestamps: 50 | time_series += [[ts]] 51 | for k in sorted(self._data.keys()): 52 | time_series[-1] += [self._GetClosest(k, ts)] 53 | 54 | g = html_graph.HTMLGraph() 55 | g.GenerateGraph(self._output, 'Combined', headers, time_series) 56 | 57 | 58 | def main(): 59 | parser = optparse.OptionParser(description=__doc__, 60 | usage='json_combiner') 61 | parser.add_option('-o', '--output', help='Output report', 62 | default='speed_index.html') 63 | (options, args) = parser.parse_args() 64 | combiner = JSONCombiner(options.output, args) 65 | combiner.Generate() 66 | 67 | 68 | if __name__ == '__main__': 69 | sys.exit(main()) 70 | -------------------------------------------------------------------------------- /png.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # $URL$ 4 | # $Rev$ 5 | 6 | # png.py - PNG encoder/decoder in pure Python 7 | # 8 | # Copyright (C) 2006 Johann C. Rocholl 9 | # Portions Copyright (C) 2009 David Jones 10 | # And probably portions Copyright (C) 2006 Nicko van Someren 11 | # 12 | # Original concept by Johann C. Rocholl. 13 | # 14 | # LICENSE (The MIT License) 15 | # 16 | # Permission is hereby granted, free of charge, to any person 17 | # obtaining a copy of this software and associated documentation files 18 | # (the "Software"), to deal in the Software without restriction, 19 | # including without limitation the rights to use, copy, modify, merge, 20 | # publish, distribute, sublicense, and/or sell copies of the Software, 21 | # and to permit persons to whom the Software is furnished to do so, 22 | # subject to the following conditions: 23 | # 24 | # The above copyright notice and this permission notice shall be 25 | # included in all copies or substantial portions of the Software. 26 | # 27 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 28 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 31 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 32 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 33 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | # 36 | # Changelog (recent first): 37 | # 2009-03-11 David: interlaced bit depth < 8 (writing). 38 | # 2009-03-10 David: interlaced bit depth < 8 (reading). 39 | # 2009-03-04 David: Flat and Boxed pixel formats. 40 | # 2009-02-26 David: Palette support (writing). 41 | # 2009-02-23 David: Bit-depths < 8; better PNM support. 42 | # 2006-06-17 Nicko: Reworked into a class, faster interlacing. 43 | # 2006-06-17 Johann: Very simple prototype PNG decoder. 44 | # 2006-06-17 Nicko: Test suite with various image generators. 45 | # 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support. 46 | # 2006-06-15 Johann: Scanline iterator interface for large input files. 47 | # 2006-06-09 Johann: Very simple prototype PNG encoder. 48 | 49 | # Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from 50 | # http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885 51 | 52 | # Incorporated into pypng by drj on 2009-03-12 from 53 | # //depot/prj/bangaio/master/code/png.py#67 54 | 55 | 56 | """ 57 | Pure Python PNG Reader/Writer 58 | 59 | This Python module implements support for PNG images (see PNG 60 | specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads 61 | and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64 62 | bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB, 63 | RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped 64 | images (1/2/4/8 bit). Adam7 interlacing is supported for reading and 65 | writing. A number of optional chunks can be specified (when writing) 66 | and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. 67 | 68 | For help, type ``import png; help(png)`` in your python interpreter. 69 | 70 | A good place to start is the :class:`Reader` and :class:`Writer` classes. 71 | 72 | Requires Python 2.3. Limited support is available for Python 2.2, but 73 | not everything works. Best with Python 2.4 and higher. Installation is 74 | trivial, but see the ``README.txt`` file (with the source distribution) 75 | for details. 76 | 77 | This file can also be used as a command-line utility to convert 78 | `Netpbm `_ PNM files to PNG, and the reverse conversion from PNG to 79 | PNM. The interface is similar to that of the ``pnmtopng`` program from 80 | Netpbm. Type ``python png.py --help`` at the shell prompt 81 | for usage and a list of options. 82 | 83 | A note on spelling and terminology 84 | ---------------------------------- 85 | 86 | Generally British English spelling is used in the documentation. So 87 | that's "greyscale" and "colour". This not only matches the author's 88 | native language, it's also used by the PNG specification. 89 | 90 | The major colour models supported by PNG (and hence by PyPNG) are: 91 | greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes 92 | referred to using the abbreviations: L, RGB, LA, RGBA. In this case 93 | each letter abbreviates a single channel: *L* is for Luminance or Luma or 94 | Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand 95 | for Red, Green, Blue, the components of a colour image; *A* stands for 96 | Alpha, the opacity channel (used for transparency effects, but higher 97 | values are more opaque, so it makes sense to call it opacity). 98 | 99 | A note on formats 100 | ----------------- 101 | 102 | When getting pixel data out of this module (reading) and presenting 103 | data to this module (writing) there are a number of ways the data could 104 | be represented as a Python value. Generally this module uses one of 105 | three formats called "flat row flat pixel", "boxed row flat pixel", and 106 | "boxed row boxed pixel". Basically the concern is whether each pixel 107 | and each row comes in its own little tuple (box), or not. 108 | 109 | Consider an image that is 3 pixels wide by 2 pixels high, and each pixel 110 | has RGB components: 111 | 112 | Boxed row flat pixel:: 113 | 114 | list([R,G,B, R,G,B, R,G,B], 115 | [R,G,B, R,G,B, R,G,B]) 116 | 117 | Each row appears as its own list, but the pixels are flattened so that 118 | three values for one pixel simply follow the three values for the previous 119 | pixel. This is the most common format used, because it provides a good 120 | compromise between space and convenience. PyPNG regards itself as 121 | at liberty to replace any sequence type with any sufficiently compatible 122 | other sequence type; in practice each row is an array (from the array 123 | module), and the outer list is sometimes an iterator rather than an 124 | explicit list (so that streaming is possible). 125 | 126 | Flat row flat pixel:: 127 | 128 | [R,G,B, R,G,B, R,G,B, 129 | R,G,B, R,G,B, R,G,B] 130 | 131 | The entire image is one single giant sequence of colour values. 132 | Generally an array will be used (to save space), not a list. 133 | 134 | Boxed row boxed pixel:: 135 | 136 | list([ (R,G,B), (R,G,B), (R,G,B) ], 137 | [ (R,G,B), (R,G,B), (R,G,B) ]) 138 | 139 | Each row appears in its own list, but each pixel also appears in its own 140 | tuple. A serious memory burn in Python. 141 | 142 | In all cases the top row comes first, and for each row the pixels are 143 | ordered from left-to-right. Within a pixel the values appear in the 144 | order, R-G-B-A (or L-A for greyscale--alpha). 145 | 146 | There is a fourth format, mentioned because it is used internally, 147 | is close to what lies inside a PNG file itself, and has some support 148 | from the public API. This format is called packed. When packed, 149 | each row is a sequence of bytes (integers from 0 to 255), just as 150 | it is before PNG scanline filtering is applied. When the bit depth 151 | is 8 this is essentially the same as boxed row flat pixel; when the 152 | bit depth is less than 8, several pixels are packed into each byte; 153 | when the bit depth is 16 (the only value more than 8 that is supported 154 | by the PNG image format) each pixel value is decomposed into 2 bytes 155 | (and `packed` is a misnomer). This format is used by the 156 | :meth:`Writer.write_packed` method. It isn't usually a convenient 157 | format, but may be just right if the source data for the PNG image 158 | comes from something that uses a similar format (for example, 1-bit 159 | BMPs, or another PNG file). 160 | 161 | And now, my famous members 162 | -------------------------- 163 | """ 164 | 165 | # http://www.python.org/doc/2.2.3/whatsnew/node5.html 166 | from __future__ import generators 167 | 168 | __version__ = "$URL$ $Rev$" 169 | 170 | from array import array 171 | try: # See :pyver:old 172 | import itertools 173 | except: 174 | pass 175 | import math 176 | # http://www.python.org/doc/2.4.4/lib/module-operator.html 177 | import operator 178 | import struct 179 | import sys 180 | import zlib 181 | # http://www.python.org/doc/2.4.4/lib/module-warnings.html 182 | import warnings 183 | try: 184 | import pyximport 185 | pyximport.install() 186 | import cpngfilters as pngfilters 187 | except ImportError: 188 | pass 189 | 190 | 191 | __all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] 192 | 193 | 194 | # The PNG signature. 195 | # http://www.w3.org/TR/PNG/#5PNG-file-signature 196 | _signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10) 197 | 198 | _adam7 = ((0, 0, 8, 8), 199 | (4, 0, 8, 8), 200 | (0, 4, 4, 8), 201 | (2, 0, 4, 4), 202 | (0, 2, 2, 4), 203 | (1, 0, 2, 2), 204 | (0, 1, 1, 2)) 205 | 206 | def group(s, n): 207 | # See 208 | # http://www.python.org/doc/2.6/library/functions.html#zip 209 | return zip(*[iter(s)]*n) 210 | 211 | def isarray(x): 212 | """Same as ``isinstance(x, array)`` except on Python 2.2, where it 213 | always returns ``False``. This helps PyPNG work on Python 2.2. 214 | """ 215 | 216 | try: 217 | return isinstance(x, array) 218 | except: 219 | return False 220 | 221 | try: # see :pyver:old 222 | array.tostring 223 | except: 224 | def tostring(row): 225 | l = len(row) 226 | return struct.pack('%dB' % l, *row) 227 | else: 228 | def tostring(row): 229 | """Convert row of bytes to string. Expects `row` to be an 230 | ``array``. 231 | """ 232 | return row.tostring() 233 | 234 | # Conditionally convert to bytes. Works on Python 2 and Python 3. 235 | try: 236 | bytes('', 'ascii') 237 | def strtobytes(x): return bytes(x, 'iso8859-1') 238 | def bytestostr(x): return str(x, 'iso8859-1') 239 | except: 240 | strtobytes = str 241 | bytestostr = str 242 | 243 | def interleave_planes(ipixels, apixels, ipsize, apsize): 244 | """ 245 | Interleave (colour) planes, e.g. RGB + A = RGBA. 246 | 247 | Return an array of pixels consisting of the `ipsize` elements of data 248 | from each pixel in `ipixels` followed by the `apsize` elements of data 249 | from each pixel in `apixels`. Conventionally `ipixels` and 250 | `apixels` are byte arrays so the sizes are bytes, but it actually 251 | works with any arrays of the same type. The returned array is the 252 | same type as the input arrays which should be the same type as each other. 253 | """ 254 | 255 | itotal = len(ipixels) 256 | atotal = len(apixels) 257 | newtotal = itotal + atotal 258 | newpsize = ipsize + apsize 259 | # Set up the output buffer 260 | # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 261 | out = array(ipixels.typecode) 262 | # It's annoying that there is no cheap way to set the array size :-( 263 | out.extend(ipixels) 264 | out.extend(apixels) 265 | # Interleave in the pixel data 266 | for i in range(ipsize): 267 | out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] 268 | for i in range(apsize): 269 | out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] 270 | return out 271 | 272 | def check_palette(palette): 273 | """Check a palette argument (to the :class:`Writer` class) for validity. 274 | Returns the palette as a list if okay; raises an exception otherwise. 275 | """ 276 | 277 | # None is the default and is allowed. 278 | if palette is None: 279 | return None 280 | 281 | p = list(palette) 282 | if not (0 < len(p) <= 256): 283 | raise ValueError("a palette must have between 1 and 256 entries") 284 | seen_triple = False 285 | for i,t in enumerate(p): 286 | if len(t) not in (3,4): 287 | raise ValueError( 288 | "palette entry %d: entries must be 3- or 4-tuples." % i) 289 | if len(t) == 3: 290 | seen_triple = True 291 | if seen_triple and len(t) == 4: 292 | raise ValueError( 293 | "palette entry %d: all 4-tuples must precede all 3-tuples" % i) 294 | for x in t: 295 | if int(x) != x or not(0 <= x <= 255): 296 | raise ValueError( 297 | "palette entry %d: values must be integer: 0 <= x <= 255" % i) 298 | return p 299 | 300 | class Error(Exception): 301 | prefix = 'Error' 302 | def __str__(self): 303 | return self.prefix + ': ' + ' '.join(self.args) 304 | 305 | class FormatError(Error): 306 | """Problem with input file format. In other words, PNG file does 307 | not conform to the specification in some way and is invalid. 308 | """ 309 | 310 | prefix = 'FormatError' 311 | 312 | class ChunkError(FormatError): 313 | prefix = 'ChunkError' 314 | 315 | 316 | class Writer: 317 | """ 318 | PNG encoder in pure Python. 319 | """ 320 | 321 | def __init__(self, width=None, height=None, 322 | size=None, 323 | greyscale=False, 324 | alpha=False, 325 | bitdepth=8, 326 | palette=None, 327 | transparent=None, 328 | background=None, 329 | gamma=None, 330 | compression=None, 331 | interlace=False, 332 | bytes_per_sample=None, # deprecated 333 | planes=None, 334 | colormap=None, 335 | maxval=None, 336 | chunk_limit=2**20): 337 | """ 338 | Create a PNG encoder object. 339 | 340 | Arguments: 341 | 342 | width, height 343 | Image size in pixels, as two separate arguments. 344 | size 345 | Image size (w,h) in pixels, as single argument. 346 | greyscale 347 | Input data is greyscale, not RGB. 348 | alpha 349 | Input data has alpha channel (RGBA or LA). 350 | bitdepth 351 | Bit depth: from 1 to 16. 352 | palette 353 | Create a palette for a colour mapped image (colour type 3). 354 | transparent 355 | Specify a transparent colour (create a ``tRNS`` chunk). 356 | background 357 | Specify a default background colour (create a ``bKGD`` chunk). 358 | gamma 359 | Specify a gamma value (create a ``gAMA`` chunk). 360 | compression 361 | zlib compression level: 0 (none) to 9 (more compressed); default: -1 or None. 362 | interlace 363 | Create an interlaced image. 364 | chunk_limit 365 | Write multiple ``IDAT`` chunks to save memory. 366 | 367 | The image size (in pixels) can be specified either by using the 368 | `width` and `height` arguments, or with the single `size` 369 | argument. If `size` is used it should be a pair (*width*, 370 | *height*). 371 | 372 | `greyscale` and `alpha` are booleans that specify whether 373 | an image is greyscale (or colour), and whether it has an 374 | alpha channel (or not). 375 | 376 | `bitdepth` specifies the bit depth of the source pixel values. 377 | Each source pixel value must be an integer between 0 and 378 | ``2**bitdepth-1``. For example, 8-bit images have values 379 | between 0 and 255. PNG only stores images with bit depths of 380 | 1,2,4,8, or 16. When `bitdepth` is not one of these values, 381 | the next highest valid bit depth is selected, and an ``sBIT`` 382 | (significant bits) chunk is generated that specifies the original 383 | precision of the source image. In this case the supplied pixel 384 | values will be rescaled to fit the range of the selected bit depth. 385 | 386 | The details of which bit depth / colour model combinations the 387 | PNG file format supports directly, are somewhat arcane 388 | (refer to the PNG specification for full details). Briefly: 389 | "small" bit depths (1,2,4) are only allowed with greyscale and 390 | colour mapped images; colour mapped images cannot have bit depth 391 | 16. 392 | 393 | For colour mapped images (in other words, when the `palette` 394 | argument is specified) the `bitdepth` argument must match one of 395 | the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a 396 | PNG image with a palette and an ``sBIT`` chunk, but the meaning 397 | is slightly different; it would be awkward to press the 398 | `bitdepth` argument into service for this.) 399 | 400 | The `palette` option, when specified, causes a colour mapped image 401 | to be created: the PNG colour type is set to 3; greyscale 402 | must not be set; alpha must not be set; transparent must 403 | not be set; the bit depth must be 1,2,4, or 8. When a colour 404 | mapped image is created, the pixel values are palette indexes 405 | and the `bitdepth` argument specifies the size of these indexes 406 | (not the size of the colour values in the palette). 407 | 408 | The palette argument value should be a sequence of 3- or 409 | 4-tuples. 3-tuples specify RGB palette entries; 4-tuples 410 | specify RGBA palette entries. If both 4-tuples and 3-tuples 411 | appear in the sequence then all the 4-tuples must come 412 | before all the 3-tuples. A ``PLTE`` chunk is created; if there 413 | are 4-tuples then a ``tRNS`` chunk is created as well. The 414 | ``PLTE`` chunk will contain all the RGB triples in the same 415 | sequence; the ``tRNS`` chunk will contain the alpha channel for 416 | all the 4-tuples, in the same sequence. Palette entries 417 | are always 8-bit. 418 | 419 | If specified, the `transparent` and `background` parameters must 420 | be a tuple with three integer values for red, green, blue, or 421 | a simple integer (or singleton tuple) for a greyscale image. 422 | 423 | If specified, the `gamma` parameter must be a positive number 424 | (generally, a float). A ``gAMA`` chunk will be created. Note that 425 | this will not change the values of the pixels as they appear in 426 | the PNG file, they are assumed to have already been converted 427 | appropriately for the gamma specified. 428 | 429 | The `compression` argument specifies the compression level to 430 | be used by the ``zlib`` module. Values from 1 to 9 specify 431 | compression, with 9 being "more compressed" (usually smaller 432 | and slower, but it doesn't always work out that way). 0 means 433 | no compression. -1 and ``None`` both mean that the default 434 | level of compession will be picked by the ``zlib`` module 435 | (which is generally acceptable). 436 | 437 | If `interlace` is true then an interlaced image is created 438 | (using PNG's so far only interace method, *Adam7*). This does not 439 | affect how the pixels should be presented to the encoder, rather 440 | it changes how they are arranged into the PNG file. On slow 441 | connexions interlaced images can be partially decoded by the 442 | browser to give a rough view of the image that is successively 443 | refined as more image data appears. 444 | 445 | .. note :: 446 | 447 | Enabling the `interlace` option requires the entire image 448 | to be processed in working memory. 449 | 450 | `chunk_limit` is used to limit the amount of memory used whilst 451 | compressing the image. In order to avoid using large amounts of 452 | memory, multiple ``IDAT`` chunks may be created. 453 | """ 454 | 455 | # At the moment the `planes` argument is ignored; 456 | # its purpose is to act as a dummy so that 457 | # ``Writer(x, y, **info)`` works, where `info` is a dictionary 458 | # returned by Reader.read and friends. 459 | # Ditto for `colormap`. 460 | 461 | # A couple of helper functions come first. Best skipped if you 462 | # are reading through. 463 | 464 | def isinteger(x): 465 | try: 466 | return int(x) == x 467 | except: 468 | return False 469 | 470 | def check_color(c, which): 471 | """Checks that a colour argument for transparent or 472 | background options is the right form. Also "corrects" bare 473 | integers to 1-tuples. 474 | """ 475 | 476 | if c is None: 477 | return c 478 | if greyscale: 479 | try: 480 | l = len(c) 481 | except TypeError: 482 | c = (c,) 483 | if len(c) != 1: 484 | raise ValueError("%s for greyscale must be 1-tuple" % 485 | which) 486 | if not isinteger(c[0]): 487 | raise ValueError( 488 | "%s colour for greyscale must be integer" % 489 | which) 490 | else: 491 | if not (len(c) == 3 and 492 | isinteger(c[0]) and 493 | isinteger(c[1]) and 494 | isinteger(c[2])): 495 | raise ValueError( 496 | "%s colour must be a triple of integers" % 497 | which) 498 | return c 499 | 500 | if size: 501 | if len(size) != 2: 502 | raise ValueError( 503 | "size argument should be a pair (width, height)") 504 | if width is not None and width != size[0]: 505 | raise ValueError( 506 | "size[0] (%r) and width (%r) should match when both are used." 507 | % (size[0], width)) 508 | if height is not None and height != size[1]: 509 | raise ValueError( 510 | "size[1] (%r) and height (%r) should match when both are used." 511 | % (size[1], height)) 512 | width,height = size 513 | del size 514 | 515 | if width <= 0 or height <= 0: 516 | raise ValueError("width and height must be greater than zero") 517 | if not isinteger(width) or not isinteger(height): 518 | raise ValueError("width and height must be integers") 519 | # http://www.w3.org/TR/PNG/#7Integers-and-byte-order 520 | if width > 2**32-1 or height > 2**32-1: 521 | raise ValueError("width and height cannot exceed 2**32-1") 522 | 523 | if alpha and transparent is not None: 524 | raise ValueError( 525 | "transparent colour not allowed with alpha channel") 526 | 527 | if bytes_per_sample is not None: 528 | warnings.warn('please use bitdepth instead of bytes_per_sample', 529 | DeprecationWarning) 530 | if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): 531 | raise ValueError( 532 | "bytes per sample must be .125, .25, .5, 1, or 2") 533 | bitdepth = int(8*bytes_per_sample) 534 | del bytes_per_sample 535 | if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: 536 | raise ValueError("bitdepth (%r) must be a postive integer <= 16" % 537 | bitdepth) 538 | 539 | self.rescale = None 540 | if palette: 541 | if bitdepth not in (1,2,4,8): 542 | raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") 543 | if transparent is not None: 544 | raise ValueError("transparent and palette not compatible") 545 | if alpha: 546 | raise ValueError("alpha and palette not compatible") 547 | if greyscale: 548 | raise ValueError("greyscale and palette not compatible") 549 | else: 550 | # No palette, check for sBIT chunk generation. 551 | if alpha or not greyscale: 552 | if bitdepth not in (8,16): 553 | targetbitdepth = (8,16)[bitdepth > 8] 554 | self.rescale = (bitdepth, targetbitdepth) 555 | bitdepth = targetbitdepth 556 | del targetbitdepth 557 | else: 558 | assert greyscale 559 | assert not alpha 560 | if bitdepth not in (1,2,4,8,16): 561 | if bitdepth > 8: 562 | targetbitdepth = 16 563 | elif bitdepth == 3: 564 | targetbitdepth = 4 565 | else: 566 | assert bitdepth in (5,6,7) 567 | targetbitdepth = 8 568 | self.rescale = (bitdepth, targetbitdepth) 569 | bitdepth = targetbitdepth 570 | del targetbitdepth 571 | 572 | if bitdepth < 8 and (alpha or not greyscale and not palette): 573 | raise ValueError( 574 | "bitdepth < 8 only permitted with greyscale or palette") 575 | if bitdepth > 8 and palette: 576 | raise ValueError( 577 | "bit depth must be 8 or less for images with palette") 578 | 579 | transparent = check_color(transparent, 'transparent') 580 | background = check_color(background, 'background') 581 | 582 | # It's important that the true boolean values (greyscale, alpha, 583 | # colormap, interlace) are converted to bool because Iverson's 584 | # convention is relied upon later on. 585 | self.width = width 586 | self.height = height 587 | self.transparent = transparent 588 | self.background = background 589 | self.gamma = gamma 590 | self.greyscale = bool(greyscale) 591 | self.alpha = bool(alpha) 592 | self.colormap = bool(palette) 593 | self.bitdepth = int(bitdepth) 594 | self.compression = compression 595 | self.chunk_limit = chunk_limit 596 | self.interlace = bool(interlace) 597 | self.palette = check_palette(palette) 598 | 599 | self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap 600 | assert self.color_type in (0,2,3,4,6) 601 | 602 | self.color_planes = (3,1)[self.greyscale or self.colormap] 603 | self.planes = self.color_planes + self.alpha 604 | # :todo: fix for bitdepth < 8 605 | self.psize = (self.bitdepth/8) * self.planes 606 | 607 | def make_palette(self): 608 | """Create the byte sequences for a ``PLTE`` and if necessary a 609 | ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be 610 | ``None`` if no ``tRNS`` chunk is necessary. 611 | """ 612 | 613 | p = array('B') 614 | t = array('B') 615 | 616 | for x in self.palette: 617 | p.extend(x[0:3]) 618 | if len(x) > 3: 619 | t.append(x[3]) 620 | p = tostring(p) 621 | t = tostring(t) 622 | if t: 623 | return p,t 624 | return p,None 625 | 626 | def write(self, outfile, rows): 627 | """Write a PNG image to the output file. `rows` should be 628 | an iterable that yields each row in boxed row flat pixel format. 629 | The rows should be the rows of the original image, so there 630 | should be ``self.height`` rows of ``self.width * self.planes`` values. 631 | If `interlace` is specified (when creating the instance), then 632 | an interlaced PNG file will be written. Supply the rows in the 633 | normal image order; the interlacing is carried out internally. 634 | 635 | .. note :: 636 | 637 | Interlacing will require the entire image to be in working memory. 638 | """ 639 | 640 | if self.interlace: 641 | fmt = 'BH'[self.bitdepth > 8] 642 | a = array(fmt, itertools.chain(*rows)) 643 | return self.write_array(outfile, a) 644 | else: 645 | nrows = self.write_passes(outfile, rows) 646 | if nrows != self.height: 647 | raise ValueError( 648 | "rows supplied (%d) does not match height (%d)" % 649 | (nrows, self.height)) 650 | 651 | def write_passes(self, outfile, rows, packed=False): 652 | """ 653 | Write a PNG image to the output file. 654 | 655 | Most users are expected to find the :meth:`write` or 656 | :meth:`write_array` method more convenient. 657 | 658 | The rows should be given to this method in the order that 659 | they appear in the output file. For straightlaced images, 660 | this is the usual top to bottom ordering, but for interlaced 661 | images the rows should have already been interlaced before 662 | passing them to this function. 663 | 664 | `rows` should be an iterable that yields each row. When 665 | `packed` is ``False`` the rows should be in boxed row flat pixel 666 | format; when `packed` is ``True`` each row should be a packed 667 | sequence of bytes. 668 | 669 | """ 670 | 671 | # http://www.w3.org/TR/PNG/#5PNG-file-signature 672 | outfile.write(_signature) 673 | 674 | # http://www.w3.org/TR/PNG/#11IHDR 675 | write_chunk(outfile, 'IHDR', 676 | struct.pack("!2I5B", self.width, self.height, 677 | self.bitdepth, self.color_type, 678 | 0, 0, self.interlace)) 679 | 680 | # See :chunk:order 681 | # http://www.w3.org/TR/PNG/#11gAMA 682 | if self.gamma is not None: 683 | write_chunk(outfile, 'gAMA', 684 | struct.pack("!L", int(round(self.gamma*1e5)))) 685 | 686 | # See :chunk:order 687 | # http://www.w3.org/TR/PNG/#11sBIT 688 | if self.rescale: 689 | write_chunk(outfile, 'sBIT', 690 | struct.pack('%dB' % self.planes, 691 | *[self.rescale[0]]*self.planes)) 692 | 693 | # :chunk:order: Without a palette (PLTE chunk), ordering is 694 | # relatively relaxed. With one, gAMA chunk must precede PLTE 695 | # chunk which must precede tRNS and bKGD. 696 | # See http://www.w3.org/TR/PNG/#5ChunkOrdering 697 | if self.palette: 698 | p,t = self.make_palette() 699 | write_chunk(outfile, 'PLTE', p) 700 | if t: 701 | # tRNS chunk is optional. Only needed if palette entries 702 | # have alpha. 703 | write_chunk(outfile, 'tRNS', t) 704 | 705 | # http://www.w3.org/TR/PNG/#11tRNS 706 | if self.transparent is not None: 707 | if self.greyscale: 708 | write_chunk(outfile, 'tRNS', 709 | struct.pack("!1H", *self.transparent)) 710 | else: 711 | write_chunk(outfile, 'tRNS', 712 | struct.pack("!3H", *self.transparent)) 713 | 714 | # http://www.w3.org/TR/PNG/#11bKGD 715 | if self.background is not None: 716 | if self.greyscale: 717 | write_chunk(outfile, 'bKGD', 718 | struct.pack("!1H", *self.background)) 719 | else: 720 | write_chunk(outfile, 'bKGD', 721 | struct.pack("!3H", *self.background)) 722 | 723 | # http://www.w3.org/TR/PNG/#11IDAT 724 | if self.compression is not None: 725 | compressor = zlib.compressobj(self.compression) 726 | else: 727 | compressor = zlib.compressobj() 728 | 729 | # Choose an extend function based on the bitdepth. The extend 730 | # function packs/decomposes the pixel values into bytes and 731 | # stuffs them onto the data array. 732 | data = array('B') 733 | if self.bitdepth == 8 or packed: 734 | extend = data.extend 735 | elif self.bitdepth == 16: 736 | # Decompose into bytes 737 | def extend(sl): 738 | fmt = '!%dH' % len(sl) 739 | data.extend(array('B', struct.pack(fmt, *sl))) 740 | else: 741 | # Pack into bytes 742 | assert self.bitdepth < 8 743 | # samples per byte 744 | spb = int(8/self.bitdepth) 745 | def extend(sl): 746 | a = array('B', sl) 747 | # Adding padding bytes so we can group into a whole 748 | # number of spb-tuples. 749 | l = float(len(a)) 750 | extra = math.ceil(l / float(spb))*spb - l 751 | a.extend([0]*int(extra)) 752 | # Pack into bytes 753 | l = group(a, spb) 754 | l = map(lambda e: reduce(lambda x,y: 755 | (x << self.bitdepth) + y, e), l) 756 | data.extend(l) 757 | if self.rescale: 758 | oldextend = extend 759 | factor = \ 760 | float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1) 761 | def extend(sl): 762 | oldextend(map(lambda x: int(round(factor*x)), sl)) 763 | 764 | # Build the first row, testing mostly to see if we need to 765 | # changed the extend function to cope with NumPy integer types 766 | # (they cause our ordinary definition of extend to fail, so we 767 | # wrap it). See 768 | # http://code.google.com/p/pypng/issues/detail?id=44 769 | enumrows = enumerate(rows) 770 | del rows 771 | 772 | # First row's filter type. 773 | data.append(0) 774 | # :todo: Certain exceptions in the call to ``.next()`` or the 775 | # following try would indicate no row data supplied. 776 | # Should catch. 777 | i,row = enumrows.next() 778 | try: 779 | # If this fails... 780 | extend(row) 781 | except: 782 | # ... try a version that converts the values to int first. 783 | # Not only does this work for the (slightly broken) NumPy 784 | # types, there are probably lots of other, unknown, "nearly" 785 | # int types it works for. 786 | def wrapmapint(f): 787 | return lambda sl: f(map(int, sl)) 788 | extend = wrapmapint(extend) 789 | del wrapmapint 790 | extend(row) 791 | 792 | for i,row in enumrows: 793 | # Add "None" filter type. Currently, it's essential that 794 | # this filter type be used for every scanline as we do not 795 | # mark the first row of a reduced pass image; that means we 796 | # could accidentally compute the wrong filtered scanline if 797 | # we used "up", "average", or "paeth" on such a line. 798 | data.append(0) 799 | extend(row) 800 | if len(data) > self.chunk_limit: 801 | compressed = compressor.compress(tostring(data)) 802 | if len(compressed): 803 | # print >> sys.stderr, len(data), len(compressed) 804 | write_chunk(outfile, 'IDAT', compressed) 805 | # Because of our very witty definition of ``extend``, 806 | # above, we must re-use the same ``data`` object. Hence 807 | # we use ``del`` to empty this one, rather than create a 808 | # fresh one (which would be my natural FP instinct). 809 | del data[:] 810 | if len(data): 811 | compressed = compressor.compress(tostring(data)) 812 | else: 813 | compressed = '' 814 | flushed = compressor.flush() 815 | if len(compressed) or len(flushed): 816 | # print >> sys.stderr, len(data), len(compressed), len(flushed) 817 | write_chunk(outfile, 'IDAT', compressed + flushed) 818 | # http://www.w3.org/TR/PNG/#11IEND 819 | write_chunk(outfile, 'IEND') 820 | return i+1 821 | 822 | def write_array(self, outfile, pixels): 823 | """ 824 | Write an array in flat row flat pixel format as a PNG file on 825 | the output file. See also :meth:`write` method. 826 | """ 827 | 828 | if self.interlace: 829 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 830 | else: 831 | self.write_passes(outfile, self.array_scanlines(pixels)) 832 | 833 | def write_packed(self, outfile, rows): 834 | """ 835 | Write PNG file to `outfile`. The pixel data comes from `rows` 836 | which should be in boxed row packed format. Each row should be 837 | a sequence of packed bytes. 838 | 839 | Technically, this method does work for interlaced images but it 840 | is best avoided. For interlaced images, the rows should be 841 | presented in the order that they appear in the file. 842 | 843 | This method should not be used when the source image bit depth 844 | is not one naturally supported by PNG; the bit depth should be 845 | 1, 2, 4, 8, or 16. 846 | """ 847 | 848 | if self.rescale: 849 | raise Error("write_packed method not suitable for bit depth %d" % 850 | self.rescale[0]) 851 | return self.write_passes(outfile, rows, packed=True) 852 | 853 | def convert_pnm(self, infile, outfile): 854 | """ 855 | Convert a PNM file containing raw pixel data into a PNG file 856 | with the parameters set in the writer object. Works for 857 | (binary) PGM, PPM, and PAM formats. 858 | """ 859 | 860 | if self.interlace: 861 | pixels = array('B') 862 | pixels.fromfile(infile, 863 | (self.bitdepth/8) * self.color_planes * 864 | self.width * self.height) 865 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 866 | else: 867 | self.write_passes(outfile, self.file_scanlines(infile)) 868 | 869 | def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): 870 | """ 871 | Convert a PPM and PGM file containing raw pixel data into a 872 | PNG outfile with the parameters set in the writer object. 873 | """ 874 | pixels = array('B') 875 | pixels.fromfile(ppmfile, 876 | (self.bitdepth/8) * self.color_planes * 877 | self.width * self.height) 878 | apixels = array('B') 879 | apixels.fromfile(pgmfile, 880 | (self.bitdepth/8) * 881 | self.width * self.height) 882 | pixels = interleave_planes(pixels, apixels, 883 | (self.bitdepth/8) * self.color_planes, 884 | (self.bitdepth/8)) 885 | if self.interlace: 886 | self.write_passes(outfile, self.array_scanlines_interlace(pixels)) 887 | else: 888 | self.write_passes(outfile, self.array_scanlines(pixels)) 889 | 890 | def file_scanlines(self, infile): 891 | """ 892 | Generates boxed rows in flat pixel format, from the input file 893 | `infile`. It assumes that the input file is in a "Netpbm-like" 894 | binary format, and is positioned at the beginning of the first 895 | pixel. The number of pixels to read is taken from the image 896 | dimensions (`width`, `height`, `planes`) and the number of bytes 897 | per value is implied by the image `bitdepth`. 898 | """ 899 | 900 | # Values per row 901 | vpr = self.width * self.planes 902 | row_bytes = vpr 903 | if self.bitdepth > 8: 904 | assert self.bitdepth == 16 905 | row_bytes *= 2 906 | fmt = '>%dH' % vpr 907 | def line(): 908 | return array('H', struct.unpack(fmt, infile.read(row_bytes))) 909 | else: 910 | def line(): 911 | scanline = array('B', infile.read(row_bytes)) 912 | return scanline 913 | for y in range(self.height): 914 | yield line() 915 | 916 | def array_scanlines(self, pixels): 917 | """ 918 | Generates boxed rows (flat pixels) from flat rows (flat pixels) 919 | in an array. 920 | """ 921 | 922 | # Values per row 923 | vpr = self.width * self.planes 924 | stop = 0 925 | for y in range(self.height): 926 | start = stop 927 | stop = start + vpr 928 | yield pixels[start:stop] 929 | 930 | def array_scanlines_interlace(self, pixels): 931 | """ 932 | Generator for interlaced scanlines from an array. `pixels` is 933 | the full source image in flat row flat pixel format. The 934 | generator yields each scanline of the reduced passes in turn, in 935 | boxed row flat pixel format. 936 | """ 937 | 938 | # http://www.w3.org/TR/PNG/#8InterlaceMethods 939 | # Array type. 940 | fmt = 'BH'[self.bitdepth > 8] 941 | # Value per row 942 | vpr = self.width * self.planes 943 | for xstart, ystart, xstep, ystep in _adam7: 944 | if xstart >= self.width: 945 | continue 946 | # Pixels per row (of reduced image) 947 | ppr = int(math.ceil((self.width-xstart)/float(xstep))) 948 | # number of values in reduced image row. 949 | row_len = ppr*self.planes 950 | for y in range(ystart, self.height, ystep): 951 | if xstep == 1: 952 | offset = y * vpr 953 | yield pixels[offset:offset+vpr] 954 | else: 955 | row = array(fmt) 956 | # There's no easier way to set the length of an array 957 | row.extend(pixels[0:row_len]) 958 | offset = y * vpr + xstart * self.planes 959 | end_offset = (y+1) * vpr 960 | skip = self.planes * xstep 961 | for i in range(self.planes): 962 | row[i::self.planes] = \ 963 | pixels[offset+i:end_offset:skip] 964 | yield row 965 | 966 | def write_chunk(outfile, tag, data=strtobytes('')): 967 | """ 968 | Write a PNG chunk to the output file, including length and 969 | checksum. 970 | """ 971 | 972 | # http://www.w3.org/TR/PNG/#5Chunk-layout 973 | outfile.write(struct.pack("!I", len(data))) 974 | tag = strtobytes(tag) 975 | outfile.write(tag) 976 | outfile.write(data) 977 | checksum = zlib.crc32(tag) 978 | checksum = zlib.crc32(data, checksum) 979 | checksum &= 2**32-1 980 | outfile.write(struct.pack("!I", checksum)) 981 | 982 | def write_chunks(out, chunks): 983 | """Create a PNG file by writing out the chunks.""" 984 | 985 | out.write(_signature) 986 | for chunk in chunks: 987 | write_chunk(out, *chunk) 988 | 989 | def filter_scanline(type, line, fo, prev=None): 990 | """Apply a scanline filter to a scanline. `type` specifies the 991 | filter type (0 to 4); `line` specifies the current (unfiltered) 992 | scanline as a sequence of bytes; `prev` specifies the previous 993 | (unfiltered) scanline as a sequence of bytes. `fo` specifies the 994 | filter offset; normally this is size of a pixel in bytes (the number 995 | of bytes per sample times the number of channels), but when this is 996 | < 1 (for bit depths < 8) then the filter offset is 1. 997 | """ 998 | 999 | assert 0 <= type < 5 1000 | 1001 | # The output array. Which, pathetically, we extend one-byte at a 1002 | # time (fortunately this is linear). 1003 | out = array('B', [type]) 1004 | 1005 | def sub(): 1006 | ai = -fo 1007 | for x in line: 1008 | if ai >= 0: 1009 | x = (x - line[ai]) & 0xff 1010 | out.append(x) 1011 | ai += 1 1012 | def up(): 1013 | for i,x in enumerate(line): 1014 | x = (x - prev[i]) & 0xff 1015 | out.append(x) 1016 | def average(): 1017 | ai = -fo 1018 | for i,x in enumerate(line): 1019 | if ai >= 0: 1020 | x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff 1021 | else: 1022 | x = (x - (prev[i] >> 1)) & 0xff 1023 | out.append(x) 1024 | ai += 1 1025 | def paeth(): 1026 | # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth 1027 | ai = -fo # also used for ci 1028 | for i,x in enumerate(line): 1029 | a = 0 1030 | b = prev[i] 1031 | c = 0 1032 | 1033 | if ai >= 0: 1034 | a = line[ai] 1035 | c = prev[ai] 1036 | p = a + b - c 1037 | pa = abs(p - a) 1038 | pb = abs(p - b) 1039 | pc = abs(p - c) 1040 | if pa <= pb and pa <= pc: Pr = a 1041 | elif pb <= pc: Pr = b 1042 | else: Pr = c 1043 | 1044 | x = (x - Pr) & 0xff 1045 | out.append(x) 1046 | ai += 1 1047 | 1048 | if not prev: 1049 | # We're on the first line. Some of the filters can be reduced 1050 | # to simpler cases which makes handling the line "off the top" 1051 | # of the image simpler. "up" becomes "none"; "paeth" becomes 1052 | # "left" (non-trivial, but true). "average" needs to be handled 1053 | # specially. 1054 | if type == 2: # "up" 1055 | return line # type = 0 1056 | elif type == 3: 1057 | prev = [0]*len(line) 1058 | elif type == 4: # "paeth" 1059 | type = 1 1060 | if type == 0: 1061 | out.extend(line) 1062 | elif type == 1: 1063 | sub() 1064 | elif type == 2: 1065 | up() 1066 | elif type == 3: 1067 | average() 1068 | else: # type == 4 1069 | paeth() 1070 | return out 1071 | 1072 | 1073 | def from_array(a, mode=None, info={}): 1074 | """Create a PNG :class:`Image` object from a 2- or 3-dimensional array. 1075 | One application of this function is easy PIL-style saving: 1076 | ``png.from_array(pixels, 'L').save('foo.png')``. 1077 | 1078 | .. note : 1079 | 1080 | The use of the term *3-dimensional* is for marketing purposes 1081 | only. It doesn't actually work. Please bear with us. Meanwhile 1082 | enjoy the complimentary snacks (on request) and please use a 1083 | 2-dimensional array. 1084 | 1085 | Unless they are specified using the *info* parameter, the PNG's 1086 | height and width are taken from the array size. For a 3 dimensional 1087 | array the first axis is the height; the second axis is the width; 1088 | and the third axis is the channel number. Thus an RGB image that is 1089 | 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 1090 | dimensional arrays the first axis is the height, but the second axis 1091 | is ``width*channels``, so an RGB image that is 16 pixels high and 8 1092 | wide will use a 2-dimensional array that is 16x24 (each row will be 1093 | 8*3==24 sample values). 1094 | 1095 | *mode* is a string that specifies the image colour format in a 1096 | PIL-style mode. It can be: 1097 | 1098 | ``'L'`` 1099 | greyscale (1 channel) 1100 | ``'LA'`` 1101 | greyscale with alpha (2 channel) 1102 | ``'RGB'`` 1103 | colour image (3 channel) 1104 | ``'RGBA'`` 1105 | colour image with alpha (4 channel) 1106 | 1107 | The mode string can also specify the bit depth (overriding how this 1108 | function normally derives the bit depth, see below). Appending 1109 | ``';16'`` to the mode will cause the PNG to be 16 bits per channel; 1110 | any decimal from 1 to 16 can be used to specify the bit depth. 1111 | 1112 | When a 2-dimensional array is used *mode* determines how many 1113 | channels the image has, and so allows the width to be derived from 1114 | the second array dimension. 1115 | 1116 | The array is expected to be a ``numpy`` array, but it can be any 1117 | suitable Python sequence. For example, a list of lists can be used: 1118 | ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact 1119 | rules are: ``len(a)`` gives the first dimension, height; 1120 | ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the 1121 | third dimension, unless an exception is raised in which case a 1122 | 2-dimensional array is assumed. It's slightly more complicated than 1123 | that because an iterator of rows can be used, and it all still 1124 | works. Using an iterator allows data to be streamed efficiently. 1125 | 1126 | The bit depth of the PNG is normally taken from the array element's 1127 | datatype (but if *mode* specifies a bitdepth then that is used 1128 | instead). The array element's datatype is determined in a way which 1129 | is supposed to work both for ``numpy`` arrays and for Python 1130 | ``array.array`` objects. A 1 byte datatype will give a bit depth of 1131 | 8, a 2 byte datatype will give a bit depth of 16. If the datatype 1132 | does not have an implicit size, for example it is a plain Python 1133 | list of lists, as above, then a default of 8 is used. 1134 | 1135 | The *info* parameter is a dictionary that can be used to specify 1136 | metadata (in the same style as the arguments to the 1137 | :class:``png.Writer`` class). For this function the keys that are 1138 | useful are: 1139 | 1140 | height 1141 | overrides the height derived from the array dimensions and allows 1142 | *a* to be an iterable. 1143 | width 1144 | overrides the width derived from the array dimensions. 1145 | bitdepth 1146 | overrides the bit depth derived from the element datatype (but 1147 | must match *mode* if that also specifies a bit depth). 1148 | 1149 | Generally anything specified in the 1150 | *info* dictionary will override any implicit choices that this 1151 | function would otherwise make, but must match any explicit ones. 1152 | For example, if the *info* dictionary has a ``greyscale`` key then 1153 | this must be true when mode is ``'L'`` or ``'LA'`` and false when 1154 | mode is ``'RGB'`` or ``'RGBA'``. 1155 | """ 1156 | 1157 | # We abuse the *info* parameter by modifying it. Take a copy here. 1158 | # (Also typechecks *info* to some extent). 1159 | info = dict(info) 1160 | 1161 | # Syntax check mode string. 1162 | bitdepth = None 1163 | try: 1164 | mode = mode.split(';') 1165 | if len(mode) not in (1,2): 1166 | raise Error() 1167 | if mode[0] not in ('L', 'LA', 'RGB', 'RGBA'): 1168 | raise Error() 1169 | if len(mode) == 2: 1170 | try: 1171 | bitdepth = int(mode[1]) 1172 | except: 1173 | raise Error() 1174 | except Error: 1175 | raise Error("mode string should be 'RGB' or 'L;16' or similar.") 1176 | mode = mode[0] 1177 | 1178 | # Get bitdepth from *mode* if possible. 1179 | if bitdepth: 1180 | if info.get('bitdepth') and bitdepth != info['bitdepth']: 1181 | raise Error("mode bitdepth (%d) should match info bitdepth (%d)." % 1182 | (bitdepth, info['bitdepth'])) 1183 | info['bitdepth'] = bitdepth 1184 | 1185 | # Fill in and/or check entries in *info*. 1186 | # Dimensions. 1187 | if 'size' in info: 1188 | # Check width, height, size all match where used. 1189 | for dimension,axis in [('width', 0), ('height', 1)]: 1190 | if dimension in info: 1191 | if info[dimension] != info['size'][axis]: 1192 | raise Error( 1193 | "info[%r] shhould match info['size'][%r]." % 1194 | (dimension, axis)) 1195 | info['width'],info['height'] = info['size'] 1196 | if 'height' not in info: 1197 | try: 1198 | l = len(a) 1199 | except: 1200 | raise Error( 1201 | "len(a) does not work, supply info['height'] instead.") 1202 | info['height'] = l 1203 | # Colour format. 1204 | if 'greyscale' in info: 1205 | if bool(info['greyscale']) != ('L' in mode): 1206 | raise Error("info['greyscale'] should match mode.") 1207 | info['greyscale'] = 'L' in mode 1208 | if 'alpha' in info: 1209 | if bool(info['alpha']) != ('A' in mode): 1210 | raise Error("info['alpha'] should match mode.") 1211 | info['alpha'] = 'A' in mode 1212 | 1213 | planes = len(mode) 1214 | if 'planes' in info: 1215 | if info['planes'] != planes: 1216 | raise Error("info['planes'] should match mode.") 1217 | 1218 | # In order to work out whether we the array is 2D or 3D we need its 1219 | # first row, which requires that we take a copy of its iterator. 1220 | # We may also need the first row to derive width and bitdepth. 1221 | a,t = itertools.tee(a) 1222 | row = t.next() 1223 | del t 1224 | try: 1225 | row[0][0] 1226 | threed = True 1227 | testelement = row[0] 1228 | except: 1229 | threed = False 1230 | testelement = row 1231 | if 'width' not in info: 1232 | if threed: 1233 | width = len(row) 1234 | else: 1235 | width = len(row) // planes 1236 | info['width'] = width 1237 | 1238 | # Not implemented yet 1239 | assert not threed 1240 | 1241 | if 'bitdepth' not in info: 1242 | try: 1243 | dtype = testelement.dtype 1244 | # goto the "else:" clause. Sorry. 1245 | except: 1246 | try: 1247 | # Try a Python array.array. 1248 | bitdepth = 8 * testelement.itemsize 1249 | except: 1250 | # We can't determine it from the array element's 1251 | # datatype, use a default of 8. 1252 | bitdepth = 8 1253 | else: 1254 | # If we got here without exception, we now assume that 1255 | # the array is a numpy array. 1256 | if dtype.kind == 'b': 1257 | bitdepth = 1 1258 | else: 1259 | bitdepth = 8 * dtype.itemsize 1260 | info['bitdepth'] = bitdepth 1261 | 1262 | for thing in 'width height bitdepth greyscale alpha'.split(): 1263 | assert thing in info 1264 | return Image(a, info) 1265 | 1266 | # So that refugee's from PIL feel more at home. Not documented. 1267 | fromarray = from_array 1268 | 1269 | class Image: 1270 | """A PNG image. 1271 | You can create an :class:`Image` object from an array of pixels by calling 1272 | :meth:`png.from_array`. It can be saved to disk with the 1273 | :meth:`save` method.""" 1274 | def __init__(self, rows, info): 1275 | """ 1276 | .. note :: 1277 | 1278 | The constructor is not public. Please do not call it. 1279 | """ 1280 | 1281 | self.rows = rows 1282 | self.info = info 1283 | 1284 | def save(self, file): 1285 | """Save the image to *file*. If *file* looks like an open file 1286 | descriptor then it is used, otherwise it is treated as a 1287 | filename and a fresh file is opened. 1288 | 1289 | In general, you can only call this method once; after it has 1290 | been called the first time and the PNG image has been saved, the 1291 | source data will have been streamed, and cannot be streamed 1292 | again. 1293 | """ 1294 | 1295 | w = Writer(**self.info) 1296 | 1297 | try: 1298 | file.write 1299 | def close(): pass 1300 | except: 1301 | file = open(file, 'wb') 1302 | def close(): file.close() 1303 | 1304 | try: 1305 | w.write(file, self.rows) 1306 | finally: 1307 | close() 1308 | 1309 | class _readable: 1310 | """ 1311 | A simple file-like interface for strings and arrays. 1312 | """ 1313 | 1314 | def __init__(self, buf): 1315 | self.buf = buf 1316 | self.offset = 0 1317 | 1318 | def read(self, n): 1319 | r = self.buf[self.offset:self.offset+n] 1320 | if isarray(r): 1321 | r = r.tostring() 1322 | self.offset += n 1323 | return r 1324 | 1325 | 1326 | class Reader: 1327 | """ 1328 | PNG decoder in pure Python. 1329 | """ 1330 | 1331 | def __init__(self, _guess=None, **kw): 1332 | """ 1333 | Create a PNG decoder object. 1334 | 1335 | The constructor expects exactly one keyword argument. If you 1336 | supply a positional argument instead, it will guess the input 1337 | type. You can choose among the following keyword arguments: 1338 | 1339 | filename 1340 | Name of input file (a PNG file). 1341 | file 1342 | A file-like object (object with a read() method). 1343 | bytes 1344 | ``array`` or ``string`` with PNG data. 1345 | 1346 | """ 1347 | if ((_guess is not None and len(kw) != 0) or 1348 | (_guess is None and len(kw) != 1)): 1349 | raise TypeError("Reader() takes exactly 1 argument") 1350 | 1351 | # Will be the first 8 bytes, later on. See validate_signature. 1352 | self.signature = None 1353 | self.transparent = None 1354 | # A pair of (len,type) if a chunk has been read but its data and 1355 | # checksum have not (in other words the file position is just 1356 | # past the 4 bytes that specify the chunk type). See preamble 1357 | # method for how this is used. 1358 | self.atchunk = None 1359 | 1360 | if _guess is not None: 1361 | if isarray(_guess): 1362 | kw["bytes"] = _guess 1363 | elif isinstance(_guess, str): 1364 | kw["filename"] = _guess 1365 | elif isinstance(_guess, file): 1366 | kw["file"] = _guess 1367 | 1368 | if "filename" in kw: 1369 | self.file = open(kw["filename"], "rb") 1370 | elif "file" in kw: 1371 | self.file = kw["file"] 1372 | elif "bytes" in kw: 1373 | self.file = _readable(kw["bytes"]) 1374 | else: 1375 | raise TypeError("expecting filename, file or bytes array") 1376 | 1377 | 1378 | def chunk(self, seek=None, lenient=False): 1379 | """ 1380 | Read the next PNG chunk from the input file; returns a 1381 | (*type*,*data*) tuple. *type* is the chunk's type as a string 1382 | (all PNG chunk types are 4 characters long). *data* is the 1383 | chunk's data content, as a string. 1384 | 1385 | If the optional `seek` argument is 1386 | specified then it will keep reading chunks until it either runs 1387 | out of file or finds the type specified by the argument. Note 1388 | that in general the order of chunks in PNGs is unspecified, so 1389 | using `seek` can cause you to miss chunks. 1390 | 1391 | If the optional `lenient` argument evaluates to True, 1392 | checksum failures will raise warnings rather than exceptions. 1393 | """ 1394 | 1395 | self.validate_signature() 1396 | 1397 | while True: 1398 | # http://www.w3.org/TR/PNG/#5Chunk-layout 1399 | if not self.atchunk: 1400 | self.atchunk = self.chunklentype() 1401 | length,type = self.atchunk 1402 | self.atchunk = None 1403 | data = self.file.read(length) 1404 | if len(data) != length: 1405 | raise ChunkError('Chunk %s too short for required %i octets.' 1406 | % (type, length)) 1407 | checksum = self.file.read(4) 1408 | if len(checksum) != 4: 1409 | raise ValueError('Chunk %s too short for checksum.', tag) 1410 | if seek and type != seek: 1411 | continue 1412 | verify = zlib.crc32(strtobytes(type)) 1413 | verify = zlib.crc32(data, verify) 1414 | # Whether the output from zlib.crc32 is signed or not varies 1415 | # according to hideous implementation details, see 1416 | # http://bugs.python.org/issue1202 . 1417 | # We coerce it to be positive here (in a way which works on 1418 | # Python 2.3 and older). 1419 | verify &= 2**32 - 1 1420 | verify = struct.pack('!I', verify) 1421 | if checksum != verify: 1422 | # print repr(checksum) 1423 | (a, ) = struct.unpack('!I', checksum) 1424 | (b, ) = struct.unpack('!I', verify) 1425 | message = "Checksum error in %s chunk: 0x%08X != 0x%08X." % (type, a, b) 1426 | if lenient: 1427 | warnings.warn(message, RuntimeWarning) 1428 | else: 1429 | raise ChunkError(message) 1430 | return type, data 1431 | 1432 | def chunks(self): 1433 | """Return an iterator that will yield each chunk as a 1434 | (*chunktype*, *content*) pair. 1435 | """ 1436 | 1437 | while True: 1438 | t,v = self.chunk() 1439 | yield t,v 1440 | if t == 'IEND': 1441 | break 1442 | 1443 | def undo_filter(self, filter_type, scanline, previous): 1444 | """Undo the filter for a scanline. `scanline` is a sequence of 1445 | bytes that does not include the initial filter type byte. 1446 | `previous` is decoded previous scanline (for straightlaced 1447 | images this is the previous pixel row, but for interlaced 1448 | images, it is the previous scanline in the reduced image, which 1449 | in general is not the previous pixel row in the final image). 1450 | When there is no previous scanline (the first row of a 1451 | straightlaced image, or the first row in one of the passes in an 1452 | interlaced image), then this argument should be ``None``. 1453 | 1454 | The scanline will have the effects of filtering removed, and the 1455 | result will be returned as a fresh sequence of bytes. 1456 | """ 1457 | 1458 | # :todo: Would it be better to update scanline in place? 1459 | # Yes, with the Cython extension making the undo_filter fast, 1460 | # updating scanline inplace makes the code 3 times faster 1461 | # (reading 50 images of 800x800 went from 40s to 16s) 1462 | result = scanline 1463 | 1464 | if filter_type == 0: 1465 | return result 1466 | 1467 | if filter_type not in (1,2,3,4): 1468 | raise FormatError('Invalid PNG Filter Type.' 1469 | ' See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .') 1470 | 1471 | # Filter unit. The stride from one pixel to the corresponding 1472 | # byte from the previous previous. Normally this is the pixel 1473 | # size in bytes, but when this is smaller than 1, the previous 1474 | # byte is used instead. 1475 | fu = max(1, self.psize) 1476 | 1477 | # For the first line of a pass, synthesize a dummy previous 1478 | # line. An alternative approach would be to observe that on the 1479 | # first line 'up' is the same as 'null', 'paeth' is the same 1480 | # as 'sub', with only 'average' requiring any special case. 1481 | if not previous: 1482 | previous = array('B', [0]*len(scanline)) 1483 | 1484 | def sub(): 1485 | """Undo sub filter.""" 1486 | 1487 | ai = 0 1488 | # Loops starts at index fu. Observe that the initial part 1489 | # of the result is already filled in correctly with 1490 | # scanline. 1491 | for i in range(fu, len(result)): 1492 | x = scanline[i] 1493 | a = result[ai] 1494 | result[i] = (x + a) & 0xff 1495 | ai += 1 1496 | 1497 | def up(): 1498 | """Undo up filter.""" 1499 | 1500 | for i in range(len(result)): 1501 | x = scanline[i] 1502 | b = previous[i] 1503 | result[i] = (x + b) & 0xff 1504 | 1505 | def average(): 1506 | """Undo average filter.""" 1507 | 1508 | ai = -fu 1509 | for i in range(len(result)): 1510 | x = scanline[i] 1511 | if ai < 0: 1512 | a = 0 1513 | else: 1514 | a = result[ai] 1515 | b = previous[i] 1516 | result[i] = (x + ((a + b) >> 1)) & 0xff 1517 | ai += 1 1518 | 1519 | def paeth(): 1520 | """Undo Paeth filter.""" 1521 | 1522 | # Also used for ci. 1523 | ai = -fu 1524 | for i in range(len(result)): 1525 | x = scanline[i] 1526 | if ai < 0: 1527 | a = c = 0 1528 | else: 1529 | a = result[ai] 1530 | c = previous[ai] 1531 | b = previous[i] 1532 | p = a + b - c 1533 | pa = abs(p - a) 1534 | pb = abs(p - b) 1535 | pc = abs(p - c) 1536 | if pa <= pb and pa <= pc: 1537 | pr = a 1538 | elif pb <= pc: 1539 | pr = b 1540 | else: 1541 | pr = c 1542 | result[i] = (x + pr) & 0xff 1543 | ai += 1 1544 | 1545 | # Call appropriate filter algorithm. Note that 0 has already 1546 | # been dealt with. 1547 | (None, 1548 | pngfilters.undo_filter_sub, 1549 | pngfilters.undo_filter_up, 1550 | pngfilters.undo_filter_average, 1551 | pngfilters.undo_filter_paeth)[filter_type](fu, scanline, previous, result) 1552 | return result 1553 | 1554 | def deinterlace(self, raw): 1555 | """ 1556 | Read raw pixel data, undo filters, deinterlace, and flatten. 1557 | Return in flat row flat pixel format. 1558 | """ 1559 | 1560 | # print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," + 1561 | # " bpp=%s") % (self.width, self.height, self.planes, self.bps) 1562 | # Values per row (of the target image) 1563 | vpr = self.width * self.planes 1564 | 1565 | # Make a result array, and make it big enough. Interleaving 1566 | # writes to the output array randomly (well, not quite), so the 1567 | # entire output array must be in memory. 1568 | fmt = 'BH'[self.bitdepth > 8] 1569 | a = array(fmt, [0]*vpr*self.height) 1570 | source_offset = 0 1571 | 1572 | for xstart, ystart, xstep, ystep in _adam7: 1573 | # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % ( 1574 | # xstart, ystart, xstep, ystep) 1575 | if xstart >= self.width: 1576 | continue 1577 | # The previous (reconstructed) scanline. None at the 1578 | # beginning of a pass to indicate that there is no previous 1579 | # line. 1580 | recon = None 1581 | # Pixels per row (reduced pass image) 1582 | ppr = int(math.ceil((self.width-xstart)/float(xstep))) 1583 | # Row size in bytes for this pass. 1584 | row_size = int(math.ceil(self.psize * ppr)) 1585 | for y in range(ystart, self.height, ystep): 1586 | filter_type = raw[source_offset] 1587 | source_offset += 1 1588 | scanline = raw[source_offset:source_offset+row_size] 1589 | source_offset += row_size 1590 | recon = self.undo_filter(filter_type, scanline, recon) 1591 | # Convert so that there is one element per pixel value 1592 | flat = self.serialtoflat(recon, ppr) 1593 | if xstep == 1: 1594 | assert xstart == 0 1595 | offset = y * vpr 1596 | a[offset:offset+vpr] = flat 1597 | else: 1598 | offset = y * vpr + xstart * self.planes 1599 | end_offset = (y+1) * vpr 1600 | skip = self.planes * xstep 1601 | for i in range(self.planes): 1602 | a[offset+i:end_offset:skip] = \ 1603 | flat[i::self.planes] 1604 | return a 1605 | 1606 | def iterboxed(self, rows): 1607 | """Iterator that yields each scanline in boxed row flat pixel 1608 | format. `rows` should be an iterator that yields the bytes of 1609 | each row in turn. 1610 | """ 1611 | 1612 | def asvalues(raw): 1613 | """Convert a row of raw bytes into a flat row. Result may 1614 | or may not share with argument""" 1615 | 1616 | if self.bitdepth == 8: 1617 | return raw 1618 | if self.bitdepth == 16: 1619 | raw = tostring(raw) 1620 | return array('H', struct.unpack('!%dH' % (len(raw)//2), raw)) 1621 | assert self.bitdepth < 8 1622 | width = self.width 1623 | # Samples per byte 1624 | spb = 8//self.bitdepth 1625 | out = array('B') 1626 | mask = 2**self.bitdepth - 1 1627 | shifts = map(self.bitdepth.__mul__, reversed(range(spb))) 1628 | for o in raw: 1629 | out.extend(map(lambda i: mask&(o>>i), shifts)) 1630 | return out[:width] 1631 | 1632 | return itertools.imap(asvalues, rows) 1633 | 1634 | def serialtoflat(self, bytes, width=None): 1635 | """Convert serial format (byte stream) pixel data to flat row 1636 | flat pixel. 1637 | """ 1638 | 1639 | if self.bitdepth == 8: 1640 | return bytes 1641 | if self.bitdepth == 16: 1642 | bytes = tostring(bytes) 1643 | return array('H', 1644 | struct.unpack('!%dH' % (len(bytes)//2), bytes)) 1645 | assert self.bitdepth < 8 1646 | if width is None: 1647 | width = self.width 1648 | # Samples per byte 1649 | spb = 8//self.bitdepth 1650 | out = array('B') 1651 | mask = 2**self.bitdepth - 1 1652 | shifts = map(self.bitdepth.__mul__, reversed(range(spb))) 1653 | l = width 1654 | for o in bytes: 1655 | out.extend([(mask&(o>>s)) for s in shifts][:l]) 1656 | l -= spb 1657 | if l <= 0: 1658 | l = width 1659 | return out 1660 | 1661 | def iterstraight(self, raw): 1662 | """Iterator that undoes the effect of filtering, and yields each 1663 | row in serialised format (as a sequence of bytes). Assumes input 1664 | is straightlaced. `raw` should be an iterable that yields the 1665 | raw bytes in chunks of arbitrary size.""" 1666 | 1667 | # length of row, in bytes 1668 | rb = self.row_bytes 1669 | a = array('B') 1670 | # The previous (reconstructed) scanline. None indicates first 1671 | # line of image. 1672 | recon = None 1673 | for some in raw: 1674 | a.extend(some) 1675 | while len(a) >= rb + 1: 1676 | filter_type = a[0] 1677 | scanline = a[1:rb+1] 1678 | del a[:rb+1] 1679 | recon = self.undo_filter(filter_type, scanline, recon) 1680 | yield recon 1681 | if len(a) != 0: 1682 | # :file:format We get here with a file format error: when the 1683 | # available bytes (after decompressing) do not pack into exact 1684 | # rows. 1685 | raise FormatError( 1686 | 'Wrong size for decompressed IDAT chunk.') 1687 | assert len(a) == 0 1688 | 1689 | def validate_signature(self): 1690 | """If signature (header) has not been read then read and 1691 | validate it; otherwise do nothing. 1692 | """ 1693 | 1694 | if self.signature: 1695 | return 1696 | self.signature = self.file.read(8) 1697 | if self.signature != _signature: 1698 | raise FormatError("PNG file has invalid signature.") 1699 | 1700 | def preamble(self, lenient=False): 1701 | """ 1702 | Extract the image metadata by reading the initial part of the PNG 1703 | file up to the start of the ``IDAT`` chunk. All the chunks that 1704 | precede the ``IDAT`` chunk are read and either processed for 1705 | metadata or discarded. 1706 | 1707 | If the optional `lenient` argument evaluates to True, 1708 | checksum failures will raise warnings rather than exceptions. 1709 | """ 1710 | 1711 | self.validate_signature() 1712 | 1713 | while True: 1714 | if not self.atchunk: 1715 | self.atchunk = self.chunklentype() 1716 | if self.atchunk is None: 1717 | raise FormatError( 1718 | 'This PNG file has no IDAT chunks.') 1719 | if self.atchunk[1] == 'IDAT': 1720 | return 1721 | self.process_chunk(lenient=lenient) 1722 | 1723 | def chunklentype(self): 1724 | """Reads just enough of the input to determine the next 1725 | chunk's length and type, returned as a (*length*, *type*) pair 1726 | where *type* is a string. If there are no more chunks, ``None`` 1727 | is returned. 1728 | """ 1729 | 1730 | x = self.file.read(8) 1731 | if not x: 1732 | return None 1733 | if len(x) != 8: 1734 | raise FormatError( 1735 | 'End of file whilst reading chunk length and type.') 1736 | length,type = struct.unpack('!I4s', x) 1737 | type = bytestostr(type) 1738 | if length > 2**31-1: 1739 | raise FormatError('Chunk %s is too large: %d.' % (type,length)) 1740 | return length,type 1741 | 1742 | def process_chunk(self, lenient=False): 1743 | """Process the next chunk and its data. This only processes the 1744 | following chunk types, all others are ignored: ``IHDR``, 1745 | ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``. 1746 | 1747 | If the optional `lenient` argument evaluates to True, 1748 | checksum failures will raise warnings rather than exceptions. 1749 | """ 1750 | 1751 | type, data = self.chunk(lenient=lenient) 1752 | if type == 'IHDR': 1753 | # http://www.w3.org/TR/PNG/#11IHDR 1754 | if len(data) != 13: 1755 | raise FormatError('IHDR chunk has incorrect length.') 1756 | (self.width, self.height, self.bitdepth, self.color_type, 1757 | self.compression, self.filter, 1758 | self.interlace) = struct.unpack("!2I5B", data) 1759 | 1760 | # Check that the header specifies only valid combinations. 1761 | if self.bitdepth not in (1,2,4,8,16): 1762 | raise Error("invalid bit depth %d" % self.bitdepth) 1763 | if self.color_type not in (0,2,3,4,6): 1764 | raise Error("invalid colour type %d" % self.color_type) 1765 | # Check indexed (palettized) images have 8 or fewer bits 1766 | # per pixel; check only indexed or greyscale images have 1767 | # fewer than 8 bits per pixel. 1768 | if ((self.color_type & 1 and self.bitdepth > 8) or 1769 | (self.bitdepth < 8 and self.color_type not in (0,3))): 1770 | raise FormatError("Illegal combination of bit depth (%d)" 1771 | " and colour type (%d)." 1772 | " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." 1773 | % (self.bitdepth, self.color_type)) 1774 | if self.compression != 0: 1775 | raise Error("unknown compression method %d" % self.compression) 1776 | if self.filter != 0: 1777 | raise FormatError("Unknown filter method %d," 1778 | " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." 1779 | % self.filter) 1780 | if self.interlace not in (0,1): 1781 | raise FormatError("Unknown interlace method %d," 1782 | " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ." 1783 | % self.interlace) 1784 | 1785 | # Derived values 1786 | # http://www.w3.org/TR/PNG/#6Colour-values 1787 | colormap = bool(self.color_type & 1) 1788 | greyscale = not (self.color_type & 2) 1789 | alpha = bool(self.color_type & 4) 1790 | color_planes = (3,1)[greyscale or colormap] 1791 | planes = color_planes + alpha 1792 | 1793 | self.colormap = colormap 1794 | self.greyscale = greyscale 1795 | self.alpha = alpha 1796 | self.color_planes = color_planes 1797 | self.planes = planes 1798 | self.psize = float(self.bitdepth)/float(8) * planes 1799 | if int(self.psize) == self.psize: 1800 | self.psize = int(self.psize) 1801 | self.row_bytes = int(math.ceil(self.width * self.psize)) 1802 | # Stores PLTE chunk if present, and is used to check 1803 | # chunk ordering constraints. 1804 | self.plte = None 1805 | # Stores tRNS chunk if present, and is used to check chunk 1806 | # ordering constraints. 1807 | self.trns = None 1808 | # Stores sbit chunk if present. 1809 | self.sbit = None 1810 | elif type == 'PLTE': 1811 | # http://www.w3.org/TR/PNG/#11PLTE 1812 | if self.plte: 1813 | warnings.warn("Multiple PLTE chunks present.") 1814 | self.plte = data 1815 | if len(data) % 3 != 0: 1816 | raise FormatError( 1817 | "PLTE chunk's length should be a multiple of 3.") 1818 | if len(data) > (2**self.bitdepth)*3: 1819 | raise FormatError("PLTE chunk is too long.") 1820 | if len(data) == 0: 1821 | raise FormatError("Empty PLTE is not allowed.") 1822 | elif type == 'bKGD': 1823 | try: 1824 | if self.colormap: 1825 | if not self.plte: 1826 | warnings.warn( 1827 | "PLTE chunk is required before bKGD chunk.") 1828 | self.background = struct.unpack('B', data) 1829 | else: 1830 | self.background = struct.unpack("!%dH" % self.color_planes, 1831 | data) 1832 | except struct.error: 1833 | raise FormatError("bKGD chunk has incorrect length.") 1834 | elif type == 'tRNS': 1835 | # http://www.w3.org/TR/PNG/#11tRNS 1836 | self.trns = data 1837 | if self.colormap: 1838 | if not self.plte: 1839 | warnings.warn("PLTE chunk is required before tRNS chunk.") 1840 | else: 1841 | if len(data) > len(self.plte)/3: 1842 | # Was warning, but promoted to Error as it 1843 | # would otherwise cause pain later on. 1844 | raise FormatError("tRNS chunk is too long.") 1845 | else: 1846 | if self.alpha: 1847 | raise FormatError( 1848 | "tRNS chunk is not valid with colour type %d." % 1849 | self.color_type) 1850 | try: 1851 | self.transparent = \ 1852 | struct.unpack("!%dH" % self.color_planes, data) 1853 | except struct.error: 1854 | raise FormatError("tRNS chunk has incorrect length.") 1855 | elif type == 'gAMA': 1856 | try: 1857 | self.gamma = struct.unpack("!L", data)[0] / 100000.0 1858 | except struct.error: 1859 | raise FormatError("gAMA chunk has incorrect length.") 1860 | elif type == 'sBIT': 1861 | self.sbit = data 1862 | if (self.colormap and len(data) != 3 or 1863 | not self.colormap and len(data) != self.planes): 1864 | raise FormatError("sBIT chunk has incorrect length.") 1865 | 1866 | def read(self, lenient=False): 1867 | """ 1868 | Read the PNG file and decode it. Returns (`width`, `height`, 1869 | `pixels`, `metadata`). 1870 | 1871 | May use excessive memory. 1872 | 1873 | `pixels` are returned in boxed row flat pixel format. 1874 | 1875 | If the optional `lenient` argument evaluates to True, 1876 | checksum failures will raise warnings rather than exceptions. 1877 | """ 1878 | 1879 | def iteridat(): 1880 | """Iterator that yields all the ``IDAT`` chunks as strings.""" 1881 | while True: 1882 | try: 1883 | type, data = self.chunk(lenient=lenient) 1884 | except ValueError, e: 1885 | raise ChunkError(e.args[0]) 1886 | if type == 'IEND': 1887 | # http://www.w3.org/TR/PNG/#11IEND 1888 | break 1889 | if type != 'IDAT': 1890 | continue 1891 | # type == 'IDAT' 1892 | # http://www.w3.org/TR/PNG/#11IDAT 1893 | if self.colormap and not self.plte: 1894 | warnings.warn("PLTE chunk is required before IDAT chunk") 1895 | yield data 1896 | 1897 | def iterdecomp(idat): 1898 | """Iterator that yields decompressed strings. `idat` should 1899 | be an iterator that yields the ``IDAT`` chunk data. 1900 | """ 1901 | 1902 | # Currently, with no max_length paramter to decompress, this 1903 | # routine will do one yield per IDAT chunk. So not very 1904 | # incremental. 1905 | d = zlib.decompressobj() 1906 | # Each IDAT chunk is passed to the decompressor, then any 1907 | # remaining state is decompressed out. 1908 | for data in idat: 1909 | # :todo: add a max_length argument here to limit output 1910 | # size. 1911 | yield array('B', d.decompress(data)) 1912 | yield array('B', d.flush()) 1913 | 1914 | self.preamble(lenient=lenient) 1915 | raw = iterdecomp(iteridat()) 1916 | 1917 | if self.interlace: 1918 | raw = array('B', itertools.chain(*raw)) 1919 | arraycode = 'BH'[self.bitdepth>8] 1920 | # Like :meth:`group` but producing an array.array object for 1921 | # each row. 1922 | pixels = itertools.imap(lambda *row: array(arraycode, row), 1923 | *[iter(self.deinterlace(raw))]*self.width*self.planes) 1924 | else: 1925 | pixels = self.iterboxed(self.iterstraight(raw)) 1926 | meta = dict() 1927 | for attr in 'greyscale alpha planes bitdepth interlace'.split(): 1928 | meta[attr] = getattr(self, attr) 1929 | meta['size'] = (self.width, self.height) 1930 | for attr in 'gamma transparent background'.split(): 1931 | a = getattr(self, attr, None) 1932 | if a is not None: 1933 | meta[attr] = a 1934 | if self.plte: 1935 | meta['palette'] = self.palette() 1936 | return self.width, self.height, pixels, meta 1937 | 1938 | 1939 | def read_flat(self): 1940 | """ 1941 | Read a PNG file and decode it into flat row flat pixel format. 1942 | Returns (*width*, *height*, *pixels*, *metadata*). 1943 | 1944 | May use excessive memory. 1945 | 1946 | `pixels` are returned in flat row flat pixel format. 1947 | 1948 | See also the :meth:`read` method which returns pixels in the 1949 | more stream-friendly boxed row flat pixel format. 1950 | """ 1951 | 1952 | x, y, pixel, meta = self.read() 1953 | arraycode = 'BH'[meta['bitdepth']>8] 1954 | pixel = array(arraycode, itertools.chain(*pixel)) 1955 | return x, y, pixel, meta 1956 | 1957 | def palette(self, alpha='natural'): 1958 | """Returns a palette that is a sequence of 3-tuples or 4-tuples, 1959 | synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These 1960 | chunks should have already been processed (for example, by 1961 | calling the :meth:`preamble` method). All the tuples are the 1962 | same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when 1963 | there is a ``tRNS`` chunk. Assumes that the image is colour type 1964 | 3 and therefore a ``PLTE`` chunk is required. 1965 | 1966 | If the `alpha` argument is ``'force'`` then an alpha channel is 1967 | always added, forcing the result to be a sequence of 4-tuples. 1968 | """ 1969 | 1970 | if not self.plte: 1971 | raise FormatError( 1972 | "Required PLTE chunk is missing in colour type 3 image.") 1973 | plte = group(array('B', self.plte), 3) 1974 | if self.trns or alpha == 'force': 1975 | trns = array('B', self.trns or '') 1976 | trns.extend([255]*(len(plte)-len(trns))) 1977 | plte = map(operator.add, plte, group(trns, 1)) 1978 | return plte 1979 | 1980 | def asDirect(self): 1981 | """Returns the image data as a direct representation of an 1982 | ``x * y * planes`` array. This method is intended to remove the 1983 | need for callers to deal with palettes and transparency 1984 | themselves. Images with a palette (colour type 3) 1985 | are converted to RGB or RGBA; images with transparency (a 1986 | ``tRNS`` chunk) are converted to LA or RGBA as appropriate. 1987 | When returned in this format the pixel values represent the 1988 | colour value directly without needing to refer to palettes or 1989 | transparency information. 1990 | 1991 | Like the :meth:`read` method this method returns a 4-tuple: 1992 | 1993 | (*width*, *height*, *pixels*, *meta*) 1994 | 1995 | This method normally returns pixel values with the bit depth 1996 | they have in the source image, but when the source PNG has an 1997 | ``sBIT`` chunk it is inspected and can reduce the bit depth of 1998 | the result pixels; pixel values will be reduced according to 1999 | the bit depth specified in the ``sBIT`` chunk (PNG nerds should 2000 | note a single result bit depth is used for all channels; the 2001 | maximum of the ones specified in the ``sBIT`` chunk. An RGB565 2002 | image will be rescaled to 6-bit RGB666). 2003 | 2004 | The *meta* dictionary that is returned reflects the `direct` 2005 | format and not the original source image. For example, an RGB 2006 | source image with a ``tRNS`` chunk to represent a transparent 2007 | colour, will have ``planes=3`` and ``alpha=False`` for the 2008 | source image, but the *meta* dictionary returned by this method 2009 | will have ``planes=4`` and ``alpha=True`` because an alpha 2010 | channel is synthesized and added. 2011 | 2012 | *pixels* is the pixel data in boxed row flat pixel format (just 2013 | like the :meth:`read` method). 2014 | 2015 | All the other aspects of the image data are not changed. 2016 | """ 2017 | 2018 | self.preamble() 2019 | 2020 | # Simple case, no conversion necessary. 2021 | if not self.colormap and not self.trns and not self.sbit: 2022 | return self.read() 2023 | 2024 | x,y,pixels,meta = self.read() 2025 | 2026 | if self.colormap: 2027 | meta['colormap'] = False 2028 | meta['alpha'] = bool(self.trns) 2029 | meta['bitdepth'] = 8 2030 | meta['planes'] = 3 + bool(self.trns) 2031 | plte = self.palette() 2032 | def iterpal(pixels): 2033 | for row in pixels: 2034 | row = map(plte.__getitem__, row) 2035 | yield array('B', itertools.chain(*row)) 2036 | pixels = iterpal(pixels) 2037 | elif self.trns: 2038 | # It would be nice if there was some reasonable way of doing 2039 | # this without generating a whole load of intermediate tuples. 2040 | # But tuples does seem like the easiest way, with no other way 2041 | # clearly much simpler or much faster. (Actually, the L to LA 2042 | # conversion could perhaps go faster (all those 1-tuples!), but 2043 | # I still wonder whether the code proliferation is worth it) 2044 | it = self.transparent 2045 | maxval = 2**meta['bitdepth']-1 2046 | planes = meta['planes'] 2047 | meta['alpha'] = True 2048 | meta['planes'] += 1 2049 | typecode = 'BH'[meta['bitdepth']>8] 2050 | def itertrns(pixels): 2051 | for row in pixels: 2052 | # For each row we group it into pixels, then form a 2053 | # characterisation vector that says whether each pixel 2054 | # is opaque or not. Then we convert True/False to 2055 | # 0/maxval (by multiplication), and add it as the extra 2056 | # channel. 2057 | row = group(row, planes) 2058 | opa = map(it.__ne__, row) 2059 | opa = map(maxval.__mul__, opa) 2060 | opa = zip(opa) # convert to 1-tuples 2061 | yield array(typecode, 2062 | itertools.chain(*map(operator.add, row, opa))) 2063 | pixels = itertrns(pixels) 2064 | targetbitdepth = None 2065 | if self.sbit: 2066 | sbit = struct.unpack('%dB' % len(self.sbit), self.sbit) 2067 | targetbitdepth = max(sbit) 2068 | if targetbitdepth > meta['bitdepth']: 2069 | raise Error('sBIT chunk %r exceeds bitdepth %d' % 2070 | (sbit,self.bitdepth)) 2071 | if min(sbit) <= 0: 2072 | raise Error('sBIT chunk %r has a 0-entry' % sbit) 2073 | if targetbitdepth == meta['bitdepth']: 2074 | targetbitdepth = None 2075 | if targetbitdepth: 2076 | shift = meta['bitdepth'] - targetbitdepth 2077 | meta['bitdepth'] = targetbitdepth 2078 | def itershift(pixels): 2079 | for row in pixels: 2080 | yield map(shift.__rrshift__, row) 2081 | pixels = itershift(pixels) 2082 | return x,y,pixels,meta 2083 | 2084 | def asFloat(self, maxval=1.0): 2085 | """Return image pixels as per :meth:`asDirect` method, but scale 2086 | all pixel values to be floating point values between 0.0 and 2087 | *maxval*. 2088 | """ 2089 | 2090 | x,y,pixels,info = self.asDirect() 2091 | sourcemaxval = 2**info['bitdepth']-1 2092 | del info['bitdepth'] 2093 | info['maxval'] = float(maxval) 2094 | factor = float(maxval)/float(sourcemaxval) 2095 | def iterfloat(): 2096 | for row in pixels: 2097 | yield map(factor.__mul__, row) 2098 | return x,y,iterfloat(),info 2099 | 2100 | def _as_rescale(self, get, targetbitdepth): 2101 | """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" 2102 | 2103 | width,height,pixels,meta = get() 2104 | maxval = 2**meta['bitdepth'] - 1 2105 | targetmaxval = 2**targetbitdepth - 1 2106 | factor = float(targetmaxval) / float(maxval) 2107 | meta['bitdepth'] = targetbitdepth 2108 | def iterscale(): 2109 | for row in pixels: 2110 | yield map(lambda x: int(round(x*factor)), row) 2111 | if maxval == targetmaxval: 2112 | return width, height, pixels, meta 2113 | else: 2114 | return width, height, iterscale(), meta 2115 | 2116 | def asRGB8(self): 2117 | """Return the image data as an RGB pixels with 8-bits per 2118 | sample. This is like the :meth:`asRGB` method except that 2119 | this method additionally rescales the values so that they 2120 | are all between 0 and 255 (8-bit). In the case where the 2121 | source image has a bit depth < 8 the transformation preserves 2122 | all the information; where the source image has bit depth 2123 | > 8, then rescaling to 8-bit values loses precision. No 2124 | dithering is performed. Like :meth:`asRGB`, an alpha channel 2125 | in the source image will raise an exception. 2126 | 2127 | This function returns a 4-tuple: 2128 | (*width*, *height*, *pixels*, *metadata*). 2129 | *width*, *height*, *metadata* are as per the :meth:`read` method. 2130 | 2131 | *pixels* is the pixel data in boxed row flat pixel format. 2132 | """ 2133 | 2134 | return self._as_rescale(self.asRGB, 8) 2135 | 2136 | def asRGBA8(self): 2137 | """Return the image data as RGBA pixels with 8-bits per 2138 | sample. This method is similar to :meth:`asRGB8` and 2139 | :meth:`asRGBA`: The result pixels have an alpha channel, *and* 2140 | values are rescaled to the range 0 to 255. The alpha channel is 2141 | synthesized if necessary (with a small speed penalty). 2142 | """ 2143 | 2144 | return self._as_rescale(self.asRGBA, 8) 2145 | 2146 | def asRGB(self): 2147 | """Return image as RGB pixels. RGB colour images are passed 2148 | through unchanged; greyscales are expanded into RGB 2149 | triplets (there is a small speed overhead for doing this). 2150 | 2151 | An alpha channel in the source image will raise an 2152 | exception. 2153 | 2154 | The return values are as for the :meth:`read` method 2155 | except that the *metadata* reflect the returned pixels, not the 2156 | source image. In particular, for this method 2157 | ``metadata['greyscale']`` will be ``False``. 2158 | """ 2159 | 2160 | width,height,pixels,meta = self.asDirect() 2161 | if meta['alpha']: 2162 | raise Error("will not convert image with alpha channel to RGB") 2163 | if not meta['greyscale']: 2164 | return width,height,pixels,meta 2165 | meta['greyscale'] = False 2166 | typecode = 'BH'[meta['bitdepth'] > 8] 2167 | def iterrgb(): 2168 | for row in pixels: 2169 | a = array(typecode, [0]) * 3 * width 2170 | for i in range(3): 2171 | a[i::3] = row 2172 | yield a 2173 | return width,height,iterrgb(),meta 2174 | 2175 | def asRGBA(self): 2176 | """Return image as RGBA pixels. Greyscales are expanded into 2177 | RGB triplets; an alpha channel is synthesized if necessary. 2178 | The return values are as for the :meth:`read` method 2179 | except that the *metadata* reflect the returned pixels, not the 2180 | source image. In particular, for this method 2181 | ``metadata['greyscale']`` will be ``False``, and 2182 | ``metadata['alpha']`` will be ``True``. 2183 | """ 2184 | 2185 | width,height,pixels,meta = self.asDirect() 2186 | if meta['alpha'] and not meta['greyscale']: 2187 | return width,height,pixels,meta 2188 | typecode = 'BH'[meta['bitdepth'] > 8] 2189 | maxval = 2**meta['bitdepth'] - 1 2190 | maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width 2191 | def newarray(): 2192 | return array(typecode, maxbuffer) 2193 | 2194 | if meta['alpha'] and meta['greyscale']: 2195 | # LA to RGBA 2196 | def convert(): 2197 | for row in pixels: 2198 | # Create a fresh target row, then copy L channel 2199 | # into first three target channels, and A channel 2200 | # into fourth channel. 2201 | a = newarray() 2202 | pngfilters.convert_la_to_rgba(row, a) 2203 | yield a 2204 | elif meta['greyscale']: 2205 | # L to RGBA 2206 | def convert(): 2207 | for row in pixels: 2208 | a = newarray() 2209 | pngfilters.convert_l_to_rgba(row, a) 2210 | yield a 2211 | else: 2212 | assert not meta['alpha'] and not meta['greyscale'] 2213 | # RGB to RGBA 2214 | def convert(): 2215 | for row in pixels: 2216 | a = newarray() 2217 | pngfilters.convert_rgb_to_rgba(row, a) 2218 | yield a 2219 | meta['alpha'] = True 2220 | meta['greyscale'] = False 2221 | return width,height,convert(),meta 2222 | 2223 | 2224 | # === Legacy Version Support === 2225 | 2226 | # :pyver:old: PyPNG works on Python versions 2.3 and 2.2, but not 2227 | # without some awkward problems. Really PyPNG works on Python 2.4 (and 2228 | # above); it works on Pythons 2.3 and 2.2 by virtue of fixing up 2229 | # problems here. It's a bit ugly (which is why it's hidden down here). 2230 | # 2231 | # Generally the strategy is one of pretending that we're running on 2232 | # Python 2.4 (or above), and patching up the library support on earlier 2233 | # versions so that it looks enough like Python 2.4. When it comes to 2234 | # Python 2.2 there is one thing we cannot patch: extended slices 2235 | # http://www.python.org/doc/2.3/whatsnew/section-slices.html. 2236 | # Instead we simply declare that features that are implemented using 2237 | # extended slices will not work on Python 2.2. 2238 | # 2239 | # In order to work on Python 2.3 we fix up a recurring annoyance involving 2240 | # the array type. In Python 2.3 an array cannot be initialised with an 2241 | # array, and it cannot be extended with a list (or other sequence). 2242 | # Both of those are repeated issues in the code. Whilst I would not 2243 | # normally tolerate this sort of behaviour, here we "shim" a replacement 2244 | # for array into place (and hope no-ones notices). You never read this. 2245 | # 2246 | # In an amusing case of warty hacks on top of warty hacks... the array 2247 | # shimming we try and do only works on Python 2.3 and above (you can't 2248 | # subclass array.array in Python 2.2). So to get it working on Python 2249 | # 2.2 we go for something much simpler and (probably) way slower. 2250 | try: 2251 | array('B').extend([]) 2252 | array('B', array('B')) 2253 | except: 2254 | # Expect to get here on Python 2.3 2255 | try: 2256 | class _array_shim(array): 2257 | true_array = array 2258 | def __new__(cls, typecode, init=None): 2259 | super_new = super(_array_shim, cls).__new__ 2260 | it = super_new(cls, typecode) 2261 | if init is None: 2262 | return it 2263 | it.extend(init) 2264 | return it 2265 | def extend(self, extension): 2266 | super_extend = super(_array_shim, self).extend 2267 | if isinstance(extension, self.true_array): 2268 | return super_extend(extension) 2269 | if not isinstance(extension, (list, str)): 2270 | # Convert to list. Allows iterators to work. 2271 | extension = list(extension) 2272 | return super_extend(self.true_array(self.typecode, extension)) 2273 | array = _array_shim 2274 | except: 2275 | # Expect to get here on Python 2.2 2276 | def array(typecode, init=()): 2277 | if type(init) == str: 2278 | return map(ord, init) 2279 | return list(init) 2280 | 2281 | # Further hacks to get it limping along on Python 2.2 2282 | try: 2283 | enumerate 2284 | except: 2285 | def enumerate(seq): 2286 | i=0 2287 | for x in seq: 2288 | yield i,x 2289 | i += 1 2290 | 2291 | try: 2292 | reversed 2293 | except: 2294 | def reversed(l): 2295 | l = list(l) 2296 | l.reverse() 2297 | for x in l: 2298 | yield x 2299 | 2300 | try: 2301 | itertools 2302 | except: 2303 | class _dummy_itertools: 2304 | pass 2305 | itertools = _dummy_itertools() 2306 | def _itertools_imap(f, seq): 2307 | for x in seq: 2308 | yield f(x) 2309 | itertools.imap = _itertools_imap 2310 | def _itertools_chain(*iterables): 2311 | for it in iterables: 2312 | for element in it: 2313 | yield element 2314 | itertools.chain = _itertools_chain 2315 | 2316 | 2317 | # === Support for users without Cython === 2318 | 2319 | try: 2320 | pngfilters 2321 | except: 2322 | class pngfilters(object): 2323 | def undo_filter_sub(filter_unit, scanline, previous, result): 2324 | """Undo sub filter.""" 2325 | 2326 | ai = 0 2327 | # Loops starts at index fu. Observe that the initial part 2328 | # of the result is already filled in correctly with 2329 | # scanline. 2330 | for i in range(filter_unit, len(result)): 2331 | x = scanline[i] 2332 | a = result[ai] 2333 | result[i] = (x + a) & 0xff 2334 | ai += 1 2335 | undo_filter_sub = staticmethod(undo_filter_sub) 2336 | 2337 | def undo_filter_up(filter_unit, scanline, previous, result): 2338 | """Undo up filter.""" 2339 | 2340 | for i in range(len(result)): 2341 | x = scanline[i] 2342 | b = previous[i] 2343 | result[i] = (x + b) & 0xff 2344 | undo_filter_up = staticmethod(undo_filter_up) 2345 | 2346 | def undo_filter_average(filter_unit, scanline, previous, result): 2347 | """Undo up filter.""" 2348 | 2349 | ai = -filter_unit 2350 | for i in range(len(result)): 2351 | x = scanline[i] 2352 | if ai < 0: 2353 | a = 0 2354 | else: 2355 | a = result[ai] 2356 | b = previous[i] 2357 | result[i] = (x + ((a + b) >> 1)) & 0xff 2358 | ai += 1 2359 | undo_filter_average = staticmethod(undo_filter_average) 2360 | 2361 | def undo_filter_paeth(filter_unit, scanline, previous, result): 2362 | """Undo Paeth filter.""" 2363 | 2364 | # Also used for ci. 2365 | ai = -filter_unit 2366 | for i in range(len(result)): 2367 | x = scanline[i] 2368 | if ai < 0: 2369 | a = c = 0 2370 | else: 2371 | a = result[ai] 2372 | c = previous[ai] 2373 | b = previous[i] 2374 | p = a + b - c 2375 | pa = abs(p - a) 2376 | pb = abs(p - b) 2377 | pc = abs(p - c) 2378 | if pa <= pb and pa <= pc: 2379 | pr = a 2380 | elif pb <= pc: 2381 | pr = b 2382 | else: 2383 | pr = c 2384 | result[i] = (x + pr) & 0xff 2385 | ai += 1 2386 | undo_filter_paeth = staticmethod(undo_filter_paeth) 2387 | 2388 | def convert_la_to_rgba(row, result): 2389 | for i in range(3): 2390 | result[i::4] = row[0::2] 2391 | result[3::4] = row[1::2] 2392 | convert_la_to_rgba = staticmethod(convert_la_to_rgba) 2393 | 2394 | def convert_l_to_rgba(row, result): 2395 | """Convert a grayscale image to RGBA. This method assumes the alpha 2396 | channel in result is already correctly initialized.""" 2397 | for i in range(3): 2398 | result[i::4] = row 2399 | convert_l_to_rgba = staticmethod(convert_l_to_rgba) 2400 | 2401 | def convert_rgb_to_rgba(row, result): 2402 | """Convert an RGB image to RGBA. This method assumes the alpha 2403 | channel in result is already correctly initialized.""" 2404 | for i in range(3): 2405 | result[i::4] = row[i::3] 2406 | convert_rgb_to_rgba = staticmethod(convert_rgb_to_rgba) 2407 | 2408 | 2409 | # === Internal Test Support === 2410 | 2411 | # This section comprises the tests that are internally validated (as 2412 | # opposed to tests which produce output files that are externally 2413 | # validated). Primarily they are unittests. 2414 | 2415 | # Note that it is difficult to internally validate the results of 2416 | # writing a PNG file. The only thing we can do is read it back in 2417 | # again, which merely checks consistency, not that the PNG file we 2418 | # produce is valid. 2419 | 2420 | # Run the tests from the command line: 2421 | # python -c 'import png;png.test()' 2422 | 2423 | # (For an in-memory binary file IO object) We use BytesIO where 2424 | # available, otherwise we use StringIO, but name it BytesIO. 2425 | try: 2426 | from io import BytesIO 2427 | except: 2428 | from StringIO import StringIO as BytesIO 2429 | import tempfile 2430 | # http://www.python.org/doc/2.4.4/lib/module-unittest.html 2431 | import unittest 2432 | 2433 | 2434 | def test(): 2435 | unittest.main(__name__) 2436 | 2437 | def topngbytes(name, rows, x, y, **k): 2438 | """Convenience function for creating a PNG file "in memory" as a 2439 | string. Creates a :class:`Writer` instance using the keyword arguments, 2440 | then passes `rows` to its :meth:`Writer.write` method. The resulting 2441 | PNG file is returned as a string. `name` is used to identify the file for 2442 | debugging. 2443 | """ 2444 | 2445 | import os 2446 | 2447 | print name 2448 | f = BytesIO() 2449 | w = Writer(x, y, **k) 2450 | w.write(f, rows) 2451 | if os.environ.get('PYPNG_TEST_TMP'): 2452 | w = open(name, 'wb') 2453 | w.write(f.getvalue()) 2454 | w.close() 2455 | return f.getvalue() 2456 | 2457 | def testWithIO(inp, out, f): 2458 | """Calls the function `f` with ``sys.stdin`` changed to `inp` 2459 | and ``sys.stdout`` changed to `out`. They are restored when `f` 2460 | returns. This function returns whatever `f` returns. 2461 | """ 2462 | 2463 | import os 2464 | 2465 | try: 2466 | oldin,sys.stdin = sys.stdin,inp 2467 | oldout,sys.stdout = sys.stdout,out 2468 | x = f() 2469 | finally: 2470 | sys.stdin = oldin 2471 | sys.stdout = oldout 2472 | if os.environ.get('PYPNG_TEST_TMP') and hasattr(out,'getvalue'): 2473 | name = mycallersname() 2474 | if name: 2475 | w = open(name+'.png', 'wb') 2476 | w.write(out.getvalue()) 2477 | w.close() 2478 | return x 2479 | 2480 | def mycallersname(): 2481 | """Returns the name of the caller of the caller of this function 2482 | (hence the name of the caller of the function in which 2483 | "mycallersname()" textually appears). Returns None if this cannot 2484 | be determined.""" 2485 | 2486 | # http://docs.python.org/library/inspect.html#the-interpreter-stack 2487 | import inspect 2488 | 2489 | frame = inspect.currentframe() 2490 | if not frame: 2491 | return None 2492 | frame_,filename_,lineno_,funname,linelist_,listi_ = ( 2493 | inspect.getouterframes(frame)[2]) 2494 | return funname 2495 | 2496 | def seqtobytes(s): 2497 | """Convert a sequence of integers to a *bytes* instance. Good for 2498 | plastering over Python 2 / Python 3 cracks. 2499 | """ 2500 | 2501 | return strtobytes(''.join(chr(x) for x in s)) 2502 | 2503 | class Test(unittest.TestCase): 2504 | # This member is used by the superclass. If we don't define a new 2505 | # class here then when we use self.assertRaises() and the PyPNG code 2506 | # raises an assertion then we get no proper traceback. I can't work 2507 | # out why, but defining a new class here means we get a proper 2508 | # traceback. 2509 | class failureException(Exception): 2510 | pass 2511 | 2512 | def helperLN(self, n): 2513 | mask = (1 << n) - 1 2514 | # Use small chunk_limit so that multiple chunk writing is 2515 | # tested. Making it a test for Issue 20. 2516 | w = Writer(15, 17, greyscale=True, bitdepth=n, chunk_limit=99) 2517 | f = BytesIO() 2518 | w.write_array(f, array('B', map(mask.__and__, range(1, 256)))) 2519 | r = Reader(bytes=f.getvalue()) 2520 | x,y,pixels,meta = r.read() 2521 | self.assertEqual(x, 15) 2522 | self.assertEqual(y, 17) 2523 | self.assertEqual(list(itertools.chain(*pixels)), 2524 | map(mask.__and__, range(1,256))) 2525 | def testL8(self): 2526 | return self.helperLN(8) 2527 | def testL4(self): 2528 | return self.helperLN(4) 2529 | def testL2(self): 2530 | "Also tests asRGB8." 2531 | w = Writer(1, 4, greyscale=True, bitdepth=2) 2532 | f = BytesIO() 2533 | w.write_array(f, array('B', range(4))) 2534 | r = Reader(bytes=f.getvalue()) 2535 | x,y,pixels,meta = r.asRGB8() 2536 | self.assertEqual(x, 1) 2537 | self.assertEqual(y, 4) 2538 | for i,row in enumerate(pixels): 2539 | self.assertEqual(len(row), 3) 2540 | self.assertEqual(list(row), [0x55*i]*3) 2541 | def testP2(self): 2542 | "2-bit palette." 2543 | a = (255,255,255) 2544 | b = (200,120,120) 2545 | c = (50,99,50) 2546 | w = Writer(1, 4, bitdepth=2, palette=[a,b,c]) 2547 | f = BytesIO() 2548 | w.write_array(f, array('B', (0,1,1,2))) 2549 | r = Reader(bytes=f.getvalue()) 2550 | x,y,pixels,meta = r.asRGB8() 2551 | self.assertEqual(x, 1) 2552 | self.assertEqual(y, 4) 2553 | self.assertEqual(map(list, pixels), map(list, [a, b, b, c])) 2554 | def testPtrns(self): 2555 | "Test colour type 3 and tRNS chunk (and 4-bit palette)." 2556 | a = (50,99,50,50) 2557 | b = (200,120,120,80) 2558 | c = (255,255,255) 2559 | d = (200,120,120) 2560 | e = (50,99,50) 2561 | w = Writer(3, 3, bitdepth=4, palette=[a,b,c,d,e]) 2562 | f = BytesIO() 2563 | w.write_array(f, array('B', (4, 3, 2, 3, 2, 0, 2, 0, 1))) 2564 | r = Reader(bytes=f.getvalue()) 2565 | x,y,pixels,meta = r.asRGBA8() 2566 | self.assertEqual(x, 3) 2567 | self.assertEqual(y, 3) 2568 | c = c+(255,) 2569 | d = d+(255,) 2570 | e = e+(255,) 2571 | boxed = [(e,d,c),(d,c,a),(c,a,b)] 2572 | flat = map(lambda row: itertools.chain(*row), boxed) 2573 | self.assertEqual(map(list, pixels), map(list, flat)) 2574 | def testRGBtoRGBA(self): 2575 | "asRGBA8() on colour type 2 source.""" 2576 | # Test for Issue 26 2577 | r = Reader(bytes=_pngsuite['basn2c08']) 2578 | x,y,pixels,meta = r.asRGBA8() 2579 | # Test the pixels at row 9 columns 0 and 1. 2580 | row9 = list(pixels)[9] 2581 | self.assertEqual(list(row9[0:8]), 2582 | [0xff, 0xdf, 0xff, 0xff, 0xff, 0xde, 0xff, 0xff]) 2583 | def testLtoRGBA(self): 2584 | "asRGBA() on grey source.""" 2585 | # Test for Issue 60 2586 | r = Reader(bytes=_pngsuite['basi0g08']) 2587 | x,y,pixels,meta = r.asRGBA() 2588 | row9 = list(list(pixels)[9]) 2589 | self.assertEqual(row9[0:8], 2590 | [222, 222, 222, 255, 221, 221, 221, 255]) 2591 | def testCtrns(self): 2592 | "Test colour type 2 and tRNS chunk." 2593 | # Test for Issue 25 2594 | r = Reader(bytes=_pngsuite['tbrn2c08']) 2595 | x,y,pixels,meta = r.asRGBA8() 2596 | # I just happen to know that the first pixel is transparent. 2597 | # In particular it should be #7f7f7f00 2598 | row0 = list(pixels)[0] 2599 | self.assertEqual(tuple(row0[0:4]), (0x7f, 0x7f, 0x7f, 0x00)) 2600 | def testAdam7read(self): 2601 | """Adam7 interlace reading. 2602 | Specifically, test that for images in the PngSuite that 2603 | have both an interlaced and straightlaced pair that both 2604 | images from the pair produce the same array of pixels.""" 2605 | for candidate in _pngsuite: 2606 | if not candidate.startswith('basn'): 2607 | continue 2608 | candi = candidate.replace('n', 'i') 2609 | if candi not in _pngsuite: 2610 | continue 2611 | print 'adam7 read', candidate 2612 | straight = Reader(bytes=_pngsuite[candidate]) 2613 | adam7 = Reader(bytes=_pngsuite[candi]) 2614 | # Just compare the pixels. Ignore x,y (because they're 2615 | # likely to be correct?); metadata is ignored because the 2616 | # "interlace" member differs. Lame. 2617 | straight = straight.read()[2] 2618 | adam7 = adam7.read()[2] 2619 | self.assertEqual(map(list, straight), map(list, adam7)) 2620 | def testAdam7write(self): 2621 | """Adam7 interlace writing. 2622 | For each test image in the PngSuite, write an interlaced 2623 | and a straightlaced version. Decode both, and compare results. 2624 | """ 2625 | # Not such a great test, because the only way we can check what 2626 | # we have written is to read it back again. 2627 | 2628 | for name,bytes in _pngsuite.items(): 2629 | # Only certain colour types supported for this test. 2630 | if name[3:5] not in ['n0', 'n2', 'n4', 'n6']: 2631 | continue 2632 | it = Reader(bytes=bytes) 2633 | x,y,pixels,meta = it.read() 2634 | pngi = topngbytes('adam7wn'+name+'.png', pixels, 2635 | x=x, y=y, bitdepth=it.bitdepth, 2636 | greyscale=it.greyscale, alpha=it.alpha, 2637 | transparent=it.transparent, 2638 | interlace=False) 2639 | x,y,ps,meta = Reader(bytes=pngi).read() 2640 | it = Reader(bytes=bytes) 2641 | x,y,pixels,meta = it.read() 2642 | pngs = topngbytes('adam7wi'+name+'.png', pixels, 2643 | x=x, y=y, bitdepth=it.bitdepth, 2644 | greyscale=it.greyscale, alpha=it.alpha, 2645 | transparent=it.transparent, 2646 | interlace=True) 2647 | x,y,pi,meta = Reader(bytes=pngs).read() 2648 | self.assertEqual(map(list, ps), map(list, pi)) 2649 | def testPGMin(self): 2650 | """Test that the command line tool can read PGM files.""" 2651 | def do(): 2652 | return _main(['testPGMin']) 2653 | s = BytesIO() 2654 | s.write(strtobytes('P5 2 2 3\n')) 2655 | s.write(strtobytes('\x00\x01\x02\x03')) 2656 | s.flush() 2657 | s.seek(0) 2658 | o = BytesIO() 2659 | testWithIO(s, o, do) 2660 | r = Reader(bytes=o.getvalue()) 2661 | x,y,pixels,meta = r.read() 2662 | self.assertTrue(r.greyscale) 2663 | self.assertEqual(r.bitdepth, 2) 2664 | def testPAMin(self): 2665 | """Test that the command line tool can read PAM file.""" 2666 | def do(): 2667 | return _main(['testPAMin']) 2668 | s = BytesIO() 2669 | s.write(strtobytes('P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n' 2670 | 'TUPLTYPE RGB_ALPHA\nENDHDR\n')) 2671 | # The pixels in flat row flat pixel format 2672 | flat = [255,0,0,255, 0,255,0,120, 0,0,255,30] 2673 | asbytes = seqtobytes(flat) 2674 | s.write(asbytes) 2675 | s.flush() 2676 | s.seek(0) 2677 | o = BytesIO() 2678 | testWithIO(s, o, do) 2679 | r = Reader(bytes=o.getvalue()) 2680 | x,y,pixels,meta = r.read() 2681 | self.assertTrue(r.alpha) 2682 | self.assertTrue(not r.greyscale) 2683 | self.assertEqual(list(itertools.chain(*pixels)), flat) 2684 | def testLA4(self): 2685 | """Create an LA image with bitdepth 4.""" 2686 | bytes = topngbytes('la4.png', [[5, 12]], 1, 1, 2687 | greyscale=True, alpha=True, bitdepth=4) 2688 | sbit = Reader(bytes=bytes).chunk('sBIT')[1] 2689 | self.assertEqual(sbit, strtobytes('\x04\x04')) 2690 | def testPal(self): 2691 | """Test that a palette PNG returns the palette in info.""" 2692 | r = Reader(bytes=_pngsuite['basn3p04']) 2693 | x,y,pixels,info = r.read() 2694 | self.assertEqual(x, 32) 2695 | self.assertEqual(y, 32) 2696 | self.assertTrue('palette' in info) 2697 | def testPalWrite(self): 2698 | """Test metadata for paletted PNG can be passed from one PNG 2699 | to another.""" 2700 | r = Reader(bytes=_pngsuite['basn3p04']) 2701 | x,y,pixels,info = r.read() 2702 | w = Writer(**info) 2703 | o = BytesIO() 2704 | w.write(o, pixels) 2705 | o.flush() 2706 | o.seek(0) 2707 | r = Reader(file=o) 2708 | _,_,_,again_info = r.read() 2709 | # Same palette 2710 | self.assertEqual(again_info['palette'], info['palette']) 2711 | def testPalExpand(self): 2712 | """Test that bitdepth can be used to fiddle with pallete image.""" 2713 | r = Reader(bytes=_pngsuite['basn3p04']) 2714 | x,y,pixels,info = r.read() 2715 | pixels = [list(row) for row in pixels] 2716 | info['bitdepth'] = 8 2717 | w = Writer(**info) 2718 | o = BytesIO() 2719 | w.write(o, pixels) 2720 | o.flush() 2721 | o.seek(0) 2722 | r = Reader(file=o) 2723 | _,_,again_pixels,again_info = r.read() 2724 | # Same pixels 2725 | again_pixels = [list(row) for row in again_pixels] 2726 | self.assertEqual(again_pixels, pixels) 2727 | 2728 | def testPNMsbit(self): 2729 | """Test that PNM files can generates sBIT chunk.""" 2730 | def do(): 2731 | return _main(['testPNMsbit']) 2732 | s = BytesIO() 2733 | s.write(strtobytes('P6 8 1 1\n')) 2734 | for pixel in range(8): 2735 | s.write(struct.pack('>sys.stderr, "skipping numpy test" 2886 | return 2887 | 2888 | rows = [map(numpy.uint16, range(0,0x10000,0x5555))] 2889 | b = topngbytes('numpyuint16.png', rows, 4, 1, 2890 | greyscale=True, alpha=False, bitdepth=16) 2891 | def testNumpyuint8(self): 2892 | """numpy uint8.""" 2893 | 2894 | try: 2895 | import numpy 2896 | except ImportError: 2897 | print >>sys.stderr, "skipping numpy test" 2898 | return 2899 | 2900 | rows = [map(numpy.uint8, range(0,0x100,0x55))] 2901 | b = topngbytes('numpyuint8.png', rows, 4, 1, 2902 | greyscale=True, alpha=False, bitdepth=8) 2903 | def testNumpybool(self): 2904 | """numpy bool.""" 2905 | 2906 | try: 2907 | import numpy 2908 | except ImportError: 2909 | print >>sys.stderr, "skipping numpy test" 2910 | return 2911 | 2912 | rows = [map(numpy.bool, [0,1])] 2913 | b = topngbytes('numpybool.png', rows, 2, 1, 2914 | greyscale=True, alpha=False, bitdepth=1) 2915 | def testNumpyarray(self): 2916 | """numpy array.""" 2917 | try: 2918 | import numpy 2919 | except ImportError: 2920 | print >>sys.stderr, "skipping numpy test" 2921 | return 2922 | 2923 | pixels = numpy.array([[0,0x5555],[0x5555,0xaaaa]], numpy.uint16) 2924 | img = from_array(pixels, 'L') 2925 | img.save('testnumpyL16.png') 2926 | 2927 | def paeth(self, x, a, b, c): 2928 | p = a + b - c 2929 | pa = abs(p - a) 2930 | pb = abs(p - b) 2931 | pc = abs(p - c) 2932 | if pa <= pb and pa <= pc: 2933 | pr = a 2934 | elif pb <= pc: 2935 | pr = b 2936 | else: 2937 | pr = c 2938 | return x - pr 2939 | 2940 | # test filters and unfilters 2941 | def testFilterScanlineFirstLine(self): 2942 | fo = 3 # bytes per pixel 2943 | line = [30, 31, 32, 230, 231, 232] 2944 | out = filter_scanline(0, line, fo, None) # none 2945 | self.assertEqual(list(out), [0, 30, 31, 32, 230, 231, 232]) 2946 | out = filter_scanline(1, line, fo, None) # sub 2947 | self.assertEqual(list(out), [1, 30, 31, 32, 200, 200, 200]) 2948 | out = filter_scanline(2, line, fo, None) # up 2949 | # TODO: All filtered scanlines start with a byte indicating the filter 2950 | # algorithm, except "up". Is this a bug? Should the expected output 2951 | # start with 2 here? 2952 | self.assertEqual(list(out), [30, 31, 32, 230, 231, 232]) 2953 | out = filter_scanline(3, line, fo, None) # average 2954 | self.assertEqual(list(out), [3, 30, 31, 32, 215, 216, 216]) 2955 | out = filter_scanline(4, line, fo, None) # paeth 2956 | self.assertEqual(list(out), [ 2957 | 4, self.paeth(30, 0, 0, 0), self.paeth(31, 0, 0, 0), 2958 | self.paeth(32, 0, 0, 0), self.paeth(230, 30, 0, 0), 2959 | self.paeth(231, 31, 0, 0), self.paeth(232, 32, 0, 0) 2960 | ]) 2961 | def testFilterScanline(self): 2962 | prev = [20, 21, 22, 210, 211, 212] 2963 | line = [30, 32, 34, 230, 233, 236] 2964 | fo = 3 2965 | out = filter_scanline(0, line, fo, prev) # none 2966 | self.assertEqual(list(out), [0, 30, 32, 34, 230, 233, 236]) 2967 | out = filter_scanline(1, line, fo, prev) # sub 2968 | self.assertEqual(list(out), [1, 30, 32, 34, 200, 201, 202]) 2969 | out = filter_scanline(2, line, fo, prev) # up 2970 | self.assertEqual(list(out), [2, 10, 11, 12, 20, 22, 24]) 2971 | out = filter_scanline(3, line, fo, prev) # average 2972 | self.assertEqual(list(out), [3, 20, 22, 23, 110, 112, 113]) 2973 | out = filter_scanline(4, line, fo, prev) # paeth 2974 | self.assertEqual(list(out), [ 2975 | 4, self.paeth(30, 0, 20, 0), self.paeth(32, 0, 21, 0), 2976 | self.paeth(34, 0, 22, 0), self.paeth(230, 30, 210, 20), 2977 | self.paeth(233, 32, 211, 21), self.paeth(236, 34, 212, 22) 2978 | ]) 2979 | def testUnfilterScanline(self): 2980 | reader = Reader(bytes='') 2981 | reader.psize = 3 2982 | scanprev = array('B', [20, 21, 22, 210, 211, 212]) 2983 | scanline = array('B', [30, 32, 34, 230, 233, 236]) 2984 | def cp(a): 2985 | return array('B', a) 2986 | 2987 | out = reader.undo_filter(0, cp(scanline), cp(scanprev)) 2988 | self.assertEqual(list(out), list(scanline)) # none 2989 | out = reader.undo_filter(1, cp(scanline), cp(scanprev)) 2990 | self.assertEqual(list(out), [30, 32, 34, 4, 9, 14]) # sub 2991 | out = reader.undo_filter(2, cp(scanline), cp(scanprev)) 2992 | self.assertEqual(list(out), [50, 53, 56, 184, 188, 192]) # up 2993 | out = reader.undo_filter(3, cp(scanline), cp(scanprev)) 2994 | self.assertEqual(list(out), [40, 42, 45, 99, 103, 108]) # average 2995 | out = reader.undo_filter(4, cp(scanline), cp(scanprev)) 2996 | self.assertEqual(list(out), [50, 53, 56, 184, 188, 192]) # paeth 2997 | def testUnfilterScanlinePaeth(self): 2998 | # This tests more edge cases in the paeth unfilter 2999 | reader = Reader(bytes='') 3000 | reader.psize = 3 3001 | scanprev = array('B', [2, 0, 0, 0, 9, 11]) 3002 | scanline = array('B', [6, 10, 9, 100, 101, 102]) 3003 | 3004 | out = reader.undo_filter(4, scanline, scanprev) 3005 | self.assertEqual(list(out), [8, 10, 9, 108, 111, 113]) # paeth 3006 | def testIterstraight(self): 3007 | def arraify(list_of_str): 3008 | return [array('B', s) for s in list_of_str] 3009 | reader = Reader(bytes='') 3010 | reader.row_bytes = 6 3011 | reader.psize = 3 3012 | rows = reader.iterstraight(arraify(['\x00abcdef', '\x00ghijkl'])) 3013 | self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) 3014 | 3015 | rows = reader.iterstraight(arraify(['\x00abc', 'def\x00ghijkl'])) 3016 | self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) 3017 | 3018 | rows = reader.iterstraight(arraify(['\x00abcdef\x00ghijkl'])) 3019 | self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) 3020 | 3021 | rows = reader.iterstraight(arraify(['\x00abcdef\x00ghi', 'jkl'])) 3022 | self.assertEqual(list(rows), arraify(['abcdef', 'ghijkl'])) 3023 | 3024 | # === Command Line Support === 3025 | 3026 | def _dehex(s): 3027 | """Liberally convert from hex string to binary string.""" 3028 | import re 3029 | import binascii 3030 | 3031 | # Remove all non-hexadecimal digits 3032 | s = re.sub(r'[^a-fA-F\d]', '', s) 3033 | # binscii.unhexlify works in Python 2 and Python 3 (unlike 3034 | # thing.decode('hex')). 3035 | return binascii.unhexlify(strtobytes(s)) 3036 | def _enhex(s): 3037 | """Convert from binary string (bytes) to hex string (str).""" 3038 | 3039 | import binascii 3040 | 3041 | return bytestostr(binascii.hexlify(s)) 3042 | 3043 | # Copies of PngSuite test files taken 3044 | # from http://www.schaik.com/pngsuite/pngsuite_bas_png.html 3045 | # on 2009-02-19 by drj and converted to hex. 3046 | # Some of these are not actually in PngSuite (but maybe they should 3047 | # be?), they use the same naming scheme, but start with a capital 3048 | # letter. 3049 | _pngsuite = { 3050 | 'basi0g01': _dehex(""" 3051 | 89504e470d0a1a0a0000000d49484452000000200000002001000000012c0677 3052 | cf0000000467414d41000186a031e8965f0000009049444154789c2d8d310ec2 3053 | 300c45dfc682c415187a00a42e197ab81e83b127e00c5639001363a580d8582c 3054 | 65c910357c4b78b0bfbfdf4f70168c19e7acb970a3f2d1ded9695ce5bf5963df 3055 | d92aaf4c9fd927ea449e6487df5b9c36e799b91bdf082b4d4bd4014fe4014b01 3056 | ab7a17aee694d28d328a2d63837a70451e1648702d9a9ff4a11d2f7a51aa21e5 3057 | a18c7ffd0094e3511d661822f20000000049454e44ae426082 3058 | """), 3059 | 'basi0g02': _dehex(""" 3060 | 89504e470d0a1a0a0000000d49484452000000200000002002000000016ba60d 3061 | 1f0000000467414d41000186a031e8965f0000005149444154789c635062e860 3062 | 00e17286bb609c93c370ec189494960631366e4467b3ae675dcf10f521ea0303 3063 | 90c1ca006444e11643482064114a4852c710baea3f18c31918020c30410403a6 3064 | 0ac1a09239009c52804d85b6d97d0000000049454e44ae426082 3065 | """), 3066 | 'basi0g04': _dehex(""" 3067 | 89504e470d0a1a0a0000000d4948445200000020000000200400000001e4e6f8 3068 | bf0000000467414d41000186a031e8965f000000ae49444154789c658e5111c2 3069 | 301044171c141c141c041c843a287510ea20d441c041c141c141c04191102454 3070 | 03994998cecd7edcecedbb9bdbc3b2c2b6457545fbc4bac1be437347f7c66a77 3071 | 3c23d60db15e88f5c5627338a5416c2e691a9b475a89cd27eda12895ae8dfdab 3072 | 43d61e590764f5c83a226b40d669bec307f93247701687723abf31ff83a2284b 3073 | a5b4ae6b63ac6520ad730ca4ed7b06d20e030369bd6720ed383290360406d24e 3074 | 13811f2781eba9d34d07160000000049454e44ae426082 3075 | """), 3076 | 'basi0g08': _dehex(""" 3077 | 89504e470d0a1a0a0000000d4948445200000020000000200800000001211615 3078 | be0000000467414d41000186a031e8965f000000b549444154789cb5905d0ac2 3079 | 3010849dbac81c42c47bf843cf253e8878b0aa17110f214bdca6be240f5d21a5 3080 | 94ced3e49bcd322c1624115515154998aa424822a82a5624a1aa8a8b24c58f99 3081 | 999908130989a04a00d76c2c09e76cf21adcb209393a6553577da17140a2c59e 3082 | 70ecbfa388dff1f03b82fb82bd07f05f7cb13f80bb07ad2fd60c011c3c588eef 3083 | f1f4e03bbec7ce832dca927aea005e431b625796345307b019c845e6bfc3bb98 3084 | 769d84f9efb02ea6c00f9bb9ff45e81f9f280000000049454e44ae426082 3085 | """), 3086 | 'basi0g16': _dehex(""" 3087 | 89504e470d0a1a0a0000000d49484452000000200000002010000000017186c9 3088 | fd0000000467414d41000186a031e8965f000000e249444154789cb5913b0ec2 3089 | 301044c7490aa8f85d81c3e4301c8f53a4ca0da8902c8144b3920b4043111282 3090 | 23bc4956681a6bf5fc3c5a3ba0448912d91a4de2c38dd8e380231eede4c4f7a1 3091 | 4677700bec7bd9b1d344689315a3418d1a6efbe5b8305ba01f8ff4808c063e26 3092 | c60d5c81edcf6c58c535e252839e93801b15c0a70d810ae0d306b205dc32b187 3093 | 272b64057e4720ff0502154034831520154034c3df81400510cdf0015c86e5cc 3094 | 5c79c639fddba9dcb5456b51d7980eb52d8e7d7fa620a75120d6064641a05120 3095 | b606771a05626b401a05f1f589827cf0fe44c1f0bae0055698ee8914fffffe00 3096 | 00000049454e44ae426082 3097 | """), 3098 | 'basi2c08': _dehex(""" 3099 | 89504e470d0a1a0a0000000d49484452000000200000002008020000018b1fdd 3100 | 350000000467414d41000186a031e8965f000000f249444154789cd59341aa04 3101 | 210c44abc07b78133d59d37333bd89d76868b566d10cf4675af8596431a11662 3102 | 7c5688919280e312257dd6a0a4cf1a01008ee312a5f3c69c37e6fcc3f47e6776 3103 | a07f8bdaf5b40feed2d33e025e2ff4fe2d4a63e1a16d91180b736d8bc45854c5 3104 | 6d951863f4a7e0b66dcf09a900f3ffa2948d4091e53ca86c048a64390f662b50 3105 | 4a999660ced906182b9a01a8be00a56404a6ede182b1223b4025e32c4de34304 3106 | 63457680c93aada6c99b73865aab2fc094920d901a203f5ddfe1970d28456783 3107 | 26cffbafeffcd30654f46d119be4793f827387fc0d189d5bc4d69a3c23d45a7f 3108 | db803146578337df4d0a3121fc3d330000000049454e44ae426082 3109 | """), 3110 | 'basi2c16': _dehex(""" 3111 | 89504e470d0a1a0a0000000d4948445200000020000000201002000001db8f01 3112 | 760000000467414d41000186a031e8965f0000020a49444154789cd5962173e3 3113 | 3010853fcf1838cc61a1818185a53e56787fa13fa130852e3b5878b4b0b03081 3114 | b97f7030070b53e6b057a0a8912bbb9163b9f109ececbc59bd7dcf2b45492409 3115 | d66f00eb1dd83cb5497d65456aeb8e1040913b3b2c04504c936dd5a9c7e2c6eb 3116 | b1b8f17a58e8d043da56f06f0f9f62e5217b6ba3a1b76f6c9e99e8696a2a72e2 3117 | c4fb1e4d452e92ec9652b807486d12b6669be00db38d9114b0c1961e375461a5 3118 | 5f76682a85c367ad6f682ff53a9c2a353191764b78bb07d8ddc3c97c1950f391 3119 | 6745c7b9852c73c2f212605a466a502705c8338069c8b9e84efab941eb393a97 3120 | d4c9fd63148314209f1c1d3434e847ead6380de291d6f26a25c1ebb5047f5f24 3121 | d85c49f0f22cc1d34282c72709cab90477bf25b89d49f0f351822297e0ea9704 3122 | f34c82bc94002448ede51866e5656aef5d7c6a385cb4d80e6a538ceba04e6df2 3123 | 480e9aa84ddedb413bb5c97b3838456df2d4fec2c7a706983e7474d085fae820 3124 | a841776a83073838973ac0413fea2f1dc4a06e71108fda73109bdae48954ad60 3125 | bf867aac3ce44c7c1589a711cf8a81df9b219679d96d1cec3d8bbbeaa2012626 3126 | df8c7802eda201b2d2e0239b409868171fc104ba8b76f10b4da09f6817ffc609 3127 | c413ede267fd1fbab46880c90f80eccf0013185eb48b47ba03df2bdaadef3181 3128 | cb8976f18e13188768170f98c0f844bb78cb04c62ddac59d09fc3fa25dfc1da4 3129 | 14deb3df1344f70000000049454e44ae426082 3130 | """), 3131 | 'basi3p08': _dehex(""" 3132 | 89504e470d0a1a0a0000000d494844520000002000000020080300000133a3ba 3133 | 500000000467414d41000186a031e8965f00000300504c5445224400f5ffed77 3134 | ff77cbffff110a003a77002222ffff11ff110000222200ffac5566ff66ff6666 3135 | ff01ff221200dcffffccff994444ff005555220000cbcbff44440055ff55cbcb 3136 | 00331a00ffecdcedffffe4ffcbffdcdc44ff446666ff330000442200ededff66 3137 | 6600ffa444ffffaaeded0000cbcbfefffffdfffeffff0133ff33552a000101ff 3138 | 8888ff00aaaa010100440000888800ffe4cbba5b0022ff22663200ffff99aaaa 3139 | ff550000aaaa00cb630011ff11d4ffaa773a00ff4444dc6b0066000001ff0188 3140 | 4200ecffdc6bdc00ffdcba00333300ed00ed7300ffff88994a0011ffff770000 3141 | ff8301ffbabafe7b00fffeff00cb00ff999922ffff880000ffff77008888ffdc 3142 | ff1a33000000aa33ffff009900990000000001326600ffbaff44ffffffaaff00 3143 | 770000fefeaa00004a9900ffff66ff22220000998bff1155ffffff0101ff88ff 3144 | 005500001111fffffefffdfea4ff4466ffffff66ff003300ffff55ff77770000 3145 | 88ff44ff00110077ffff006666ffffed000100fff5ed1111ffffff44ff22ffff 3146 | eded11110088ffff00007793ff2200dcdc3333fffe00febabaff99ffff333300 3147 | 63cb00baba00acff55ffffdcffff337bfe00ed00ed5555ffaaffffdcdcff5555 3148 | 00000066dcdc00dc00dc83ff017777fffefeffffffcbff5555777700fefe00cb 3149 | 00cb0000fe010200010000122200ffff220044449bff33ffd4aa0000559999ff 3150 | 999900ba00ba2a5500ffcbcbb4ff66ff9b33ffffbaaa00aa42880053aa00ffaa 3151 | aa0000ed00babaffff1100fe00000044009999990099ffcc99ba000088008800 3152 | dc00ff93220000dcfefffeaa5300770077020100cb0000000033ffedff00ba00 3153 | ff3333edffedffc488bcff7700aa00660066002222dc0000ffcbffdcffdcff8b 3154 | 110000cb00010155005500880000002201ffffcbffcbed0000ff88884400445b 3155 | ba00ffbc77ff99ff006600baffba00777773ed00fe00003300330000baff77ff 3156 | 004400aaffaafffefe000011220022c4ff8800eded99ff99ff55ff002200ffb4 3157 | 661100110a1100ff1111dcffbabaffff88ff88010001ff33ffb98ed362000002 3158 | a249444154789c65d0695c0b001806f03711a9904a94d24dac63292949e5a810 3159 | d244588a14ca5161d1a1323973252242d62157d12ae498c8124d25ca3a11398a 3160 | 16e55a3cdffab0ffe7f77d7fcff3528645349b584c3187824d9d19d4ec2e3523 3161 | 9eb0ae975cf8de02f2486d502191841b42967a1ad49e5ddc4265f69a899e26b5 3162 | e9e468181baae3a71a41b95669da8df2ea3594c1b31046d7b17bfb86592e4cbe 3163 | d89b23e8db0af6304d756e60a8f4ad378bdc2552ae5948df1d35b52143141533 3164 | 33bbbbababebeb3b3bc9c9c9c6c6c0c0d7b7b535323225a5aa8a02024a4bedec 3165 | 0a0a2a2bcdcd7d7cf2f3a9a9c9cdcdd8b8adcdd5b5ababa828298982824a4ab2 3166 | b21212acadbdbc1414e2e24859b9a72730302f4f49292c4c57373c9c0a0b7372 3167 | 8c8c1c1c3a3a92936d6dfdfd293e3e26262a4a4eaea2424b4b5fbfbc9c323278 3168 | 3c0b0ba1303abaae8ecdeeed950d6669a9a7a7a141d4de9e9d5d5cdcd2229b94 3169 | c572716132f97cb1d8db9bc3110864a39795d9db6b6a26267a7a9a98d4d6a6a7 3170 | cb76090ef6f030354d4d75766e686030545464cb393a1a1ac6c68686eae8f8f9 3171 | a9aa4644c8b66d6e1689dcdd2512a994cb35330b0991ad9f9b6b659596a6addd 3172 | d8282fafae5e5323fb8f41d01f76c22fd8061be01bfc041a0323e1002c81cd30 3173 | 0b9ec027a0c930014ec035580fc3e112bc069a0b53e11c0c8095f00176c163a0 3174 | e5301baec06a580677600ddc05ba0f13e120bc81a770133ec355a017300d4ec2 3175 | 0c7800bbe1219c02fa08f3e13c1c85dbb00a2ec05ea0dff00a6ec15a98027360 3176 | 070c047a06d7e1085c84f1b014f6c03fa0b33018b6c0211801ebe018fc00da0a 3177 | 6f61113c877eb01d4ec317a085700f26c130f80efbe132bc039a0733e106fc81 3178 | f7f017f6c10aa0d1300a0ec374780943e1382c06fa0a9b60238c83473016cec0 3179 | 02f80f73fefe1072afc1e50000000049454e44ae426082 3180 | """), 3181 | 'basi6a08': _dehex(""" 3182 | 89504e470d0a1a0a0000000d4948445200000020000000200806000001047d4a 3183 | 620000000467414d41000186a031e8965f0000012049444154789cc595414ec3 3184 | 3010459fa541b8bbb26641b8069b861e8b4d12c1c112c1452a710a2a65d840d5 3185 | 949041fc481ec98ae27c7f3f8d27e3e4648047600fec0d1f390fbbe2633a31e2 3186 | 9389e4e4ea7bfdbf3d9a6b800ab89f1bd6b553cfcbb0679e960563d72e0a9293 3187 | b7337b9f988cc67f5f0e186d20e808042f1c97054e1309da40d02d7e27f92e03 3188 | 6cbfc64df0fc3117a6210a1b6ad1a00df21c1abcf2a01944c7101b0cb568a001 3189 | 909c9cf9e399cf3d8d9d4660a875405d9a60d000b05e2de55e25780b7a5268e0 3190 | 622118e2399aab063a815808462f1ab86890fc2e03e48bb109ded7d26ce4bf59 3191 | 0db91bac0050747fec5015ce80da0e5700281be533f0ce6d5900b59bcb00ea6d 3192 | 200314cf801faab200ea752803a8d7a90c503a039f824a53f4694e7342000000 3193 | 0049454e44ae426082 3194 | """), 3195 | 'basn0g01': _dehex(""" 3196 | 89504e470d0a1a0a0000000d49484452000000200000002001000000005b0147 3197 | 590000000467414d41000186a031e8965f0000005b49444154789c2dccb10903 3198 | 300c05d1ebd204b24a200b7a346f90153c82c18d0a61450751f1e08a2faaead2 3199 | a4846ccea9255306e753345712e211b221bf4b263d1b427325255e8bdab29e6f 3200 | 6aca30692e9d29616ee96f3065f0bf1f1087492fd02f14c90000000049454e44 3201 | ae426082 3202 | """), 3203 | 'basn0g02': _dehex(""" 3204 | 89504e470d0a1a0a0000000d49484452000000200000002002000000001ca13d 3205 | 890000000467414d41000186a031e8965f0000001f49444154789c6360085df5 3206 | 1f8cf1308850c20053868f0133091f6390b90700bd497f818b0989a900000000 3207 | 49454e44ae426082 3208 | """), 3209 | # A version of basn0g04 dithered down to 3 bits. 3210 | 'Basn0g03': _dehex(""" 3211 | 89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 3212 | 2900000001734249540371d88211000000fd49444154789c6d90d18906210c84 3213 | c356f22356b2889588604301b112112b11d94a96bb495cf7fe87f32d996f2689 3214 | 44741cc658e39c0b118f883e1f63cc89dafbc04c0f619d7d898396c54b875517 3215 | 83f3a2e7ac09a2074430e7f497f00f1138a5444f82839c5206b1f51053cca968 3216 | 63258821e7f2b5438aac16fbecc052b646e709de45cf18996b29648508728612 3217 | 952ca606a73566d44612b876845e9a347084ea4868d2907ff06be4436c4b41a3 3218 | a3e1774285614c5affb40dbd931a526619d9fa18e4c2be420858de1df0e69893 3219 | a0e3e5523461be448561001042b7d4a15309ce2c57aef2ba89d1c13794a109d7 3220 | b5880aa27744fc5c4aecb5e7bcef5fe528ec6293a930690000000049454e44ae 3221 | 426082 3222 | """), 3223 | 'basn0g04': _dehex(""" 3224 | 89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 3225 | 290000000467414d41000186a031e8965f0000004849444154789c6360601014 3226 | 545232367671090d4d4b2b2f6720430095dbd1418e002a77e64c720450b9ab56 3227 | 912380caddbd9b1c0154ee9933e408a072efde25470095fbee1d1902001f14ee 3228 | 01eaff41fa0000000049454e44ae426082 3229 | """), 3230 | 'basn0g08': _dehex(""" 3231 | 89504e470d0a1a0a0000000d4948445200000020000000200800000000561125 3232 | 280000000467414d41000186a031e8965f0000004149444154789c6364602400 3233 | 1408c8b30c05058c0f0829f8f71f3f6079301c1430ca11906764a2795c0c0605 3234 | 8c8ff0cafeffcff887e67131181430cae0956564040050e5fe7135e2d8590000 3235 | 000049454e44ae426082 3236 | """), 3237 | 'basn0g16': _dehex(""" 3238 | 89504e470d0a1a0a0000000d49484452000000200000002010000000000681f9 3239 | 6b0000000467414d41000186a031e8965f0000005e49444154789cd5d2310ac0 3240 | 300c4351395bef7fc6dca093c0287b32d52a04a3d98f3f3880a7b857131363a0 3241 | 3a82601d089900dd82f640ca04e816dc06422640b7a03d903201ba05b7819009 3242 | d02d680fa44c603f6f07ec4ff41938cf7f0016d84bd85fae2b9fd70000000049 3243 | 454e44ae426082 3244 | """), 3245 | 'basn2c08': _dehex(""" 3246 | 89504e470d0a1a0a0000000d4948445200000020000000200802000000fc18ed 3247 | a30000000467414d41000186a031e8965f0000004849444154789cedd5c10900 3248 | 300c024085ec91fdb772133b442bf4a1f8cee12bb40d043b800a14f81ca0ede4 3249 | 7d4c784081020f4a871fc284071428f0a0743823a94081bb7077a3c00182b1f9 3250 | 5e0f40cf4b0000000049454e44ae426082 3251 | """), 3252 | 'basn2c16': _dehex(""" 3253 | 89504e470d0a1a0a0000000d4948445200000020000000201002000000ac8831 3254 | e00000000467414d41000186a031e8965f000000e549444154789cd596c10a83 3255 | 301044a7e0417fcb7eb7fdadf6961e06039286266693cc7a188645e43dd6a08f 3256 | 1042003e2fe09aef6472737e183d27335fcee2f35a77b702ebce742870a23397 3257 | f3edf2705dd10160f3b2815fe8ecf2027974a6b0c03f74a6e4192843e75c6c03 3258 | 35e8ec3202f5e84c0181bbe8cca967a00d9df3491bb040671f2e6087ce1c2860 3259 | 8d1e05f8c7ee0f1d00b667e70df44467ef26d01fbd9bc028f42860f71d188bce 3260 | fb8d3630039dbd59601e7ab3c06cf428507f0634d039afdc80123a7bb1801e7a 3261 | b1802a7a14c89f016d74ce331bf080ce9e08f8414f04bca133bfe642fe5e07bb 3262 | c4ec0000000049454e44ae426082 3263 | """), 3264 | 'basn3p04': _dehex(""" 3265 | 89504e470d0a1a0a0000000d4948445200000020000000200403000000815467 3266 | c70000000467414d41000186a031e8965f000000037342495404040477f8b5a3 3267 | 0000002d504c54452200ff00ffff8800ff22ff000099ffff6600dd00ff77ff00 3268 | ff000000ff99ddff00ff00bbffbb000044ff00ff44d2b049bd00000047494441 3269 | 54789c63e8e8080d3d7366d5aaf27263e377ef66ce64204300952b28488e002a 3270 | d7c5851c0154eeddbbe408a07119c81140e52a29912380ca4d4b23470095bb7b 3271 | 37190200e0c4ead10f82057d0000000049454e44ae426082 3272 | """), 3273 | 'basn6a08': _dehex(""" 3274 | 89504e470d0a1a0a0000000d4948445200000020000000200806000000737a7a 3275 | f40000000467414d41000186a031e8965f0000006f49444154789cedd6310a80 3276 | 300c46e12764684fa1f73f55048f21c4ddc545781d52e85028fc1f4d28d98a01 3277 | 305e7b7e9cffba33831d75054703ca06a8f90d58a0074e351e227d805c8254e3 3278 | 1bb0420f5cdc2e0079208892ffe2a00136a07b4007943c1004d900195036407f 3279 | 011bf00052201a9c160fb84c0000000049454e44ae426082 3280 | """), 3281 | 'cs3n3p08': _dehex(""" 3282 | 89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a 3283 | c60000000467414d41000186a031e8965f0000000373424954030303a392a042 3284 | 00000054504c544592ff0000ff9200ffff00ff0000dbff00ff6dffb600006dff 3285 | b6ff00ff9200dbff000049ffff2400ff000024ff0049ff0000ffdb00ff4900ff 3286 | b6ffff0000ff2400b6ffffdb000092ffff6d000024ffff49006dff00df702b17 3287 | 0000004b49444154789c85cac70182000000b1b3625754b0edbfa72324ef7486 3288 | 184ed0177a437b680bcdd0031c0ed00ea21f74852ed00a1c9ed0086da0057487 3289 | 6ed0121cd6d004bda0013a421ff803224033e177f4ae260000000049454e44ae 3290 | 426082 3291 | """), 3292 | 's09n3p02': _dehex(""" 3293 | 89504e470d0a1a0a0000000d49484452000000090000000902030000009dffee 3294 | 830000000467414d41000186a031e8965f000000037342495404040477f8b5a3 3295 | 0000000c504c544500ff000077ffff00ffff7700ff5600640000001f49444154 3296 | 789c63600002fbff0c0c56ab19182ca381581a4283f82071200000696505c36a 3297 | 437f230000000049454e44ae426082 3298 | """), 3299 | 'tbgn3p08': _dehex(""" 3300 | 89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a 3301 | c60000000467414d41000186a031e8965f00000207504c54457f7f7fafafafab 3302 | abab110000222200737300999999510d00444400959500959595e6e600919191 3303 | 8d8d8d620d00898989666600b7b700911600000000730d007373736f6f6faaaa 3304 | 006b6b6b676767c41a00cccc0000f30000ef00d51e0055555567670000dd0051 3305 | 515100d1004d4d4de61e0038380000b700160d0d00ab00560d00090900009500 3306 | 009100008d003333332f2f2f2f2b2f2b2b000077007c7c001a05002b27000073 3307 | 002b2b2b006f00bb1600272727780d002323230055004d4d00cc1e00004d00cc 3308 | 1a000d00003c09006f6f00002f003811271111110d0d0d55554d090909001100 3309 | 4d0900050505000d00e2e200000900000500626200a6a6a6a2a2a29e9e9e8484 3310 | 00fb00fbd5d500801100800d00ea00ea555500a6a600e600e6f7f700e200e233 3311 | 0500888888d900d9848484c01a007777003c3c05c8c8008080804409007c7c7c 3312 | bb00bbaa00aaa600a61e09056262629e009e9a009af322005e5e5e05050000ee 3313 | 005a5a5adddd00a616008d008d00e20016050027270088110078780000c40078 3314 | 00787300736f006f44444400aa00c81e004040406600663c3c3c090000550055 3315 | 1a1a00343434d91e000084004d004d007c004500453c3c00ea1e00222222113c 3316 | 113300331e1e1efb22001a1a1a004400afaf00270027003c001616161e001e0d 3317 | 160d2f2f00808000001e00d1d1001100110d000db7b7b7090009050005b3b3b3 3318 | 6d34c4230000000174524e530040e6d86600000001624b474402660b7c640000 3319 | 01f249444154789c6360c0048c8c58049100575f215ee92e6161ef109cd2a15e 3320 | 4b9645ce5d2c8f433aa4c24f3cbd4c98833b2314ab74a186f094b9c2c27571d2 3321 | 6a2a58e4253c5cda8559057a392363854db4d9d0641973660b0b0bb76bb16656 3322 | 06970997256877a07a95c75a1804b2fbcd128c80b482a0b0300f8a824276a9a8 3323 | ec6e61612b3e57ee06fbf0009619d5fac846ac5c60ed20e754921625a2daadc6 3324 | 1967e29e97d2239c8aec7e61fdeca9cecebef54eb36c848517164514af16169e 3325 | 866444b2b0b7b55534c815cc2ec22d89cd1353800a8473100a4485852d924a6a 3326 | 412adc74e7ad1016ceed043267238c901716f633a812022998a4072267c4af02 3327 | 92127005c0f811b62830054935ce017b38bf0948cc5c09955f030a24617d9d46 3328 | 63371fd940b0827931cbfdf4956076ac018b592f72d45594a9b1f307f3261b1a 3329 | 084bc2ad50018b1900719ba6ba4ca325d0427d3f6161449486f981144cf3100e 3330 | 2a5f2a1ce8683e4ddf1b64275240c8438d98af0c729bbe07982b8a1c94201dc2 3331 | b3174c9820bcc06201585ad81b25b64a2146384e3798290c05ad280a18c0a62e 3332 | e898260c07fca80a24c076cc864b777131a00190cdfa3069035eccbc038c30e1 3333 | 3e88b46d16b6acc5380d6ac202511c392f4b789aa7b0b08718765990111606c2 3334 | 9e854c38e5191878fbe471e749b0112bb18902008dc473b2b2e8e72700000000 3335 | 49454e44ae426082 3336 | """), 3337 | 'Tp2n3p08': _dehex(""" 3338 | 89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a 3339 | c60000000467414d41000186a031e8965f00000300504c544502ffff80ff05ff 3340 | 7f0703ff7f0180ff04ff00ffff06ff000880ff05ff7f07ffff06ff000804ff00 3341 | 0180ff02ffff03ff7f02ffff80ff0503ff7f0180ffff0008ff7f0704ff00ffff 3342 | 06ff000802ffffff7f0704ff0003ff7fffff0680ff050180ff04ff000180ffff 3343 | 0008ffff0603ff7f80ff05ff7f0702ffffff000880ff05ffff0603ff7f02ffff 3344 | ff7f070180ff04ff00ffff06ff000880ff050180ffff7f0702ffff04ff0003ff 3345 | 7fff7f0704ff0003ff7f0180ffffff06ff000880ff0502ffffffff0603ff7fff 3346 | 7f0702ffff04ff000180ff80ff05ff0008ff7f07ffff0680ff0504ff00ff0008 3347 | 0180ff03ff7f02ffff02ffffffff0604ff0003ff7f0180ffff000880ff05ff7f 3348 | 0780ff05ff00080180ff02ffffff7f0703ff7fffff0604ff00ff7f07ff0008ff 3349 | ff0680ff0504ff0002ffff0180ff03ff7fff0008ffff0680ff0504ff000180ff 3350 | 02ffff03ff7fff7f070180ff02ffff04ff00ffff06ff0008ff7f0780ff0503ff 3351 | 7fffff06ff0008ff7f0780ff0502ffff03ff7f0180ff04ff0002ffffff7f07ff 3352 | ff0604ff0003ff7fff00080180ff80ff05ffff0603ff7f0180ffff000804ff00 3353 | 80ff0502ffffff7f0780ff05ffff0604ff000180ffff000802ffffff7f0703ff 3354 | 7fff0008ff7f070180ff03ff7f02ffff80ff05ffff0604ff00ff0008ffff0602 3355 | ffff0180ff04ff0003ff7f80ff05ff7f070180ff04ff00ff7f0780ff0502ffff 3356 | ff000803ff7fffff0602ffffff7f07ffff0680ff05ff000804ff0003ff7f0180 3357 | ff02ffff0180ffff7f0703ff7fff000804ff0080ff05ffff0602ffff04ff00ff 3358 | ff0603ff7fff7f070180ff80ff05ff000803ff7f0180ffff7f0702ffffff0008 3359 | 04ff00ffff0680ff0503ff7f0180ff04ff0080ff05ffff06ff000802ffffff7f 3360 | 0780ff05ff0008ff7f070180ff03ff7f04ff0002ffffffff0604ff00ff7f07ff 3361 | 000880ff05ffff060180ff02ffff03ff7f80ff05ffff0602ffff0180ff03ff7f 3362 | 04ff00ff7f07ff00080180ffff000880ff0502ffff04ff00ff7f0703ff7fffff 3363 | 06ff0008ffff0604ff00ff7f0780ff0502ffff03ff7f0180ffdeb83387000000 3364 | f874524e53000000000000000008080808080808081010101010101010181818 3365 | 1818181818202020202020202029292929292929293131313131313131393939 3366 | 393939393941414141414141414a4a4a4a4a4a4a4a52525252525252525a5a5a 3367 | 5a5a5a5a5a62626262626262626a6a6a6a6a6a6a6a73737373737373737b7b7b 3368 | 7b7b7b7b7b83838383838383838b8b8b8b8b8b8b8b94949494949494949c9c9c 3369 | 9c9c9c9c9ca4a4a4a4a4a4a4a4acacacacacacacacb4b4b4b4b4b4b4b4bdbdbd 3370 | bdbdbdbdbdc5c5c5c5c5c5c5c5cdcdcdcdcdcdcdcdd5d5d5d5d5d5d5d5dedede 3371 | dededededee6e6e6e6e6e6e6e6eeeeeeeeeeeeeeeef6f6f6f6f6f6f6f6b98ac5 3372 | ca0000012c49444154789c6360e7169150d230b475f7098d4ccc28a96ced9e32 3373 | 63c1da2d7b8e9fb97af3d1fb8f3f18e8a0808953544a4dd7c4c2c9233c2621bf 3374 | b4aab17fdacce5ab36ee3a72eafaad87efbefea68702362e7159652d031b07cf 3375 | c0b8a4cce28aa68e89f316aedfb4ffd0b92bf79fbcfcfe931e0a183904e55435 3376 | 8decdcbcc22292b3caaadb7b27cc5db67af3be63e72fdf78fce2d31f7a2860e5 3377 | 119356d037b374f10e8a4fc92eaa6fee99347fc9caad7b0f9ebd74f7c1db2fbf 3378 | e8a180995f484645dbdccad12f38363dafbcb6a573faeca5ebb6ed3e7ce2c29d 3379 | e76fbefda38702063e0149751d537b67ff80e8d4dcc29a86bea97316add9b0e3 3380 | c0e96bf79ebdfafc971e0a587885e515f58cad5d7d43a2d2720aeadaba26cf5a 3381 | bc62fbcea3272fde7efafac37f3a28000087c0fe101bc2f85f0000000049454e 3382 | 44ae426082 3383 | """), 3384 | 'tbbn1g04': _dehex(""" 3385 | 89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 3386 | 290000000467414d41000186a031e8965f0000000274524e530007e8f7589b00 3387 | 000002624b47440000aa8d23320000013e49444154789c55d1cd4b024118c7f1 3388 | efbe6419045b6a48a72d352808b435284f9187ae9b098627a1573a19945beba5 3389 | e8129e8222af11d81e3a4545742de8ef6af6d5762e0fbf0fc33c33f36085cb76 3390 | bc4204778771b867260683ee57e13f0c922df5c719c2b3b6c6c25b2382cea4b9 3391 | 9f7d4f244370746ac71f4ca88e0f173a6496749af47de8e44ba8f3bf9bdfa98a 3392 | 0faf857a7dd95c7dc8d7c67c782c99727997f41eb2e3c1e554152465bb00fe8e 3393 | b692d190b718d159f4c0a45c4435915a243c58a7a4312a7a57913f05747594c6 3394 | 46169866c57101e4d4ce4d511423119c419183a3530cc63db88559ae28e7342a 3395 | 1e9c8122b71139b8872d6e913153224bc1f35b60e4445bd4004e20ed6682c759 3396 | 1d9873b3da0fbf50137dc5c9bde84fdb2ec8bde1189e0448b63584735993c209 3397 | 7a601bd2710caceba6158797285b7f2084a2f82c57c01a0000000049454e44ae 3398 | 426082 3399 | """), 3400 | 'tbrn2c08': _dehex(""" 3401 | 89504e470d0a1a0a0000000d4948445200000020000000200802000000fc18ed 3402 | a30000000467414d41000186a031e8965f0000000674524e53007f007f007f8a 3403 | 33334f00000006624b474400ff0000000033277cf3000004d649444154789cad 3404 | 965f68537714c73fd912d640235e692f34d0406fa0c1663481045ab060065514 3405 | 56660a295831607df0a1488715167060840a1614e6431e9cb34fd2c00a762c85 3406 | f6a10f816650c13b0cf40612e1822ddc4863bd628a8924d23d6464f9d3665dd9 3407 | f7e977ce3dbff3cd3939bfdfef6bb87dfb364782dbed065ebe7cd93acc78b4ec 3408 | a228debd7bb7bfbfbfbbbbfb7f261045311a8d261209405194274f9ea4d3e916 3409 | f15f1c3eb5dd6e4fa5fecce526239184a2b0b8486f6f617171b1f5ae4311381c 3410 | 8e57af5e5dbd7a351088150a78bd389d44222c2f93cdfe66b7db8f4ee07038b6 3411 | b6b6bebf766d7e7e7e60a06432313b4ba984c3c1c4049a46b95c5a58583822c1 3412 | dbb76f27272733d1b9df853c3030c0f232562b9108cf9eb1b888d7cbf030abab 3413 | 31abd5fa1f08dc6ef7e7cf9f1f3f7e1c8944745d4f1400c62c001313acad21cb 3414 | b8dd2c2c603271eb1640341aad4c6d331aa7e8c48913a150a861307ecc11e964 3415 | 74899919bc5e14e56fffc404f1388502f178dceff7ef4bf0a5cfe7abb533998c 3416 | e5f9ea2f1dd88c180d64cb94412df3dd57e83a6b3b3c7a84c98420100c72fd3a 3417 | 636348bae726379fe69e8e8d8dbd79f3a6558b0607079796965256479b918085 3418 | 7b02db12712b6181950233023f3f647494ee6e2e5ea45864cce5b8a7fe3acffc 3419 | 3aebb22c2bd5d20e22d0757d7b7bbbbdbd3d94a313bed1b0aa3cd069838b163a 3420 | 8d4c59585f677292d0b84d9a995bd337def3fe6bbe5e6001989b9b6bfe27ea08 3421 | 36373781542ab56573248b4c5bc843ac4048c7ab21aa24ca00534c25482828a3 3422 | 8c9ee67475bbaaaab22cb722c8e57240a150301a8d219de94e44534d7d90e885 3423 | 87acb0e2c4f9800731629b6c5ee14a35a6b9887d2a0032994cb9cf15dbe59650 3424 | ff7b46a04c9a749e7cc5112214266cc65c31354d5b5d5d3d90209bcd5616a552 3425 | a95c2e87f2a659bd9ee01c2cd73964e438f129a6aa9e582c363838b80f81d7eb 3426 | 5555b56a2a8ad2d9d7affd0409f8015c208013fea00177b873831b0282c964f2 3427 | 783c1e8fa7582cee5f81a669b5e6eeeeaee58e8559b0c233d8843c7c0b963a82 3428 | 34e94b5cb2396d7d7d7db22c8ba258fb0afd43f0e2c58b919191ba9de9b4d425 3429 | 118329b0c3323c8709d02041b52b4ea7f39de75d2a934a2693c0a953a76a93d4 3430 | 5d157ebf7f6565a5542a553df97c5e10045dd731c130b86113cc300cbd489224 3431 | 08422a952a140a95788fc763b1d41558d7a2d7af5f5fb870a1d6a3aaaacd6603 3432 | 18802da84c59015bd2e6897b745d9765b99a1df0f97c0daf74e36deaf7fbcd66 3433 | 73ad2797cb89a2c839880188a2e8743a8bc5a22ccbba5e376466b3b9bdbdbd21 3434 | 6123413a9d0e0402b51e4dd3bababa788eb022b85caeb6b6364551b6b7b76942 3435 | 43f7f727007a7a7a04a1ee8065b3595fde2768423299ac1ec6669c3973e65004 3436 | c0f8f878ad69341a33994ced2969c0d0d0502412f9f8f163f3a7fd654b474787 3437 | 288ad53e74757535df6215b85cae60302849d2410aecc037f9f2e5cbd5b5c160 3438 | 680eb0dbede170381c0e7ff8f0a185be3b906068684892a4ca7a6f6faff69328 3439 | 8ad3d3d3f7efdfdfdbdbfb57e96868a14d0d0643381c96242997cbe5f3794010 3440 | 84603078fcf8f1d6496bd14a3aba5c2ea7d369341a5555b5582c8140e0fcf9f3 3441 | 1b1b1b87cf4eeb0a8063c78e45a3d19e9e1ebfdfdf5a831e844655d18093274f 3442 | 9e3d7bf6d3a74f3b3b3b47c80efc05ff7af28fefb70d9b0000000049454e44ae 3443 | 426082 3444 | """), 3445 | 'basn6a16': _dehex(""" 3446 | 89504e470d0a1a0a0000000d494844520000002000000020100600000023eaa6 3447 | b70000000467414d41000186a031e8965f00000d2249444154789cdd995f6c1c 3448 | d775c67ff38fb34b724d2ee55a8e4b04a0ac87049100cab4dbd8c6528902cb4d 3449 | 10881620592e52d4325ac0905bc98a94025e71fd622cb5065ac98a0c283050c0 3450 | 728a00b6e542a1d126885cd3298928891d9a0444037e904434951d4b90b84b2f 3451 | c9dde1fcebc33977a95555348f411e16dfce9d3b77ee77eebde77ce78c95a669 3452 | 0ad07c17009a13edd898b87dfb1fcb7d2b4d1bff217f33df80deb1e6267df0ff 3453 | c1e6e6dfafdf1f5a7fd30f9aef66b6d546dd355bf02c40662e3307f9725a96c6 3454 | 744c3031f83782f171c148dbc3bf1774f5dad1e79d6f095a3f54d4fbec5234ef 3455 | d9a2f8d73afe4f14f57ef4f42def7b44f19060f06b45bddf1c5534d77fd922be 3456 | 2973a15a82e648661c6e3240aa3612ead952b604bde57458894f29deaf133bac 3457 | 13d2766f5227a4a3b8cf08da7adfd6fbd6bd8a4fe9dbb43d35e3dfa3f844fbf8 3458 | 9119bf4f7144094fb56333abf8a86063ca106f94b3a3b512343765e60082097f 3459 | 1bb86ba72439a653519b09f5cee1ce61c897d37eedf5553580ae60f4af8af33a 3460 | b14fd400b6a0f34535c0434afc0b3a9f07147527a5fa7ca218ff56c74d74dc3f 3461 | 155cfd3325fc278acf2ae1cb4a539f5f9937c457263b0bd51234c732a300cdd1 3462 | cc1840f0aaff54db0e4874ed5a9b5d6d27d4bb36746d80de72baa877ff4b275a 3463 | d7895ed1897ea4139b5143fcbb1a62560da1ed9662aaed895ec78a91c18795b8 3464 | 5e07ab4af8ba128e95e682e0728bf8f2e5ae815a091a53d902ac1920d8e05f06 3465 | 589de8d8d66680789f4e454fb9d9ec66cd857af796ee2d902fa73fd5bba775a2 3466 | 153580ae44705ed0d37647d15697cb8f14bfa3e3e8fdf8031d47af571503357c 3467 | f30d25acedcbbf135c9a35c49766ba07ab255859e8ec03684e66860182dff8f7 3468 | 0304bff6ff1c20fc81b7afdd00a71475539a536e36bb5973a19e3b923b02bde5 3469 | e4efd4003ac170eb2d13fe274157afedbd82d6fb3a9a1e85e4551d47cf7078f8 3470 | 9671fe4289ebf5f2bf08d63f37c4eb4773c55a0996efeefa0ca011671d8060ca 3471 | 2f0004c7fcc300e166ef0240f825efe3361f106d57d423d0723f7acacd66376b 3472 | 2ed47b7a7a7a205f4ef4ac4691e0aad9aa0d41cf13741c3580a506487574ddca 3473 | 61a8c403c1863ebfbcac3475168b2de28b8b3d77544bb05ce92a02aceced3c0d 3474 | d0cc65ea371b201cf1c601c24dde1c4078cedbdeb60322f50126a019bf6edc9b 3475 | 39e566b39b3517eaf97c3e0fbde5e4491d45bd74537145d155b476aa0176e868 3476 | c6abebf30dbd5e525c54ac8e18e2d56abeb756827a3d970358a97416019a6f64 3477 | f60004fdfe1580d5c98e618070cc1b05887eee7e0d209a70db7d8063029889b4 3478 | c620ead78d7b33a7dc6c76b3e6427ddddbebde867c393aa7845e5403e8ca794a 3479 | d0d6fb897af5f03525fe5782f5e7046bdaef468bf88d1debc6ab25583cd17310 3480 | 6079b9ab0ba059c914018245bf076075b5a303200c3c1f209a733701444fbbaf 3481 | 00c4134ebb016c5d0b23614c243701cdf875e3decce9349bddacb9505fbf7dfd 3482 | 76e82d87736a00f5d2b5ffd4b7dce2719a4d25ae717ee153c1abef18e257cfad 3483 | 7fa45682da48ef38c052b53b0fd06864b300c151ff08c0ea431de701a287dd5f 3484 | 004497dc7b01a253ee3e80b8c7f91c20f967fb6fdb7c80ada7d8683723614c24 3485 | 3701cdf875e3decc29379bddacb950ef3fd47f08f2e5a61ea4aa2a3eb757cd55 3486 | 13345efcfa59c12b2f19e2578ef77fb75a82854ffbee01a83f977b11a031931d 3487 | 040802df07082b5e11207cc17b1e209a770700e2df0a83e409fb7580f827c230 3488 | 99b06fd901fb058d6835dacd481813c94d40337eddb83773cacd66376b2ed437 3489 | bebcf165e82d2f4e4beb7f3fa6e652c2d7ee10bc78c010bfb87fe3c95a09ae9f 3490 | bd732740bd2fb700d0f865f64180e059ff044018ca0ca28a5b04883f701e0088 3491 | bfec7c0c909cb71f0448c6ec518074b375012079d9dedf66004bcfbc51eb2dd1 3492 | aadacd481813c94d40337eddb83773cacd66376b2ed487868686205fbe7c49ef 3493 | 5605a73f34c4a7a787eeab96e0da81bb4e022c15ba27019a5b339300e16bf286 3494 | a8eae601e25866907cdf3e0890acb36f00245fb57f05904e59c300e92561946e 3495 | b2e600d209ab7d07f04d458dfb46ad1bd16ab49b913026929b8066fcba716fe6 3496 | 949bcd6ed65ca8ef7e7cf7e3d05b7e7c8f217ee6cdddbb6a25a856f37980e0c7 3497 | fe4e80a82623c48193014846ec7180f4acf518409aca0cd28a5504e03b32c374 3498 | de1a00608a0240faaa327a4b19fe946fb6f90054dbb5f2333d022db56eb4966a 3499 | 3723614c243701cdf8f556bea8a7dc6c76b3e66bd46584ddbbcebc0990cf4b0f 3500 | ff4070520c282338a7e26700ec725202b01e4bcf0258963c6f1d4d8f0030cb20 3501 | 805549c520930c03584fa522b676f11600ffc03fde3e1b3489a9c9054c9aa23b 3502 | c08856a3dd8c843191dc0434e3d78d7b33a75c36fb993761f7ae5a69f72ef97f 3503 | e6ad336fed7e1c60e8bee96980bbdebbb60da07b7069062033d9dc0ae03d296f 3504 | 70ab511ec071640676252902d833c916007b3e1900b0a6d2028035968e025861 3505 | ea01581369fb11488c34d18cbc95989afccca42baad65ba2d5683723614c24d7 3506 | 8066fcbab8b7e96918baaf5aaa56219f975fb50a43f7c9bde90fa73f1c1a02d8 3507 | 78f2e27e803b77ca08b90519315b6fe400fc1392097a9eccc0ad444500e70199 3508 | a1331f0f00d8934901c07e5d526ceb87c2d07e2579badd005a2b31a5089391b7 3509 | 1253358049535a6add8856dd0146c298482e01ede27ed878b256ba7600ee3a09 3510 | c18fc1df09fe01084ec25defc1b56db0f1a4f4bd78e0e2818d2f0334e7330300 3511 | 7df7c888b917e50dd9c1c60c80efcb0cbc63e1f700bce7c31700dccbd1060027 3512 | 8add9b0de06c8e2f00d84962b7d7030e2a61538331b98051f92631bd253f336a 3513 | dd8856a3dd44c25c390efddfad96ae9f853b77c25201ba27c533b8bdf28b6ad0 3514 | 3d084b33d2e7fa59099e9901b8f2d29597fa0f01848f78e70082117f1ca07b76 3515 | 6910209b9519f895a008d031bbba05c09d8f06005c5b18b8fba25300cea6780e 3516 | c03e911c6ccf06d507b48a4fa606634a114609de929f9934c5a87511ad57cfc1 3517 | fa476aa5854fa1ef1e3910b905686e85cc24c40138198915f133d2d6dc2a7dea 3518 | 7df2ccc2a752faf2cec1d577aebeb37e3b4034eeee0008dff3be0e6b923773b4 3519 | 7904c0ef9119767cb4fa1500ef1361e08e452500f71561e84cc4ed3e20fab6a2 3520 | c905f40cb76a3026bf3319b91ac2e46792a6dcd801ebc6aba5da08f48ecb81c8 3521 | bd088d5f42f6417191de93908c803d0e76199292b485af41b60e8d9c3c537f0e 3522 | 8211f0c7211a077707dc18b931b2ee6d80a4d7ae024491ebc24d4a708ff70680 3523 | 7f25e807e8785f1878e322d6ddaf453f0770ff2dfa769b01423dbbad72a391b6 3524 | 5a7c3235985629423372494cab55c8f7d64a8b27a0e7202c55a13b0f8d19c80e 3525 | 4ae9ca3f015115dc3ca467c17a4c7ee95970ab10e5a54ff0ac3cd39881ee5958 3526 | 1a84f03df0be0e492fd855a8d6aa35d10b4962dbb0a604a3d3ee5e80a8eee600 3527 | a24977f8660378bf0bbf00e01d0a8fb7f980f04b8aa6ce6aca8d5a7533c52753 3528 | 839152c4e222f4dc512dd5eb90cbc981e8ea12cf90cd8a8bf47d89159e2741d3 3529 | 7124f65b96fcd254dae258fa84a13c13043246a32129574787e49eae2b49b86d 3530 | c3e2e78b9ff7f4002415bb08907c66df0d103b4e0c104db90500ff70700c203a 3531 | ee1e82dba4c3e16e256c0acca6ceaae9afd1f612d7eb472157ac95962bd05594 3532 | 7dd1598466053245088e827f44628657942a825b84e4fb601f84b4025611aca3 3533 | 901e01bb024911dc0a4445f08e41f83df02b10142173149ab71baf027611ea95 3534 | 7a257704201d14cd9af4d90b00f194530088cb4e09c0df1c5c0088f7393f6833 3535 | c0aa3ac156655de3bca9b34ab9716906ba07aba5e5bba1eb3358d90b9da7c533 3536 | 64f6888bf47b60f521e8380fe10be03d2feac17900927560df40f4e48f805960 3537 | 50328d648bf4893f9067c217a0631656b7c898c122847bc07b03a2d3e0ee85e4 3538 | 33b0ef867450c4fad2ecd26cf7168074c0ba0c904cdac300c9cfec4701924df6 3539 | 1cdca61e10685c6f7d52d0caba1498972f43d740adb4b2009d7d7220b20e3473 3540 | 90a943d00ffe959bb6eac3e0fe42ea49ee00c45f06e76329b1dabf127d690d80 3541 | 5581b408f63c2403e0cc433c00ee658836803b0fd100747c04ab5f917704fd10 3542 | d5c1cd41ec801343d207f602a403605d86e5f9e5f9ae0d00e994556833806685 3543 | c931fb709b0f08b4e869bea5c827859549e82c544b8d29c816a0390999613920 3544 | 7e610d5727a16318c2003c1fa24be0de2b32caf92224e7c17e5004b6350c4c01 3545 | 05601218066b0ad28224e149019c086257ca315102de2712903bde97b8144d82 3546 | 3b2c6ac52d403c054e019249b087f53d0558995a99ea946c70cc927458b3c1ff 3547 | 550f30050df988d4284376b4566a8e416654cc921985e037e0df0fc131f00f4b 3548 | acf0c6211c036f14a239703741740adc7da227edd7e56b833d0ae92549b4d357 3549 | 25dfb49ed2ff63908e6adf27d6d0dda7638d4154d2778daca17f58e61297c129 3550 | 41f233b01f5dc3740cac51688c35c6b22580f48224fee9b83502569a66b629f1 3551 | 09f3713473413e2666e7fe6f6c6efefdfafda1f56f6e06f93496d9d67cb7366a 3552 | 9964b6f92e64b689196ec6c604646fd3fe4771ff1bf03f65d8ecc3addbb5f300 3553 | 00000049454e44ae426082 3554 | """), 3555 | } 3556 | 3557 | def read_pam_header(infile): 3558 | """ 3559 | Read (the rest of a) PAM header. `infile` should be positioned 3560 | immediately after the initial 'P7' line (at the beginning of the 3561 | second line). Returns are as for `read_pnm_header`. 3562 | """ 3563 | 3564 | # Unlike PBM, PGM, and PPM, we can read the header a line at a time. 3565 | header = dict() 3566 | while True: 3567 | l = infile.readline().strip() 3568 | if l == strtobytes('ENDHDR'): 3569 | break 3570 | if not l: 3571 | raise EOFError('PAM ended prematurely') 3572 | if l[0] == strtobytes('#'): 3573 | continue 3574 | l = l.split(None, 1) 3575 | if l[0] not in header: 3576 | header[l[0]] = l[1] 3577 | else: 3578 | header[l[0]] += strtobytes(' ') + l[1] 3579 | 3580 | required = ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL'] 3581 | required = [strtobytes(x) for x in required] 3582 | WIDTH,HEIGHT,DEPTH,MAXVAL = required 3583 | present = [x for x in required if x in header] 3584 | if len(present) != len(required): 3585 | raise Error('PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL') 3586 | width = int(header[WIDTH]) 3587 | height = int(header[HEIGHT]) 3588 | depth = int(header[DEPTH]) 3589 | maxval = int(header[MAXVAL]) 3590 | if (width <= 0 or 3591 | height <= 0 or 3592 | depth <= 0 or 3593 | maxval <= 0): 3594 | raise Error( 3595 | 'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers') 3596 | return 'P7', width, height, depth, maxval 3597 | 3598 | def read_pnm_header(infile, supported=('P5','P6')): 3599 | """ 3600 | Read a PNM header, returning (format,width,height,depth,maxval). 3601 | `width` and `height` are in pixels. `depth` is the number of 3602 | channels in the image; for PBM and PGM it is synthesized as 1, for 3603 | PPM as 3; for PAM images it is read from the header. `maxval` is 3604 | synthesized (as 1) for PBM images. 3605 | """ 3606 | 3607 | # Generally, see http://netpbm.sourceforge.net/doc/ppm.html 3608 | # and http://netpbm.sourceforge.net/doc/pam.html 3609 | 3610 | supported = [strtobytes(x) for x in supported] 3611 | 3612 | # Technically 'P7' must be followed by a newline, so by using 3613 | # rstrip() we are being liberal in what we accept. I think this 3614 | # is acceptable. 3615 | type = infile.read(3).rstrip() 3616 | if type not in supported: 3617 | raise NotImplementedError('file format %s not supported' % type) 3618 | if type == strtobytes('P7'): 3619 | # PAM header parsing is completely different. 3620 | return read_pam_header(infile) 3621 | # Expected number of tokens in header (3 for P4, 4 for P6) 3622 | expected = 4 3623 | pbm = ('P1', 'P4') 3624 | if type in pbm: 3625 | expected = 3 3626 | header = [type] 3627 | 3628 | # We have to read the rest of the header byte by byte because the 3629 | # final whitespace character (immediately following the MAXVAL in 3630 | # the case of P6) may not be a newline. Of course all PNM files in 3631 | # the wild use a newline at this point, so it's tempting to use 3632 | # readline; but it would be wrong. 3633 | def getc(): 3634 | c = infile.read(1) 3635 | if not c: 3636 | raise Error('premature EOF reading PNM header') 3637 | return c 3638 | 3639 | c = getc() 3640 | while True: 3641 | # Skip whitespace that precedes a token. 3642 | while c.isspace(): 3643 | c = getc() 3644 | # Skip comments. 3645 | while c == '#': 3646 | while c not in '\n\r': 3647 | c = getc() 3648 | if not c.isdigit(): 3649 | raise Error('unexpected character %s found in header' % c) 3650 | # According to the specification it is legal to have comments 3651 | # that appear in the middle of a token. 3652 | # This is bonkers; I've never seen it; and it's a bit awkward to 3653 | # code good lexers in Python (no goto). So we break on such 3654 | # cases. 3655 | token = strtobytes('') 3656 | while c.isdigit(): 3657 | token += c 3658 | c = getc() 3659 | # Slight hack. All "tokens" are decimal integers, so convert 3660 | # them here. 3661 | header.append(int(token)) 3662 | if len(header) == expected: 3663 | break 3664 | # Skip comments (again) 3665 | while c == '#': 3666 | while c not in '\n\r': 3667 | c = getc() 3668 | if not c.isspace(): 3669 | raise Error('expected header to end with whitespace, not %s' % c) 3670 | 3671 | if type in pbm: 3672 | # synthesize a MAXVAL 3673 | header.append(1) 3674 | depth = (1,3)[type == strtobytes('P6')] 3675 | return header[0], header[1], header[2], depth, header[3] 3676 | 3677 | def write_pnm(file, width, height, pixels, meta): 3678 | """Write a Netpbm PNM/PAM file.""" 3679 | 3680 | bitdepth = meta['bitdepth'] 3681 | maxval = 2**bitdepth - 1 3682 | # Rudely, the number of image planes can be used to determine 3683 | # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). 3684 | planes = meta['planes'] 3685 | # Can be an assert as long as we assume that pixels and meta came 3686 | # from a PNG file. 3687 | assert planes in (1,2,3,4) 3688 | if planes in (1,3): 3689 | if 1 == planes: 3690 | # PGM 3691 | # Could generate PBM if maxval is 1, but we don't (for one 3692 | # thing, we'd have to convert the data, not just blat it 3693 | # out). 3694 | fmt = 'P5' 3695 | else: 3696 | # PPM 3697 | fmt = 'P6' 3698 | file.write('%s %d %d %d\n' % (fmt, width, height, maxval)) 3699 | if planes in (2,4): 3700 | # PAM 3701 | # See http://netpbm.sourceforge.net/doc/pam.html 3702 | if 2 == planes: 3703 | tupltype = 'GRAYSCALE_ALPHA' 3704 | else: 3705 | tupltype = 'RGB_ALPHA' 3706 | file.write('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n' 3707 | 'TUPLTYPE %s\nENDHDR\n' % 3708 | (width, height, planes, maxval, tupltype)) 3709 | # Values per row 3710 | vpr = planes * width 3711 | # struct format 3712 | fmt = '>%d' % vpr 3713 | if maxval > 0xff: 3714 | fmt = fmt + 'H' 3715 | else: 3716 | fmt = fmt + 'B' 3717 | for row in pixels: 3718 | file.write(struct.pack(fmt, *row)) 3719 | file.flush() 3720 | 3721 | def color_triple(color): 3722 | """ 3723 | Convert a command line colour value to a RGB triple of integers. 3724 | FIXME: Somewhere we need support for greyscale backgrounds etc. 3725 | """ 3726 | if color.startswith('#') and len(color) == 4: 3727 | return (int(color[1], 16), 3728 | int(color[2], 16), 3729 | int(color[3], 16)) 3730 | if color.startswith('#') and len(color) == 7: 3731 | return (int(color[1:3], 16), 3732 | int(color[3:5], 16), 3733 | int(color[5:7], 16)) 3734 | elif color.startswith('#') and len(color) == 13: 3735 | return (int(color[1:5], 16), 3736 | int(color[5:9], 16), 3737 | int(color[9:13], 16)) 3738 | 3739 | def _add_common_options(parser): 3740 | """Call *parser.add_option* for each of the options that are 3741 | common between this PNG--PNM conversion tool and the gen 3742 | tool. 3743 | """ 3744 | parser.add_option("-i", "--interlace", 3745 | default=False, action="store_true", 3746 | help="create an interlaced PNG file (Adam7)") 3747 | parser.add_option("-t", "--transparent", 3748 | action="store", type="string", metavar="#RRGGBB", 3749 | help="mark the specified colour as transparent") 3750 | parser.add_option("-b", "--background", 3751 | action="store", type="string", metavar="#RRGGBB", 3752 | help="save the specified background colour") 3753 | parser.add_option("-g", "--gamma", 3754 | action="store", type="float", metavar="value", 3755 | help="save the specified gamma value") 3756 | parser.add_option("-c", "--compression", 3757 | action="store", type="int", metavar="level", 3758 | help="zlib compression level (0-9)") 3759 | return parser 3760 | 3761 | def _main(argv): 3762 | """ 3763 | Run the PNG encoder with options from the command line. 3764 | """ 3765 | 3766 | # Parse command line arguments 3767 | from optparse import OptionParser 3768 | import re 3769 | version = '%prog ' + re.sub(r'( ?\$|URL: |Rev:)', '', __version__) 3770 | parser = OptionParser(version=version) 3771 | parser.set_usage("%prog [options] [imagefile]") 3772 | parser.add_option('-r', '--read-png', default=False, 3773 | action='store_true', 3774 | help='Read PNG, write PNM') 3775 | parser.add_option("-a", "--alpha", 3776 | action="store", type="string", metavar="pgmfile", 3777 | help="alpha channel transparency (RGBA)") 3778 | _add_common_options(parser) 3779 | 3780 | (options, args) = parser.parse_args(args=argv[1:]) 3781 | 3782 | # Convert options 3783 | if options.transparent is not None: 3784 | options.transparent = color_triple(options.transparent) 3785 | if options.background is not None: 3786 | options.background = color_triple(options.background) 3787 | 3788 | # Prepare input and output files 3789 | if len(args) == 0: 3790 | infilename = '-' 3791 | infile = sys.stdin 3792 | elif len(args) == 1: 3793 | infilename = args[0] 3794 | infile = open(infilename, 'rb') 3795 | else: 3796 | parser.error("more than one input file") 3797 | outfile = sys.stdout 3798 | if sys.platform == "win32": 3799 | import msvcrt, os 3800 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 3801 | 3802 | if options.read_png: 3803 | # Encode PNG to PPM 3804 | png = Reader(file=infile) 3805 | width,height,pixels,meta = png.asDirect() 3806 | write_pnm(outfile, width, height, pixels, meta) 3807 | else: 3808 | # Encode PNM to PNG 3809 | format, width, height, depth, maxval = \ 3810 | read_pnm_header(infile, ('P5','P6','P7')) 3811 | # When it comes to the variety of input formats, we do something 3812 | # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour 3813 | # types supported by PNG and that they correspond to 1, 2, 3, 4 3814 | # channels respectively. So we use the number of channels in 3815 | # the source image to determine which one we have. We do not 3816 | # care about TUPLTYPE. 3817 | greyscale = depth <= 2 3818 | pamalpha = depth in (2,4) 3819 | supported = map(lambda x: 2**x-1, range(1,17)) 3820 | try: 3821 | mi = supported.index(maxval) 3822 | except ValueError: 3823 | raise NotImplementedError( 3824 | 'your maxval (%s) not in supported list %s' % 3825 | (maxval, str(supported))) 3826 | bitdepth = mi+1 3827 | writer = Writer(width, height, 3828 | greyscale=greyscale, 3829 | bitdepth=bitdepth, 3830 | interlace=options.interlace, 3831 | transparent=options.transparent, 3832 | background=options.background, 3833 | alpha=bool(pamalpha or options.alpha), 3834 | gamma=options.gamma, 3835 | compression=options.compression) 3836 | if options.alpha: 3837 | pgmfile = open(options.alpha, 'rb') 3838 | format, awidth, aheight, adepth, amaxval = \ 3839 | read_pnm_header(pgmfile, 'P5') 3840 | if amaxval != '255': 3841 | raise NotImplementedError( 3842 | 'maxval %s not supported for alpha channel' % amaxval) 3843 | if (awidth, aheight) != (width, height): 3844 | raise ValueError("alpha channel image size mismatch" 3845 | " (%s has %sx%s but %s has %sx%s)" 3846 | % (infilename, width, height, 3847 | options.alpha, awidth, aheight)) 3848 | writer.convert_ppm_and_pgm(infile, pgmfile, outfile) 3849 | else: 3850 | writer.convert_pnm(infile, outfile) 3851 | 3852 | 3853 | if __name__ == '__main__': 3854 | try: 3855 | _main(sys.argv) 3856 | except Error, e: 3857 | print >>sys.stderr, e 3858 | -------------------------------------------------------------------------------- /run_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import optparse 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | ACTIVITIES = { 9 | 'calculator': 10 | 'com.android.calculator2/.Calculator', 11 | 'clock': 12 | 'com.google.android.deskclock/com.android.deskclock.DeskClock', 13 | } 14 | 15 | def main(): 16 | parser = optparse.OptionParser(description=__doc__, 17 | usage='run_all') 18 | parser.add_option('-f', '--filter', 19 | action='append', choices=sorted(ACTIVITIES.keys())) 20 | parser.add_option('-a', '--activity', 21 | action='append', default=[]) 22 | (options, args) = parser.parse_args() 23 | ran = [] 24 | for name in sorted(ACTIVITIES.keys()): 25 | activity = ACTIVITIES[name] 26 | if options.filter and name not in options.filter: 27 | print 'Skipping ', name 28 | continue 29 | print 'Launching ', name 30 | ran += [name] 31 | subprocess.check_output(['python', 'speed_index.py', '-v', '-a', activity, 32 | '-o', os.path.join('/tmp', name)]) 33 | for activity in sorted(options.activity): 34 | name = activity.split('/')[0].split('.')[-1] 35 | print 'Launching ', name 36 | ran += [name] 37 | subprocess.check_output(['python', 'speed_index.py', '-v', '-a', activity, 38 | '-o', os.path.join('/tmp', name)]) 39 | combined = [os.path.join('/tmp', a) + '.json' for a in ran] 40 | print 'Combining...' 41 | subprocess.check_output(['python', 'json_combiner.py', 42 | '-o', '/tmp/combined'] + combined) 43 | 44 | if __name__ == '__main__': 45 | sys.exit(main()) 46 | -------------------------------------------------------------------------------- /speed_index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2014 The Chromium Authors. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """Records a video and measures the 'speed index'. 8 | See more at https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index. 9 | """ 10 | 11 | import logging 12 | import optparse 13 | import os 14 | import shlex 15 | import subprocess 16 | import sys 17 | import tempfile 18 | import time 19 | 20 | import bitmap 21 | import html_graph 22 | import video 23 | 24 | 25 | class SpeedIndex(object): 26 | _VIDEO_FILE_NAME = 'speed_index.mp4' 27 | _DEVICE_VIDEO_FILE_NAME = '/sdcard/' + _VIDEO_FILE_NAME 28 | _BITRATE = str(4 * 1000 * 1000) 29 | _WHITE = bitmap.RgbaColor(255, 255, 255) 30 | 31 | def __init__(self, cmd, activity_kill, wait, output): 32 | self._cmd = cmd 33 | self._activity_kill = activity_kill 34 | self._wait = wait 35 | self._video_capture_pid = None 36 | self._html_graph = html_graph.HTMLGraph() 37 | self._output = output 38 | 39 | def _StartVideoCapture(self): 40 | self._KillPending() 41 | subprocess.call(['adb', 'shell', 'echo 3 > ' 42 | '/proc/sys/vm/drop_caches']) 43 | video_capture = subprocess.Popen(['adb', 'shell', 'screenrecord', 44 | '--bit-rate', self._BITRATE, 45 | self._DEVICE_VIDEO_FILE_NAME]) 46 | # Wait a bit for the screenrecord to kick in. 47 | time.sleep(0.5) 48 | video_capture = subprocess.Popen([ 49 | 'adb', 'shell', 'am', 'start', '-S', 50 | 'com.google.android.apps.appspeedindex/.BoundingBox']) 51 | time.sleep(1.0) 52 | video_capture_pid = subprocess.check_output(['adb', 'shell', 'ps', 53 | 'screenrecord']) 54 | video_capture_pid = video_capture_pid.replace('\r', '').splitlines() 55 | self._video_capture_pid = video_capture_pid[-1].split()[1] 56 | 57 | def _RunCommandAndWait(self): 58 | print self._cmd 59 | start_cmd = subprocess.Popen(shlex.split(self._cmd)) 60 | start_cmd.wait() 61 | time.sleep(self._wait) 62 | 63 | def _StopVideoCapture(self): 64 | subprocess.check_output(['adb', 'shell', 'kill', '-SIGINT', 65 | self._video_capture_pid]) 66 | # Wait a bit for the screenrecord to flush the file. 67 | time.sleep(1.0) 68 | subprocess.check_output(['adb', 'pull', self._DEVICE_VIDEO_FILE_NAME]) 69 | self._KillPending() 70 | 71 | def _KillPending(self): 72 | if self._activity_kill: 73 | subprocess.check_output(shlex.split(self._activity_kill)) 74 | subprocess.check_output([ 75 | 'adb', 'shell', 'am', 'force-stop', 76 | 'com.google.android.speed_index']) 77 | 78 | def _Calculate(self): 79 | video_capture = video.Video(self._VIDEO_FILE_NAME) 80 | histograms = [(time, bmp.ColorHistogram(ignore_color=self._WHITE, 81 | tolerance=8)) 82 | for time, bmp in video_capture.GetVideoFrameIter()] 83 | 84 | start_histogram = histograms[0][1] 85 | final_histogram = histograms[-1][1] 86 | total_distance = start_histogram.Distance(final_histogram) 87 | 88 | def FrameProgress(histogram): 89 | if total_distance == 0: 90 | if histogram.Distance(final_histogram) == 0: 91 | return 1.0 92 | else: 93 | return 0.0 94 | return 1 - histogram.Distance(final_histogram) / total_distance 95 | 96 | time_completeness_list = [(time, FrameProgress(hist)) 97 | for time, hist in histograms] 98 | return time_completeness_list 99 | 100 | def Run(self): 101 | logging.info('Starting video capture...') 102 | self._StartVideoCapture() 103 | logging.info('Starting app...') 104 | self._RunCommandAndWait() 105 | logging.info('Downloading video...') 106 | self._StopVideoCapture() 107 | logging.info('Analyzing video...') 108 | data = self._Calculate() 109 | self._html_graph.GenerateGraph(self._output, self._cmd, 110 | ['Time', '%Visual Complete'], data) 111 | 112 | 113 | def main(): 114 | parser = optparse.OptionParser(description=__doc__, 115 | usage='speed_index') 116 | parser.add_option('-c', '--cmd', help='ADB Shell command to run.') 117 | parser.add_option('-a', '--activity', help='Activity to launch.') 118 | parser.add_option('-w', '--wait', help='Wait for N seconds.', 119 | type='int', default=5) 120 | parser.add_option('-o', '--output', help='Output report', 121 | default='speed_index.html') 122 | parser.add_option('-v', '--verbose', help='Verbose logging.', 123 | action='store_true') 124 | (options, args) = parser.parse_args() 125 | if options.verbose: 126 | logging.getLogger().setLevel(logging.DEBUG) 127 | if options.cmd and options.activity: 128 | sys.exit('--cmd and --activity are mutually exclusive.') 129 | if not options.cmd and not options.activity: 130 | sys.exit('Specify one of --cmd or --activity.') 131 | cmd = options.cmd 132 | activity_kill = None 133 | if not cmd: 134 | cmd = 'adb shell am start -S ' + options.activity 135 | activity_kill = ('adb shell am force-stop ' + 136 | options.activity[:options.activity.index('/')]) 137 | speed_index = SpeedIndex(cmd, activity_kill, options.wait, options.output) 138 | speed_index.Run() 139 | 140 | 141 | if __name__ == '__main__': 142 | sys.exit(main()) 143 | -------------------------------------------------------------------------------- /video.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 The Chromium Authors. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | import subprocess 6 | 7 | import bitmap 8 | 9 | HIGHLIGHT_FRAME = bitmap.RgbaColor(0, 255, 0) 10 | 11 | class BoundingBoxNotFoundException(Exception): 12 | pass 13 | 14 | class Video(object): 15 | """Utilities for storing and interacting with the video capture.""" 16 | 17 | def __init__(self, video_file_path): 18 | self._video_file_path = video_file_path 19 | 20 | def GetVideoFrameIter(self): 21 | """Returns the iteration for processing the video capture. 22 | 23 | This looks for the initial color flash in the first frame to establish the 24 | ContentView boundaries and then omits all frames displaying the flash. 25 | 26 | Yields: 27 | (time_ms, bitmap) tuples representing each video keyframe. Only the first 28 | frame in a run of sequential duplicate bitmaps is typically included. 29 | time_ms is milliseconds since navigationStart. 30 | bitmap is a telemetry.core.Bitmap. 31 | """ 32 | frame_generator = self._FramesFromMp4(self._video_file_path) 33 | 34 | # Flip through frames until we find the initial ContentView flash. 35 | content_box = None 36 | for _, bmp in frame_generator: 37 | content_box = self._FindHighlightBoundingBox( 38 | bmp, HIGHLIGHT_FRAME) 39 | if content_box: 40 | break 41 | 42 | if not content_box: 43 | raise BoundingBoxNotFoundException( 44 | 'Failed to identify ContentView in video capture.') 45 | 46 | # Flip through frames until the flash goes away and emit that as frame 0. 47 | timestamp = 0 48 | for timestamp, bmp in frame_generator: 49 | if not self._FindHighlightBoundingBox(bmp, HIGHLIGHT_FRAME): 50 | yield 0, bmp.Crop(*content_box) 51 | break 52 | 53 | start_time = timestamp 54 | for timestamp, bmp in frame_generator: 55 | yield timestamp - start_time, bmp.Crop(*content_box) 56 | 57 | def _FindHighlightBoundingBox(self, bmp, color, bounds_tolerance=8, 58 | color_tolerance=8): 59 | """Returns the bounding box of the content highlight of the given color. 60 | 61 | Raises: 62 | BoundingBoxNotFoundException if the highlight could not be found. 63 | """ 64 | content_box, pixel_count = bmp.GetBoundingBox(color, 65 | tolerance=color_tolerance) 66 | 67 | if not content_box: 68 | return None 69 | 70 | # We assume arbitrarily that ContentView are all larger than 200x200. If 71 | # this fails it either means that assumption has changed or something is 72 | # awry with our bounding box calculation. 73 | if content_box[2] < 200 or content_box[3] < 200: 74 | return None 75 | 76 | if pixel_count < 0.9 * content_box[2] * content_box[3]: 77 | return None 78 | 79 | return content_box 80 | 81 | def _FramesFromMp4(self, mp4_file): 82 | def GetDimensions(video): 83 | proc = subprocess.Popen(['avconv', '-i', video], stderr=subprocess.PIPE) 84 | dimensions = None 85 | output = '' 86 | for line in proc.stderr.readlines(): 87 | output += line 88 | if 'Video:' in line: 89 | dimensions = line.split(',')[2] 90 | dimensions = map(int, dimensions.split()[0].split('x')) 91 | break 92 | proc.communicate() 93 | assert dimensions, ('Failed to determine video dimensions. output=%s' % 94 | output) 95 | return dimensions 96 | 97 | def GetFrameTimestampMs(stderr): 98 | """Returns the frame timestamp in integer milliseconds from the dump log. 99 | 100 | The expected line format is: 101 | ' dts=1.715 pts=1.715\n' 102 | 103 | We have to be careful to only read a single timestamp per call to avoid 104 | deadlock because avconv interleaves its writes to stdout and stderr. 105 | """ 106 | while True: 107 | line = '' 108 | next_char = '' 109 | while next_char != '\n': 110 | next_char = stderr.read(1) 111 | line += next_char 112 | if 'pts=' in line: 113 | return int(1000 * float(line.split('=')[-1])) 114 | 115 | dimensions = GetDimensions(mp4_file) 116 | frame_length = dimensions[0] * dimensions[1] * 3 117 | frame_data = bytearray(frame_length) 118 | 119 | # Use rawvideo so that we don't need any external library to parse frames. 120 | proc = subprocess.Popen(['avconv', '-i', mp4_file, '-vcodec', 121 | 'rawvideo', '-pix_fmt', 'rgb24', '-dump', 122 | '-loglevel', 'debug', '-f', 'rawvideo', '-'], 123 | stderr=subprocess.PIPE, stdout=subprocess.PIPE) 124 | while True: 125 | num_read = proc.stdout.readinto(frame_data) 126 | if not num_read: 127 | raise StopIteration 128 | assert num_read == len(frame_data), 'Unexpected frame size: %d' % num_read 129 | yield (GetFrameTimestampMs(proc.stderr), 130 | bitmap.Bitmap(3, dimensions[0], dimensions[1], frame_data)) 131 | --------------------------------------------------------------------------------