~manuq/+junk/manuqs_interactives

« back to all changes in this revision

Viewing changes to casa_tomada_2/pyglet/image/codecs/pypng.py

  • Committer: Manuel Quiñones
  • Date: 2008-07-07 06:48:54 UTC
  • Revision ID: manuel.por.aca@gmail.com-20080707064854-0dpwpz7at6atdne3
Import of casa_tomada_2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ----------------------------------------------------------------------------
 
2
# pyglet
 
3
# Copyright (c) 2006-2008 Alex Holkner
 
4
# All rights reserved.
 
5
 
6
# Redistribution and use in source and binary forms, with or without
 
7
# modification, are permitted provided that the following conditions 
 
8
# are met:
 
9
#
 
10
#  * Redistributions of source code must retain the above copyright
 
11
#    notice, this list of conditions and the following disclaimer.
 
12
#  * Redistributions in binary form must reproduce the above copyright 
 
13
#    notice, this list of conditions and the following disclaimer in
 
14
#    the documentation and/or other materials provided with the
 
15
#    distribution.
 
16
#  * Neither the name of pyglet nor the names of its
 
17
#    contributors may be used to endorse or promote products
 
18
#    derived from this software without specific prior written
 
19
#    permission.
 
20
#
 
21
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 
22
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 
23
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 
24
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 
25
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 
26
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 
27
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 
28
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 
29
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 
30
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 
31
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 
32
# POSSIBILITY OF SUCH DAMAGE.
 
33
# ----------------------------------------------------------------------------
 
34
# png.py - PNG encoder in pure Python
 
35
# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
 
36
# <ah> Modifications for pyglet by Alex Holkner <alex.holkner@gmail.com> 
 
37
#
 
38
# Permission is hereby granted, free of charge, to any person
 
39
# obtaining a copy of this software and associated documentation files
 
40
# (the "Software"), to deal in the Software without restriction,
 
41
# including without limitation the rights to use, copy, modify, merge,
 
42
# publish, distribute, sublicense, and/or sell copies of the Software,
 
43
# and to permit persons to whom the Software is furnished to do so,
 
44
# subject to the following conditions:
 
45
#
 
46
# The above copyright notice and this permission notice shall be
 
47
# included in all copies or substantial portions of the Software.
 
48
#
 
49
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 
50
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 
51
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 
52
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 
53
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 
54
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 
55
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 
56
# SOFTWARE.
 
57
#
 
58
# Contributors (alphabetical):
 
59
# Nicko van Someren <nicko@nicko.org>
 
60
#
 
61
# Changelog (recent first):
 
62
# 2006-06-17 Nicko: Reworked into a class, faster interlacing.
 
63
# 2006-06-17 Johann: Very simple prototype PNG decoder.
 
64
# 2006-06-17 Nicko: Test suite with various image generators.
 
65
# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support.
 
66
# 2006-06-15 Johann: Scanline iterator interface for large input files.
 
67
# 2006-06-09 Johann: Very simple prototype PNG encoder.
 
68
 
 
69
 
 
70
"""
 
71
Pure Python PNG Reader/Writer
 
72
 
 
73
This is an implementation of a subset of the PNG specification at
 
74
http://www.w3.org/TR/2003/REC-PNG-20031110 in pure Python. It reads
 
75
and writes PNG files with 8/16/24/32/48/64 bits per pixel (greyscale,
 
76
RGB, RGBA, with 8 or 16 bits per layer), with a number of options. For
 
77
help, type "import png; help(png)" in your python interpreter.
 
78
 
 
79
This file can also be used as a command-line utility to convert PNM
 
80
files to PNG. The interface is similar to that of the pnmtopng program
 
81
from the netpbm package. Type "python png.py --help" at the shell
 
82
prompt for usage and a list of options.
 
83
"""
 
84
 
 
85
 
 
86
__revision__ = '$Rev$'
 
87
__date__ = '$Date$'
 
88
__author__ = '$Author$'
 
89
 
 
90
 
 
91
import sys
 
92
import zlib
 
93
import struct
 
94
import math
 
95
from array import array
 
96
 
 
97
 
 
98
_adam7 = ((0, 0, 8, 8),
 
99
          (4, 0, 8, 8),
 
100
          (0, 4, 4, 8),
 
101
          (2, 0, 4, 4),
 
102
          (0, 2, 2, 4),
 
103
          (1, 0, 2, 2),
 
104
          (0, 1, 1, 2))
 
105
 
 
106
 
 
107
def interleave_planes(ipixels, apixels, ipsize, apsize):
 
108
    """
 
109
    Interleave color planes, e.g. RGB + A = RGBA.
 
110
 
 
111
    Return an array of pixels consisting of the ipsize bytes of data
 
112
    from each pixel in ipixels followed by the apsize bytes of data
 
113
    from each pixel in apixels, for an image of size width x height.
 
114
    """
 
115
    itotal = len(ipixels)
 
116
    atotal = len(apixels)
 
117
    newtotal = itotal + atotal
 
118
    newpsize = ipsize + apsize
 
119
    # Set up the output buffer
 
120
    out = array('B')
 
121
    # It's annoying that there is no cheap way to set the array size :-(
 
122
    out.extend(ipixels)
 
123
    out.extend(apixels)
 
124
    # Interleave in the pixel data
 
125
    for i in range(ipsize):
 
126
        out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize]
 
127
    for i in range(apsize):
 
128
        out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize]
 
129
    return out
 
130
 
 
131
class Error(Exception):
 
132
    pass
 
133
 
 
134
class Writer:
 
135
    """
 
136
    PNG encoder in pure Python.
 
137
    """
 
138
 
 
139
    def __init__(self, width, height,
 
140
                 transparent=None,
 
141
                 background=None,
 
142
                 gamma=None,
 
143
                 greyscale=False,
 
144
                 has_alpha=False,
 
145
                 bytes_per_sample=1,
 
146
                 compression=None,
 
147
                 interlaced=False,
 
148
                 chunk_limit=2**20):
 
149
        """
 
150
        Create a PNG encoder object.
 
151
 
 
152
        Arguments:
 
153
        width, height - size of the image in pixels
 
154
        transparent - create a tRNS chunk
 
155
        background - create a bKGD chunk
 
156
        gamma - create a gAMA chunk
 
157
        greyscale - input data is greyscale, not RGB
 
158
        has_alpha - input data has alpha channel (RGBA)
 
159
        bytes_per_sample - 8-bit or 16-bit input data
 
160
        compression - zlib compression level (1-9)
 
161
        chunk_limit - write multiple IDAT chunks to save memory
 
162
 
 
163
        If specified, the transparent and background parameters must
 
164
        be a tuple with three integer values for red, green, blue, or
 
165
        a simple integer (or singleton tuple) for a greyscale image.
 
166
 
 
167
        If specified, the gamma parameter must be a float value.
 
168
 
 
169
        """
 
170
        if width <= 0 or height <= 0:
 
171
            raise ValueError("width and height must be greater than zero")
 
172
 
 
173
        if has_alpha and transparent is not None:
 
174
            raise ValueError(
 
175
                "transparent color not allowed with alpha channel")
 
176
 
 
177
        if bytes_per_sample < 1 or bytes_per_sample > 2:
 
178
            raise ValueError("bytes per sample must be 1 or 2")
 
179
 
 
180
        if transparent is not None:
 
181
            if greyscale:
 
182
                if type(transparent) is not int:
 
183
                    raise ValueError(
 
184
                        "transparent color for greyscale must be integer")
 
185
            else:
 
186
                if not (len(transparent) == 3 and
 
187
                        type(transparent[0]) is int and
 
188
                        type(transparent[1]) is int and
 
189
                        type(transparent[2]) is int):
 
190
                    raise ValueError(
 
191
                        "transparent color must be a triple of integers")
 
192
 
 
193
        if background is not None:
 
194
            if greyscale:
 
195
                if type(background) is not int:
 
196
                    raise ValueError(
 
197
                        "background color for greyscale must be integer")
 
198
            else:
 
199
                if not (len(background) == 3 and
 
200
                        type(background[0]) is int and
 
201
                        type(background[1]) is int and
 
202
                        type(background[2]) is int):
 
203
                    raise ValueError(
 
204
                        "background color must be a triple of integers")
 
205
 
 
206
        self.width = width
 
207
        self.height = height
 
208
        self.transparent = transparent
 
209
        self.background = background
 
210
        self.gamma = gamma
 
211
        self.greyscale = greyscale
 
212
        self.has_alpha = has_alpha
 
213
        self.bytes_per_sample = bytes_per_sample
 
214
        self.compression = compression
 
215
        self.chunk_limit = chunk_limit
 
216
        self.interlaced = interlaced
 
217
 
 
218
        if self.greyscale:
 
219
            self.color_depth = 1
 
220
            if self.has_alpha:
 
221
                self.color_type = 4
 
222
                self.psize = self.bytes_per_sample * 2
 
223
            else:
 
224
                self.color_type = 0
 
225
                self.psize = self.bytes_per_sample
 
226
        else:
 
227
            self.color_depth = 3
 
228
            if self.has_alpha:
 
229
                self.color_type = 6
 
230
                self.psize = self.bytes_per_sample * 4
 
231
            else:
 
232
                self.color_type = 2
 
233
                self.psize = self.bytes_per_sample * 3
 
234
 
 
235
    def write_chunk(self, outfile, tag, data):
 
236
        """
 
237
        Write a PNG chunk to the output file, including length and checksum.
 
238
        """
 
239
        # http://www.w3.org/TR/PNG/#5Chunk-layout
 
240
        outfile.write(struct.pack("!I", len(data)))
 
241
        outfile.write(tag)
 
242
        outfile.write(data)
 
243
        checksum = zlib.crc32(tag)
 
244
        checksum = zlib.crc32(data, checksum)
 
245
        # <ah> Avoid DeprecationWarning: struct integer overflow masking
 
246
        #      with Python2.5/Windows.
 
247
        checksum = checksum & 0xffffffff
 
248
        outfile.write(struct.pack("!I", checksum))
 
249
 
 
250
    def write(self, outfile, scanlines):
 
251
        """
 
252
        Write a PNG image to the output file.
 
253
        """
 
254
        # http://www.w3.org/TR/PNG/#5PNG-file-signature
 
255
        outfile.write(struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10))
 
256
 
 
257
        # http://www.w3.org/TR/PNG/#11IHDR
 
258
        if self.interlaced:
 
259
            interlaced = 1
 
260
        else:
 
261
            interlaced = 0
 
262
        self.write_chunk(outfile, 'IHDR',
 
263
                         struct.pack("!2I5B", self.width, self.height,
 
264
                                     self.bytes_per_sample * 8,
 
265
                                     self.color_type, 0, 0, interlaced))
 
266
 
 
267
        # http://www.w3.org/TR/PNG/#11tRNS
 
268
        if self.transparent is not None:
 
269
            if self.greyscale:
 
270
                self.write_chunk(outfile, 'tRNS',
 
271
                                 struct.pack("!1H", *self.transparent))
 
272
            else:
 
273
                self.write_chunk(outfile, 'tRNS',
 
274
                                 struct.pack("!3H", *self.transparent))
 
275
 
 
276
        # http://www.w3.org/TR/PNG/#11bKGD
 
277
        if self.background is not None:
 
278
            if self.greyscale:
 
279
                self.write_chunk(outfile, 'bKGD',
 
280
                                 struct.pack("!1H", *self.background))
 
281
            else:
 
282
                self.write_chunk(outfile, 'bKGD',
 
283
                                 struct.pack("!3H", *self.background))
 
284
 
 
285
        # http://www.w3.org/TR/PNG/#11gAMA
 
286
        if self.gamma is not None:
 
287
            self.write_chunk(outfile, 'gAMA',
 
288
                             struct.pack("!L", int(self.gamma * 100000)))
 
289
 
 
290
        # http://www.w3.org/TR/PNG/#11IDAT
 
291
        if self.compression is not None:
 
292
            compressor = zlib.compressobj(self.compression)
 
293
        else:
 
294
            compressor = zlib.compressobj()
 
295
 
 
296
        data = array('B')
 
297
        for scanline in scanlines:
 
298
            data.append(0)
 
299
            data.extend(scanline)
 
300
            if len(data) > self.chunk_limit:
 
301
                compressed = compressor.compress(data.tostring())
 
302
                if len(compressed):
 
303
                    # print >> sys.stderr, len(data), len(compressed)
 
304
                    self.write_chunk(outfile, 'IDAT', compressed)
 
305
                data = array('B')
 
306
        if len(data):
 
307
            compressed = compressor.compress(data.tostring())
 
308
        else:
 
309
            compressed = ''
 
310
        flushed = compressor.flush()
 
311
        if len(compressed) or len(flushed):
 
312
            # print >> sys.stderr, len(data), len(compressed), len(flushed)
 
313
            self.write_chunk(outfile, 'IDAT', compressed + flushed)
 
314
 
 
315
        # http://www.w3.org/TR/PNG/#11IEND
 
316
        self.write_chunk(outfile, 'IEND', '')
 
317
 
 
318
    def write_array(self, outfile, pixels):
 
319
        """
 
320
        Encode a pixel array to PNG and write output file.
 
321
        """
 
322
        if self.interlaced:
 
323
            self.write(outfile, self.array_scanlines_interlace(pixels))
 
324
        else:
 
325
            self.write(outfile, self.array_scanlines(pixels))
 
326
 
 
327
    def convert_ppm(self, ppmfile, outfile):
 
328
        """
 
329
        Convert a PPM file containing raw pixel data into a PNG file
 
330
        with the parameters set in the writer object.
 
331
        """
 
332
        if self.interlaced:
 
333
            pixels = array('B')
 
334
            pixels.fromfile(ppmfile,
 
335
                            self.bytes_per_sample * self.color_depth *
 
336
                            self.width * self.height)
 
337
            self.write(outfile, self.array_scanlines_interlace(pixels))
 
338
        else:
 
339
            self.write(outfile, self.file_scanlines(ppmfile))
 
340
 
 
341
    def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile):
 
342
        """
 
343
        Convert a PPM and PGM file containing raw pixel data into a
 
344
        PNG outfile with the parameters set in the writer object.
 
345
        """
 
346
        pixels = array('B')
 
347
        pixels.fromfile(ppmfile,
 
348
                        self.bytes_per_sample * self.color_depth *
 
349
                        self.width * self.height)
 
350
        apixels = array('B')
 
351
        apixels.fromfile(pgmfile,
 
352
                         self.bytes_per_sample *
 
353
                         self.width * self.height)
 
354
        pixels = interleave_planes(pixels, apixels,
 
355
                                   self.bytes_per_sample * self.color_depth,
 
356
                                   self.bytes_per_sample)
 
357
        if self.interlaced:
 
358
            self.write(outfile, self.array_scanlines_interlace(pixels))
 
359
        else:
 
360
            self.write(outfile, self.array_scanlines(pixels))
 
361
 
 
362
    def file_scanlines(self, infile):
 
363
        """
 
364
        Generator for scanlines from an input file.
 
365
        """
 
366
        row_bytes = self.psize * self.width
 
367
        for y in range(self.height):
 
368
            scanline = array('B')
 
369
            scanline.fromfile(infile, row_bytes)
 
370
            yield scanline
 
371
 
 
372
    def array_scanlines(self, pixels):
 
373
        """
 
374
        Generator for scanlines from an array.
 
375
        """
 
376
        row_bytes = self.width * self.psize
 
377
        stop = 0
 
378
        for y in range(self.height):
 
379
            start = stop
 
380
            stop = start + row_bytes
 
381
            yield pixels[start:stop]
 
382
 
 
383
    def old_array_scanlines_interlace(self, pixels):
 
384
        """
 
385
        Generator for interlaced scanlines from an array.
 
386
        http://www.w3.org/TR/PNG/#8InterlaceMethods
 
387
        """
 
388
        row_bytes = self.psize * self.width
 
389
        for xstart, ystart, xstep, ystep in _adam7:
 
390
            for y in range(ystart, self.height, ystep):
 
391
                if xstart < self.width:
 
392
                    if xstep == 1:
 
393
                        offset = y*row_bytes
 
394
                        yield pixels[offset:offset+row_bytes]
 
395
                    else:
 
396
                        row = array('B')
 
397
                        offset = y*row_bytes + xstart* self.psize
 
398
                        skip = self.psize * xstep
 
399
                        for x in range(xstart, self.width, xstep):
 
400
                            row.extend(pixels[offset:offset + self.psize])
 
401
                            offset += skip
 
402
                        yield row
 
403
 
 
404
    def array_scanlines_interlace(self, pixels):
 
405
        """
 
406
        Generator for interlaced scanlines from an array.
 
407
        http://www.w3.org/TR/PNG/#8InterlaceMethods
 
408
        """
 
409
        row_bytes = self.psize * self.width
 
410
        for xstart, ystart, xstep, ystep in _adam7:
 
411
            for y in range(ystart, self.height, ystep):
 
412
                if xstart >= self.width:
 
413
                    continue
 
414
                if xstep == 1:
 
415
                    offset = y * row_bytes
 
416
                    yield pixels[offset:offset+row_bytes]
 
417
                else:
 
418
                    row = array('B')
 
419
                    # Note we want the ceiling of (self.width - xstart) / xtep
 
420
                    row_len = self.psize * (
 
421
                        (self.width - xstart + xstep - 1) / xstep)
 
422
                    # There's no easier way to set the length of an array
 
423
                    row.extend(pixels[0:row_len])
 
424
                    offset = y * row_bytes + xstart * self.psize
 
425
                    end_offset = (y+1) * row_bytes
 
426
                    skip = self.psize * xstep
 
427
                    for i in range(self.psize):
 
428
                        row[i:row_len:self.psize] = \
 
429
                            pixels[offset+i:end_offset:skip]
 
430
                    yield row
 
431
 
 
432
class _readable:
 
433
    """
 
434
    A simple file-like interface for strings and arrays.
 
435
    """
 
436
 
 
437
    def __init__(self, buf):
 
438
        self.buf = buf
 
439
        self.offset = 0
 
440
 
 
441
    def read(self, n):
 
442
        r = buf[offset:offset+n]
 
443
        if isinstance(r, array):
 
444
            r = r.tostring()
 
445
        offset += n
 
446
        return r
 
447
 
 
448
class Reader:
 
449
    """
 
450
    PNG decoder in pure Python.
 
451
    """
 
452
 
 
453
    def __init__(self, _guess=None, **kw):
 
454
        """
 
455
        Create a PNG decoder object.
 
456
 
 
457
        The constructor expects exactly one keyword argument. If you
 
458
        supply a positional argument instead, it will guess the input
 
459
        type. You can choose among the following arguments:
 
460
        filename - name of PNG input file
 
461
        file - object with a read() method
 
462
        pixels - array or string with PNG data
 
463
 
 
464
        """
 
465
        if ((_guess is not None and len(kw) != 0) or
 
466
            (_guess is None and len(kw) != 1)):
 
467
            raise TypeError("Reader() takes exactly 1 argument")
 
468
 
 
469
        if _guess is not None:
 
470
            if isinstance(_guess, array):
 
471
                kw["pixels"] = _guess
 
472
            elif isinstance(_guess, str):
 
473
                kw["filename"] = _guess
 
474
            elif isinstance(_guess, file):
 
475
                kw["file"] = _guess
 
476
 
 
477
        if "filename" in kw:
 
478
            self.file = file(kw["filename"])
 
479
        elif "file" in kw:
 
480
            self.file = kw["file"]
 
481
        elif "pixels" in kw:
 
482
            self.file = _readable(kw["pixels"])
 
483
        else:
 
484
            raise TypeError("expecting filename, file or pixels array")
 
485
 
 
486
    def read_chunk(self):
 
487
        """
 
488
        Read a PNG chunk from the input file, return tag name and data.
 
489
        """
 
490
        # http://www.w3.org/TR/PNG/#5Chunk-layout
 
491
        try:
 
492
            data_bytes, tag = struct.unpack('!I4s', self.file.read(8))
 
493
        except struct.error:
 
494
            raise ValueError('Chunk too short for header')
 
495
        data = self.file.read(data_bytes)
 
496
        if len(data) != data_bytes:
 
497
            raise ValueError('Chunk %s too short for required %i data octets'
 
498
                             % (tag, data_bytes))
 
499
        checksum = self.file.read(4)
 
500
        if len(checksum) != 4:
 
501
            raise ValueError('Chunk %s too short for checksum', tag)
 
502
        verify = zlib.crc32(tag)
 
503
        verify = zlib.crc32(data, verify)
 
504
        verify = struct.pack('!i', verify)
 
505
        if checksum != verify:
 
506
            # print repr(checksum)
 
507
            (a,) = struct.unpack('!I', checksum)
 
508
            (b,) = struct.unpack('!I', verify)
 
509
            raise ValueError("Checksum error in %s chunk: 0x%X != 0x%X"
 
510
                             % (tag, a, b))
 
511
        return tag, data
 
512
 
 
513
    def _reconstruct_sub(self, offset, xstep, ystep):
 
514
        """
 
515
        Reverse sub filter.
 
516
        """
 
517
        pixels = self.pixels
 
518
        a_offset = offset
 
519
        offset += self.psize * xstep
 
520
        if xstep == 1:
 
521
            for index in range(self.psize, self.row_bytes):
 
522
                x = pixels[offset]
 
523
                a = pixels[a_offset]
 
524
                pixels[offset] = (x + a) & 0xff
 
525
                offset += 1
 
526
                a_offset += 1
 
527
        else:
 
528
            byte_step = self.psize * xstep
 
529
            for index in range(byte_step, self.row_bytes, byte_step):
 
530
                for i in range(self.psize):
 
531
                    x = pixels[offset + i]
 
532
                    a = pixels[a_offset + i]
 
533
                    pixels[offset + i] = (x + a) & 0xff
 
534
                offset += self.psize * xstep
 
535
                a_offset += self.psize * xstep
 
536
 
 
537
    def _reconstruct_up(self, offset, xstep, ystep):
 
538
        """
 
539
        Reverse up filter.
 
540
        """
 
541
        pixels = self.pixels
 
542
        b_offset = offset - (self.row_bytes * ystep)
 
543
        if xstep == 1:
 
544
            for index in range(self.row_bytes):
 
545
                x = pixels[offset]
 
546
                b = pixels[b_offset]
 
547
                pixels[offset] = (x + b) & 0xff
 
548
                offset += 1
 
549
                b_offset += 1
 
550
        else:
 
551
            for index in range(0, self.row_bytes, xstep * self.psize):
 
552
                for i in range(self.psize):
 
553
                    x = pixels[offset + i]
 
554
                    b = pixels[b_offset + i]
 
555
                    pixels[offset + i] = (x + b) & 0xff
 
556
                offset += self.psize * xstep
 
557
                b_offset += self.psize * xstep
 
558
 
 
559
    def _reconstruct_average(self, offset, xstep, ystep):
 
560
        """
 
561
        Reverse average filter.
 
562
        """
 
563
        pixels = self.pixels
 
564
        a_offset = offset - (self.psize * xstep)
 
565
        b_offset = offset - (self.row_bytes * ystep)
 
566
        if xstep == 1:
 
567
            for index in range(self.row_bytes):
 
568
                x = pixels[offset]
 
569
                if index < self.psize:
 
570
                    a = 0
 
571
                else:
 
572
                    a = pixels[a_offset]
 
573
                if b_offset < 0:
 
574
                    b = 0
 
575
                else:
 
576
                    b = pixels[b_offset]
 
577
                pixels[offset] = (x + ((a + b) >> 1)) & 0xff
 
578
                offset += 1
 
579
                a_offset += 1
 
580
                b_offset += 1
 
581
        else:
 
582
            for index in range(0, self.row_bytes, self.psize * xstep):
 
583
                for i in range(self.psize):
 
584
                    x = pixels[offset+i]
 
585
                    if index < self.psize:
 
586
                        a = 0
 
587
                    else:
 
588
                        a = pixels[a_offset + i]
 
589
                    if b_offset < 0:
 
590
                        b = 0
 
591
                    else:
 
592
                        b = pixels[b_offset + i]
 
593
                    pixels[offset + i] = (x + ((a + b) >> 1)) & 0xff
 
594
                offset += self.psize * xstep
 
595
                a_offset += self.psize * xstep
 
596
                b_offset += self.psize * xstep
 
597
 
 
598
    def _reconstruct_paeth(self, offset, xstep, ystep):
 
599
        """
 
600
        Reverse Paeth filter.
 
601
        """
 
602
        pixels = self.pixels
 
603
        a_offset = offset - (self.psize * xstep)
 
604
        b_offset = offset - (self.row_bytes * ystep)
 
605
        c_offset = b_offset - (self.psize * xstep)
 
606
        # There's enough inside this loop that it's probably not worth
 
607
        # optimising for xstep == 1
 
608
        for index in range(0, self.row_bytes, self.psize * xstep):
 
609
            for i in range(self.psize):
 
610
                x = pixels[offset+i]
 
611
                if index < self.psize:
 
612
                    a = c = 0
 
613
                    b = pixels[b_offset+i]
 
614
                else:
 
615
                    a = pixels[a_offset+i]
 
616
                    b = pixels[b_offset+i]
 
617
                    c = pixels[c_offset+i]
 
618
                p = a + b - c
 
619
                pa = abs(p - a)
 
620
                pb = abs(p - b)
 
621
                pc = abs(p - c)
 
622
                if pa <= pb and pa <= pc:
 
623
                    pr = a
 
624
                elif pb <= pc:
 
625
                    pr = b
 
626
                else:
 
627
                    pr = c
 
628
                pixels[offset+i] = (x + pr) & 0xff
 
629
            offset += self.psize * xstep
 
630
            a_offset += self.psize * xstep
 
631
            b_offset += self.psize * xstep
 
632
            c_offset += self.psize * xstep
 
633
 
 
634
    # N.B. PNG files with 'up', 'average' or 'paeth' filters on the
 
635
    # first line of a pass are legal. The code above for 'average'
 
636
    # deals with this case explicitly. For up we map to the null
 
637
    # filter and for paeth we map to the sub filter.
 
638
 
 
639
    def reconstruct_line(self, filter_type, first_line, offset, xstep, ystep):
 
640
        # print >> sys.stderr, "Filter type %s, first_line=%s" % (
 
641
        #                      filter_type, first_line)
 
642
        filter_type += (first_line << 8)
 
643
        if filter_type == 1 or filter_type == 0x101 or filter_type == 0x104:
 
644
            self._reconstruct_sub(offset, xstep, ystep)
 
645
        elif filter_type == 2:
 
646
            self._reconstruct_up(offset, xstep, ystep)
 
647
        elif filter_type == 3 or filter_type == 0x103:
 
648
            self._reconstruct_average(offset, xstep, ystep)
 
649
        elif filter_type == 4:
 
650
            self._reconstruct_paeth(offset, xstep, ystep)
 
651
        return
 
652
 
 
653
    def deinterlace(self, scanlines):
 
654
        # print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," +
 
655
        #     " bpp=%s") % (self.width, self.height, self.planes, self.bps)
 
656
        a = array('B')
 
657
        self.pixels = a
 
658
        # Make the array big enough
 
659
        temp = scanlines[0:self.width*self.height*self.psize]
 
660
        a.extend(temp)
 
661
        source_offset = 0
 
662
        for xstart, ystart, xstep, ystep in _adam7:
 
663
            # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % (
 
664
            #     xstart, ystart, xstep, ystep)
 
665
            filter_first_line = 1
 
666
            for y in range(ystart, self.height, ystep):
 
667
                if xstart >= self.width:
 
668
                    continue
 
669
                filter_type = scanlines[source_offset]
 
670
                source_offset += 1
 
671
                if xstep == 1:
 
672
                    offset = y * self.row_bytes
 
673
                    a[offset:offset+self.row_bytes] = \
 
674
                        scanlines[source_offset:source_offset + self.row_bytes]
 
675
                    source_offset += self.row_bytes
 
676
                else:
 
677
                    # Note we want the ceiling of (width - xstart) / xtep
 
678
                    row_len = self.psize * (
 
679
                        (self.width - xstart + xstep - 1) / xstep)
 
680
                    offset = y * self.row_bytes + xstart * self.psize
 
681
                    end_offset = (y+1) * self.row_bytes
 
682
                    skip = self.psize * xstep
 
683
                    for i in range(self.psize):
 
684
                        a[offset+i:end_offset:skip] = \
 
685
                            scanlines[source_offset + i:
 
686
                                      source_offset + row_len:
 
687
                                      self.psize]
 
688
                    source_offset += row_len
 
689
                if filter_type:
 
690
                    self.reconstruct_line(filter_type, filter_first_line,
 
691
                                          offset, xstep, ystep)
 
692
                filter_first_line = 0
 
693
        return a
 
694
 
 
695
    def read_flat(self, scanlines):
 
696
        a = array('B')
 
697
        self.pixels = a
 
698
        offset = 0
 
699
        source_offset = 0
 
700
        filter_first_line = 1
 
701
        for y in range(self.height):
 
702
            filter_type = scanlines[source_offset]
 
703
            source_offset += 1
 
704
            a.extend(scanlines[source_offset: source_offset + self.row_bytes])
 
705
            if filter_type:
 
706
                self.reconstruct_line(filter_type, filter_first_line,
 
707
                                      offset, 1, 1)
 
708
            filter_first_line = 0
 
709
            offset += self.row_bytes
 
710
            source_offset += self.row_bytes
 
711
        return a
 
712
 
 
713
    def read(self):
 
714
        """
 
715
        Read a simple PNG file, return width, height, pixels and image metadata
 
716
 
 
717
        This function is a very early prototype with limited flexibility
 
718
        and excessive use of memory.
 
719
        """
 
720
        signature = self.file.read(8)
 
721
        if (signature != struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)):
 
722
            raise Error("PNG file has invalid header")
 
723
        compressed = []
 
724
        image_metadata = {}
 
725
        while True:
 
726
            try:
 
727
                tag, data = self.read_chunk()
 
728
            except ValueError, e:
 
729
                raise Error('Chunk error: ' + e.args[0])
 
730
 
 
731
            # print >> sys.stderr, tag, len(data)
 
732
            if tag == 'IHDR': # http://www.w3.org/TR/PNG/#11IHDR
 
733
                (width, height, bits_per_sample, color_type,
 
734
                 compression_method, filter_method,
 
735
                 interlaced) = struct.unpack("!2I5B", data)
 
736
                bps = bits_per_sample / 8
 
737
                if bps == 0:
 
738
                    raise Error("unsupported pixel depth")
 
739
                if bps > 2 or bits_per_sample != (bps * 8):
 
740
                    raise Error("invalid pixel depth")
 
741
                if color_type == 0:
 
742
                    greyscale = True
 
743
                    has_alpha = False
 
744
                    planes = 1
 
745
                elif color_type == 2:
 
746
                    greyscale = False
 
747
                    has_alpha = False
 
748
                    planes = 3
 
749
                elif color_type == 4:
 
750
                    greyscale = True
 
751
                    has_alpha = True
 
752
                    planes = 2
 
753
                elif color_type == 6:
 
754
                    greyscale = False
 
755
                    has_alpha = True
 
756
                    planes = 4
 
757
                else:
 
758
                    raise Error("unknown PNG colour type %s" % color_type)
 
759
                if compression_method != 0:
 
760
                    raise Error("unknown compression method")
 
761
                if filter_method != 0:
 
762
                    raise Error("unknown filter method")
 
763
                self.bps = bps
 
764
                self.planes = planes
 
765
                self.psize = bps * planes
 
766
                self.width = width
 
767
                self.height = height
 
768
                self.row_bytes = width * self.psize
 
769
            elif tag == 'IDAT': # http://www.w3.org/TR/PNG/#11IDAT
 
770
                compressed.append(data)
 
771
            elif tag == 'bKGD':
 
772
                if greyscale:
 
773
                    image_metadata["background"] = struct.unpack("!1H", data)
 
774
                else:
 
775
                    image_metadata["background"] = struct.unpack("!3H", data)
 
776
            elif tag == 'tRNS':
 
777
                if greyscale:
 
778
                    image_metadata["transparent"] = struct.unpack("!1H", data)
 
779
                else:
 
780
                    image_metadata["transparent"] = struct.unpack("!3H", data)
 
781
            elif tag == 'gAMA':
 
782
                image_metadata["gamma"] = (
 
783
                    struct.unpack("!L", data)[0]) / 100000.0
 
784
            elif tag == 'IEND': # http://www.w3.org/TR/PNG/#11IEND
 
785
                break
 
786
        scanlines = array('B', zlib.decompress(''.join(compressed)))
 
787
        if interlaced:
 
788
            pixels = self.deinterlace(scanlines)
 
789
        else:
 
790
            pixels = self.read_flat(scanlines)
 
791
        image_metadata["greyscale"] = greyscale
 
792
        image_metadata["has_alpha"] = has_alpha
 
793
        image_metadata["bytes_per_sample"] = bps
 
794
        image_metadata["interlaced"] = interlaced
 
795
        return width, height, pixels, image_metadata
 
796
 
 
797
 
 
798
def test_suite(options):
 
799
    """
 
800
    Run regression test and write PNG file to stdout.
 
801
    """
 
802
 
 
803
    # Below is a big stack of test image generators
 
804
 
 
805
    def test_gradient_horizontal_lr(x, y):
 
806
        return x
 
807
 
 
808
    def test_gradient_horizontal_rl(x, y):
 
809
        return 1-x
 
810
 
 
811
    def test_gradient_vertical_tb(x, y):
 
812
        return y
 
813
 
 
814
    def test_gradient_vertical_bt(x, y):
 
815
        return 1-y
 
816
 
 
817
    def test_radial_tl(x, y):
 
818
        return max(1-math.sqrt(x*x+y*y), 0.0)
 
819
 
 
820
    def test_radial_center(x, y):
 
821
        return test_radial_tl(x-0.5, y-0.5)
 
822
 
 
823
    def test_radial_tr(x, y):
 
824
        return test_radial_tl(1-x, y)
 
825
 
 
826
    def test_radial_bl(x, y):
 
827
        return test_radial_tl(x, 1-y)
 
828
 
 
829
    def test_radial_br(x, y):
 
830
        return test_radial_tl(1-x, 1-y)
 
831
 
 
832
    def test_stripe(x, n):
 
833
        return 1.0*(int(x*n) & 1)
 
834
 
 
835
    def test_stripe_h_2(x, y):
 
836
        return test_stripe(x, 2)
 
837
 
 
838
    def test_stripe_h_4(x, y):
 
839
        return test_stripe(x, 4)
 
840
 
 
841
    def test_stripe_h_10(x, y):
 
842
        return test_stripe(x, 10)
 
843
 
 
844
    def test_stripe_v_2(x, y):
 
845
        return test_stripe(y, 2)
 
846
 
 
847
    def test_stripe_v_4(x, y):
 
848
        return test_stripe(y, 4)
 
849
 
 
850
    def test_stripe_v_10(x, y):
 
851
        return test_stripe(y, 10)
 
852
 
 
853
    def test_stripe_lr_10(x, y):
 
854
        return test_stripe(x+y, 10)
 
855
 
 
856
    def test_stripe_rl_10(x, y):
 
857
        return test_stripe(x-y, 10)
 
858
 
 
859
    def test_checker(x, y, n):
 
860
        return 1.0*((int(x*n) & 1) ^ (int(y*n) & 1))
 
861
 
 
862
    def test_checker_8(x, y):
 
863
        return test_checker(x, y, 8)
 
864
 
 
865
    def test_checker_15(x, y):
 
866
        return test_checker(x, y, 15)
 
867
 
 
868
    def test_zero(x, y):
 
869
        return 0
 
870
 
 
871
    def test_one(x, y):
 
872
        return 1
 
873
 
 
874
    test_patterns = {
 
875
        "GLR": test_gradient_horizontal_lr,
 
876
        "GRL": test_gradient_horizontal_rl,
 
877
        "GTB": test_gradient_vertical_tb,
 
878
        "GBT": test_gradient_vertical_bt,
 
879
        "RTL": test_radial_tl,
 
880
        "RTR": test_radial_tr,
 
881
        "RBL": test_radial_bl,
 
882
        "RBR": test_radial_br,
 
883
        "RCTR": test_radial_center,
 
884
        "HS2": test_stripe_h_2,
 
885
        "HS4": test_stripe_h_4,
 
886
        "HS10": test_stripe_h_10,
 
887
        "VS2": test_stripe_v_2,
 
888
        "VS4": test_stripe_v_4,
 
889
        "VS10": test_stripe_v_10,
 
890
        "LRS": test_stripe_lr_10,
 
891
        "RLS": test_stripe_rl_10,
 
892
        "CK8": test_checker_8,
 
893
        "CK15": test_checker_15,
 
894
        "ZERO": test_zero,
 
895
        "ONE": test_one,
 
896
        }
 
897
 
 
898
    def test_pattern(width, height, depth, pattern):
 
899
        a = array('B')
 
900
        fw = float(width)
 
901
        fh = float(height)
 
902
        pfun = test_patterns[pattern]
 
903
        if depth == 1:
 
904
            for y in range(height):
 
905
                for x in range(width):
 
906
                    a.append(int(pfun(float(x)/fw, float(y)/fh) * 255))
 
907
        elif depth == 2:
 
908
            for y in range(height):
 
909
                for x in range(width):
 
910
                    v = int(pfun(float(x)/fw, float(y)/fh) * 65535)
 
911
                    a.append(v >> 8)
 
912
                    a.append(v & 0xff)
 
913
        return a
 
914
 
 
915
    def test_rgba(size=256, depth=1,
 
916
                    red="GTB", green="GLR", blue="RTL", alpha=None):
 
917
        r = test_pattern(size, size, depth, red)
 
918
        g = test_pattern(size, size, depth, green)
 
919
        b = test_pattern(size, size, depth, blue)
 
920
        if alpha:
 
921
            a = test_pattern(size, size, depth, alpha)
 
922
        i = interleave_planes(r, g, depth, depth)
 
923
        i = interleave_planes(i, b, 2 * depth, depth)
 
924
        if alpha:
 
925
            i = interleave_planes(i, a, 3 * depth, depth)
 
926
        return i
 
927
 
 
928
    # The body of test_suite()
 
929
    size = 256
 
930
    if options.test_size:
 
931
        size = options.test_size
 
932
    depth = 1
 
933
    if options.test_deep:
 
934
        depth = 2
 
935
 
 
936
    kwargs = {}
 
937
    if options.test_red:
 
938
        kwargs["red"] = options.test_red
 
939
    if options.test_green:
 
940
        kwargs["green"] = options.test_green
 
941
    if options.test_blue:
 
942
        kwargs["blue"] = options.test_blue
 
943
    if options.test_alpha:
 
944
        kwargs["alpha"] = options.test_alpha
 
945
    pixels = test_rgba(size, depth, **kwargs)
 
946
 
 
947
    writer = Writer(size, size,
 
948
                    bytes_per_sample=depth,
 
949
                    transparent=options.transparent,
 
950
                    background=options.background,
 
951
                    gamma=options.gamma,
 
952
                    has_alpha=options.test_alpha,
 
953
                    compression=options.compression,
 
954
                    interlaced=options.interlace)
 
955
    writer.write_array(sys.stdout, pixels)
 
956
 
 
957
 
 
958
def read_pnm_header(infile, supported='P6'):
 
959
    """
 
960
    Read a PNM header, return width and height of the image in pixels.
 
961
    """
 
962
    header = []
 
963
    while len(header) < 4:
 
964
        line = infile.readline()
 
965
        sharp = line.find('#')
 
966
        if sharp > -1:
 
967
            line = line[:sharp]
 
968
        header.extend(line.split())
 
969
        if len(header) == 3 and header[0] == 'P4':
 
970
            break # PBM doesn't have maxval
 
971
    if header[0] not in supported:
 
972
        raise NotImplementedError('file format %s not supported' % header[0])
 
973
    if header[0] != 'P4' and header[3] != '255':
 
974
        raise NotImplementedError('maxval %s not supported' % header[3])
 
975
    return int(header[1]), int(header[2])
 
976
 
 
977
 
 
978
def color_triple(color):
 
979
    """
 
980
    Convert a command line color value to a RGB triple of integers.
 
981
    FIXME: Somewhere we need support for greyscale backgrounds etc.
 
982
    """
 
983
    if color.startswith('#') and len(color) == 4:
 
984
        return (int(color[1], 16),
 
985
                int(color[2], 16),
 
986
                int(color[3], 16))
 
987
    if color.startswith('#') and len(color) == 7:
 
988
        return (int(color[1:3], 16),
 
989
                int(color[3:5], 16),
 
990
                int(color[5:7], 16))
 
991
    elif color.startswith('#') and len(color) == 13:
 
992
        return (int(color[1:5], 16),
 
993
                int(color[5:9], 16),
 
994
                int(color[9:13], 16))
 
995
 
 
996
 
 
997
def _main():
 
998
    """
 
999
    Run the PNG encoder with options from the command line.
 
1000
    """
 
1001
    # Parse command line arguments
 
1002
    from optparse import OptionParser
 
1003
    version = '%prog ' + __revision__.strip('$').replace('Rev: ', 'r')
 
1004
    parser = OptionParser(version=version)
 
1005
    parser.set_usage("%prog [options] [pnmfile]")
 
1006
    parser.add_option("-i", "--interlace",
 
1007
                      default=False, action="store_true",
 
1008
                      help="create an interlaced PNG file (Adam7)")
 
1009
    parser.add_option("-t", "--transparent",
 
1010
                      action="store", type="string", metavar="color",
 
1011
                      help="mark the specified color as transparent")
 
1012
    parser.add_option("-b", "--background",
 
1013
                      action="store", type="string", metavar="color",
 
1014
                      help="save the specified background color")
 
1015
    parser.add_option("-a", "--alpha",
 
1016
                      action="store", type="string", metavar="pgmfile",
 
1017
                      help="alpha channel transparency (RGBA)")
 
1018
    parser.add_option("-g", "--gamma",
 
1019
                      action="store", type="float", metavar="value",
 
1020
                      help="save the specified gamma value")
 
1021
    parser.add_option("-c", "--compression",
 
1022
                      action="store", type="int", metavar="level",
 
1023
                      help="zlib compression level (0-9)")
 
1024
    parser.add_option("-T", "--test",
 
1025
                      default=False, action="store_true",
 
1026
                      help="create a test image")
 
1027
    parser.add_option("-R", "--test-red",
 
1028
                      action="store", type="string", metavar="pattern",
 
1029
                      help="test pattern for the red image layer")
 
1030
    parser.add_option("-G", "--test-green",
 
1031
                      action="store", type="string", metavar="pattern",
 
1032
                      help="test pattern for the green image layer")
 
1033
    parser.add_option("-B", "--test-blue",
 
1034
                      action="store", type="string", metavar="pattern",
 
1035
                      help="test pattern for the blue image layer")
 
1036
    parser.add_option("-A", "--test-alpha",
 
1037
                      action="store", type="string", metavar="pattern",
 
1038
                      help="test pattern for the alpha image layer")
 
1039
    parser.add_option("-D", "--test-deep",
 
1040
                      default=False, action="store_true",
 
1041
                      help="use test patterns with 16 bits per layer")
 
1042
    parser.add_option("-S", "--test-size",
 
1043
                      action="store", type="int", metavar="size",
 
1044
                      help="width and height of the test image")
 
1045
    (options, args) = parser.parse_args()
 
1046
 
 
1047
    # Convert options
 
1048
    if options.transparent is not None:
 
1049
        options.transparent = color_triple(options.transparent)
 
1050
    if options.background is not None:
 
1051
        options.background = color_triple(options.background)
 
1052
 
 
1053
    # Run regression tests
 
1054
    if options.test:
 
1055
        return test_suite(options)
 
1056
 
 
1057
    # Prepare input and output files
 
1058
    if len(args) == 0:
 
1059
        ppmfilename = '-'
 
1060
        ppmfile = sys.stdin
 
1061
    elif len(args) == 1:
 
1062
        ppmfilename = args[0]
 
1063
        ppmfile = open(ppmfilename, 'rb')
 
1064
    else:
 
1065
        parser.error("more than one input file")
 
1066
    outfile = sys.stdout
 
1067
 
 
1068
    # Encode PNM to PNG
 
1069
    width, height = read_pnm_header(ppmfile)
 
1070
    writer = Writer(width, height,
 
1071
                    transparent=options.transparent,
 
1072
                    background=options.background,
 
1073
                    has_alpha=options.alpha is not None,
 
1074
                    gamma=options.gamma,
 
1075
                    compression=options.compression)
 
1076
    if options.alpha is not None:
 
1077
        pgmfile = open(options.alpha, 'rb')
 
1078
        awidth, aheight = read_pnm_header(pgmfile, 'P5')
 
1079
        if (awidth, aheight) != (width, height):
 
1080
            raise ValueError("alpha channel image size mismatch" +
 
1081
                             " (%s has %sx%s but %s has %sx%s)"
 
1082
                             % (ppmfilename, width, height,
 
1083
                                options.alpha, awidth, aheight))
 
1084
        writer.convert_ppm_and_pgm(ppmfile, pgmfile, outfile,
 
1085
                           interlace=options.interlace)
 
1086
    else:
 
1087
        writer.convert_ppm(ppmfile, outfile,
 
1088
                           interlace=options.interlace)
 
1089
 
 
1090
 
 
1091
if __name__ == '__main__':
 
1092
    _main()