mirror of https://github.com/roytam1/boc-uxp.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3786 lines
152 KiB
3786 lines
152 KiB
4 years ago
|
#!/usr/bin/env python
|
||
|
|
||
|
# $URL: http://pypng.googlecode.com/svn/trunk/code/png.py $
|
||
|
# $Rev: 228 $
|
||
|
|
||
|
# png.py - PNG encoder/decoder in pure Python
|
||
|
#
|
||
|
# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
|
||
|
# Portions Copyright (C) 2009 David Jones <drj@pobox.com>
|
||
|
# And probably portions Copyright (C) 2006 Nicko van Someren <nicko@nicko.org>
|
||
|
#
|
||
|
# Original concept by Johann C. Rocholl.
|
||
|
#
|
||
|
# LICENSE (The MIT License)
|
||
|
#
|
||
|
# Permission is hereby granted, free of charge, to any person
|
||
|
# obtaining a copy of this software and associated documentation files
|
||
|
# (the "Software"), to deal in the Software without restriction,
|
||
|
# including without limitation the rights to use, copy, modify, merge,
|
||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||
|
# and to permit persons to whom the Software is furnished to do so,
|
||
|
# subject to the following conditions:
|
||
|
#
|
||
|
# The above copyright notice and this permission notice shall be
|
||
|
# included in all copies or substantial portions of the Software.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
# SOFTWARE.
|
||
|
#
|
||
|
# Changelog (recent first):
|
||
|
# 2009-03-11 David: interlaced bit depth < 8 (writing).
|
||
|
# 2009-03-10 David: interlaced bit depth < 8 (reading).
|
||
|
# 2009-03-04 David: Flat and Boxed pixel formats.
|
||
|
# 2009-02-26 David: Palette support (writing).
|
||
|
# 2009-02-23 David: Bit-depths < 8; better PNM support.
|
||
|
# 2006-06-17 Nicko: Reworked into a class, faster interlacing.
|
||
|
# 2006-06-17 Johann: Very simple prototype PNG decoder.
|
||
|
# 2006-06-17 Nicko: Test suite with various image generators.
|
||
|
# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support.
|
||
|
# 2006-06-15 Johann: Scanline iterator interface for large input files.
|
||
|
# 2006-06-09 Johann: Very simple prototype PNG encoder.
|
||
|
|
||
|
# Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from
|
||
|
# http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885
|
||
|
|
||
|
# Incorporated into pypng by drj on 2009-03-12 from
|
||
|
# //depot/prj/bangaio/master/code/png.py#67
|
||
|
|
||
|
|
||
|
"""
|
||
|
Pure Python PNG Reader/Writer
|
||
|
|
||
|
This Python module implements support for PNG images (see PNG
|
||
|
specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads
|
||
|
and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64
|
||
|
bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB,
|
||
|
RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped
|
||
|
images (1/2/4/8 bit). Adam7 interlacing is supported for reading and
|
||
|
writing. A number of optional chunks can be specified (when writing)
|
||
|
and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
|
||
|
|
||
|
For help, type ``import png; help(png)`` in your python interpreter.
|
||
|
|
||
|
A good place to start is the :class:`Reader` and :class:`Writer` classes.
|
||
|
|
||
|
Requires Python 2.3. Limited support is available for Python 2.2, but
|
||
|
not everything works. Best with Python 2.4 and higher. Installation is
|
||
|
trivial, but see the ``README.txt`` file (with the source distribution)
|
||
|
for details.
|
||
|
|
||
|
This file can also be used as a command-line utility to convert
|
||
|
`Netpbm <http://netpbm.sourceforge.net/>`_ PNM files to PNG, and the reverse conversion from PNG to
|
||
|
PNM. The interface is similar to that of the ``pnmtopng`` program from
|
||
|
Netpbm. Type ``python png.py --help`` at the shell prompt
|
||
|
for usage and a list of options.
|
||
|
|
||
|
A note on spelling and terminology
|
||
|
----------------------------------
|
||
|
|
||
|
Generally British English spelling is used in the documentation. So
|
||
|
that's "greyscale" and "colour". This not only matches the author's
|
||
|
native language, it's also used by the PNG specification.
|
||
|
|
||
|
The major colour models supported by PNG (and hence by PyPNG) are:
|
||
|
greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes
|
||
|
referred to using the abbreviations: L, RGB, LA, RGBA. In this case
|
||
|
each letter abbreviates a single channel: *L* is for Luminance or Luma or
|
||
|
Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand
|
||
|
for Red, Green, Blue, the components of a colour image; *A* stands for
|
||
|
Alpha, the opacity channel (used for transparency effects, but higher
|
||
|
values are more opaque, so it makes sense to call it opacity).
|
||
|
|
||
|
A note on formats
|
||
|
-----------------
|
||
|
|
||
|
When getting pixel data out of this module (reading) and presenting
|
||
|
data to this module (writing) there are a number of ways the data could
|
||
|
be represented as a Python value. Generally this module uses one of
|
||
|
three formats called "flat row flat pixel", "boxed row flat pixel", and
|
||
|
"boxed row boxed pixel". Basically the concern is whether each pixel
|
||
|
and each row comes in its own little tuple (box), or not.
|
||
|
|
||
|
Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
|
||
|
has RGB components:
|
||
|
|
||
|
Boxed row flat pixel::
|
||
|
|
||
|
list([R,G,B, R,G,B, R,G,B],
|
||
|
[R,G,B, R,G,B, R,G,B])
|
||
|
|
||
|
Each row appears as its own list, but the pixels are flattened so that
|
||
|
three values for one pixel simply follow the three values for the previous
|
||
|
pixel. This is the most common format used, because it provides a good
|
||
|
compromise between space and convenience. PyPNG regards itself as
|
||
|
at liberty to replace any sequence type with any sufficiently compatible
|
||
|
other sequence type; in practice each row is an array (from the array
|
||
|
module), and the outer list is sometimes an iterator rather than an
|
||
|
explicit list (so that streaming is possible).
|
||
|
|
||
|
Flat row flat pixel::
|
||
|
|
||
|
[R,G,B, R,G,B, R,G,B,
|
||
|
R,G,B, R,G,B, R,G,B]
|
||
|
|
||
|
The entire image is one single giant sequence of colour values.
|
||
|
Generally an array will be used (to save space), not a list.
|
||
|
|
||
|
Boxed row boxed pixel::
|
||
|
|
||
|
list([ (R,G,B), (R,G,B), (R,G,B) ],
|
||
|
[ (R,G,B), (R,G,B), (R,G,B) ])
|
||
|
|
||
|
Each row appears in its own list, but each pixel also appears in its own
|
||
|
tuple. A serious memory burn in Python.
|
||
|
|
||
|
In all cases the top row comes first, and for each row the pixels are
|
||
|
ordered from left-to-right. Within a pixel the values appear in the
|
||
|
order, R-G-B-A (or L-A for greyscale--alpha).
|
||
|
|
||
|
There is a fourth format, mentioned because it is used internally,
|
||
|
is close to what lies inside a PNG file itself, and has some support
|
||
|
from the public API. This format is called packed. When packed,
|
||
|
each row is a sequence of bytes (integers from 0 to 255), just as
|
||
|
it is before PNG scanline filtering is applied. When the bit depth
|
||
|
is 8 this is essentially the same as boxed row flat pixel; when the
|
||
|
bit depth is less than 8, several pixels are packed into each byte;
|
||
|
when the bit depth is 16 (the only value more than 8 that is supported
|
||
|
by the PNG image format) each pixel value is decomposed into 2 bytes
|
||
|
(and `packed` is a misnomer). This format is used by the
|
||
|
:meth:`Writer.write_packed` method. It isn't usually a convenient
|
||
|
format, but may be just right if the source data for the PNG image
|
||
|
comes from something that uses a similar format (for example, 1-bit
|
||
|
BMPs, or another PNG file).
|
||
|
|
||
|
And now, my famous members
|
||
|
--------------------------
|
||
|
"""
|
||
|
|
||
|
# http://www.python.org/doc/2.2.3/whatsnew/node5.html
|
||
|
from __future__ import generators
|
||
|
|
||
|
__version__ = "$URL: http://pypng.googlecode.com/svn/trunk/code/png.py $ $Rev: 228 $"
|
||
|
|
||
|
from array import array
|
||
|
try: # See :pyver:old
|
||
|
import itertools
|
||
|
except:
|
||
|
pass
|
||
|
import math
|
||
|
# http://www.python.org/doc/2.4.4/lib/module-operator.html
|
||
|
import operator
|
||
|
import struct
|
||
|
import sys
|
||
|
import zlib
|
||
|
# http://www.python.org/doc/2.4.4/lib/module-warnings.html
|
||
|
import warnings
|
||
|
|
||
|
|
||
|
__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
|
||
|
|
||
|
|
||
|
# The PNG signature.
|
||
|
# http://www.w3.org/TR/PNG/#5PNG-file-signature
|
||
|
_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
|
||
|
|
||
|
_adam7 = ((0, 0, 8, 8),
|
||
|
(4, 0, 8, 8),
|
||
|
(0, 4, 4, 8),
|
||
|
(2, 0, 4, 4),
|
||
|
(0, 2, 2, 4),
|
||
|
(1, 0, 2, 2),
|
||
|
(0, 1, 1, 2))
|
||
|
|
||
|
def group(s, n):
|
||
|
# See
|
||
|
# http://www.python.org/doc/2.6/library/functions.html#zip
|
||
|
return zip(*[iter(s)]*n)
|
||
|
|
||
|
def isarray(x):
|
||
|
"""Same as ``isinstance(x, array)`` except on Python 2.2, where it
|
||
|
always returns ``False``. This helps PyPNG work on Python 2.2.
|
||
|
"""
|
||
|
|
||
|
try:
|
||
|
return isinstance(x, array)
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
try: # see :pyver:old
|
||
|
array.tostring
|
||
|
except:
|
||
|
def tostring(row):
|
||
|
l = len(row)
|
||
|
return struct.pack('%dB' % l, *row)
|
||
|
else:
|
||
|
def tostring(row):
|
||
|
"""Convert row of bytes to string. Expects `row` to be an
|
||
|
``array``.
|
||
|
"""
|
||
|
return row.tostring()
|
||
|
|
||
|
# Conditionally convert to bytes. Works on Python 2 and Python 3.
|
||
|
try:
|
||
|
bytes('', 'ascii')
|
||
|
def strtobytes(x): return bytes(x, 'iso8859-1')
|
||
|
def bytestostr(x): return str(x, 'iso8859-1')
|
||
|
except:
|
||
|
strtobytes = str
|
||
|
bytestostr = str
|
||
|
|
||
|
def interleave_planes(ipixels, apixels, ipsize, apsize):
|
||
|
"""
|
||
|
Interleave (colour) planes, e.g. RGB + A = RGBA.
|
||
|
|
||
|
Return an array of pixels consisting of the `ipsize` elements of data
|
||
|
from each pixel in `ipixels` followed by the `apsize` elements of data
|
||
|
from each pixel in `apixels`. Conventionally `ipixels` and
|
||
|
`apixels` are byte arrays so the sizes are bytes, but it actually
|
||
|
works with any arrays of the same type. The returned array is the
|
||
|
same type as the input arrays which should be the same type as each other.
|
||
|
"""
|
||
|
|
||
|
itotal = len(ipixels)
|
||
|
atotal = len(apixels)
|
||
|
newtotal = itotal + atotal
|
||
|
newpsize = ipsize + apsize
|
||
|
# Set up the output buffer
|
||
|
# See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356
|
||
|
out = array(ipixels.typecode)
|
||
|
# It's annoying that there is no cheap way to set the array size :-(
|
||
|
out.extend(ipixels)
|
||
|
out.extend(apixels)
|
||
|
# Interleave in the pixel data
|
||
|
for i in range(ipsize):
|
||
|
out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize]
|
||
|
for i in range(apsize):
|
||
|
out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize]
|
||
|
return out
|
||
|
|
||
|
def check_palette(palette):
|
||
|
"""Check a palette argument (to the :class:`Writer` class) for validity.
|
||
|
Returns the palette as a list if okay; raises an exception otherwise.
|
||
|
"""
|
||
|
|
||
|
# None is the default and is allowed.
|
||
|
if palette is None:
|
||
|
return None
|
||
|
|
||
|
p = list(palette)
|
||
|
if not (0 < len(p) <= 256):
|
||
|
raise ValueError("a palette must have between 1 and 256 entries")
|
||
|
seen_triple = False
|
||
|
for i,t in enumerate(p):
|
||
|
if len(t) not in (3,4):
|
||
|
raise ValueError(
|
||
|
"palette entry %d: entries must be 3- or 4-tuples." % i)
|
||
|
if len(t) == 3:
|
||
|
seen_triple = True
|
||
|
if seen_triple and len(t) == 4:
|
||
|
raise ValueError(
|
||
|
"palette entry %d: all 4-tuples must precede all 3-tuples" % i)
|
||
|
for x in t:
|
||
|
if int(x) != x or not(0 <= x <= 255):
|
||
|
raise ValueError(
|
||
|
"palette entry %d: values must be integer: 0 <= x <= 255" % i)
|
||
|
return p
|
||
|
|
||
|
class Error(Exception):
|
||
|
prefix = 'Error'
|
||
|
def __str__(self):
|
||
|
return self.prefix + ': ' + ' '.join(self.args)
|
||
|
|
||
|
class FormatError(Error):
|
||
|
"""Problem with input file format. In other words, PNG file does
|
||
|
not conform to the specification in some way and is invalid.
|
||
|
"""
|
||
|
|
||
|
prefix = 'FormatError'
|
||
|
|
||
|
class ChunkError(FormatError):
|
||
|
prefix = 'ChunkError'
|
||
|
|
||
|
|
||
|
class Writer:
|
||
|
"""
|
||
|
PNG encoder in pure Python.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, width=None, height=None,
|
||
|
size=None,
|
||
|
greyscale=False,
|
||
|
alpha=False,
|
||
|
bitdepth=8,
|
||
|
palette=None,
|
||
|
transparent=None,
|
||
|
background=None,
|
||
|
gamma=None,
|
||
|
compression=None,
|
||
|
interlace=False,
|
||
|
bytes_per_sample=None, # deprecated
|
||
|
planes=None,
|
||
|
colormap=None,
|
||
|
maxval=None,
|
||
|
chunk_limit=2**20):
|
||
|
"""
|
||
|
Create a PNG encoder object.
|
||
|
|
||
|
Arguments:
|
||
|
|
||
|
width, height
|
||
|
Image size in pixels, as two separate arguments.
|
||
|
size
|
||
|
Image size (w,h) in pixels, as single argument.
|
||
|
greyscale
|
||
|
Input data is greyscale, not RGB.
|
||
|
alpha
|
||
|
Input data has alpha channel (RGBA or LA).
|
||
|
bitdepth
|
||
|
Bit depth: from 1 to 16.
|
||
|
palette
|
||
|
Create a palette for a colour mapped image (colour type 3).
|
||
|
transparent
|
||
|
Specify a transparent colour (create a ``tRNS`` chunk).
|
||
|
background
|
||
|
Specify a default background colour (create a ``bKGD`` chunk).
|
||
|
gamma
|
||
|
Specify a gamma value (create a ``gAMA`` chunk).
|
||
|
compression
|
||
|
zlib compression level (1-9).
|
||
|
interlace
|
||
|
Create an interlaced image.
|
||
|
chunk_limit
|
||
|
Write multiple ``IDAT`` chunks to save memory.
|
||
|
|
||
|
The image size (in pixels) can be specified either by using the
|
||
|
`width` and `height` arguments, or with the single `size`
|
||
|
argument. If `size` is used it should be a pair (*width*,
|
||
|
*height*).
|
||
|
|
||
|
`greyscale` and `alpha` are booleans that specify whether
|
||
|
an image is greyscale (or colour), and whether it has an
|
||
|
alpha channel (or not).
|
||
|
|
||
|
`bitdepth` specifies the bit depth of the source pixel values.
|
||
|
Each source pixel value must be an integer between 0 and
|
||
|
``2**bitdepth-1``. For example, 8-bit images have values
|
||
|
between 0 and 255. PNG only stores images with bit depths of
|
||
|
1,2,4,8, or 16. When `bitdepth` is not one of these values,
|
||
|
the next highest valid bit depth is selected, and an ``sBIT``
|
||
|
(significant bits) chunk is generated that specifies the original
|
||
|
precision of the source image. In this case the supplied pixel
|
||
|
values will be rescaled to fit the range of the selected bit depth.
|
||
|
|
||
|
The details of which bit depth / colour model combinations the
|
||
|
PNG file format supports directly, are somewhat arcane
|
||
|
(refer to the PNG specification for full details). Briefly:
|
||
|
"small" bit depths (1,2,4) are only allowed with greyscale and
|
||
|
colour mapped images; colour mapped images cannot have bit depth
|
||
|
16.
|
||
|
|
||
|
For colour mapped images (in other words, when the `palette`
|
||
|
argument is specified) the `bitdepth` argument must match one of
|
||
|
the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a
|
||
|
PNG image with a palette and an ``sBIT`` chunk, but the meaning
|
||
|
is slightly different; it would be awkward to press the
|
||
|
`bitdepth` argument into service for this.)
|
||
|
|
||
|
The `palette` option, when specified, causes a colour mapped image
|
||
|
to be created: the PNG colour type is set to 3; greyscale
|
||
|
must not be set; alpha must not be set; transparent must
|
||
|
not be set; the bit depth must be 1,2,4, or 8. When a colour
|
||
|
mapped image is created, the pixel values are palette indexes
|
||
|
and the `bitdepth` argument specifies the size of these indexes
|
||
|
(not the size of the colour values in the palette).
|
||
|
|
||
|
The palette argument value should be a sequence of 3- or
|
||
|
4-tuples. 3-tuples specify RGB palette entries; 4-tuples
|
||
|
specify RGBA palette entries. If both 4-tuples and 3-tuples
|
||
|
appear in the sequence then all the 4-tuples must come
|
||
|
before all the 3-tuples. A ``PLTE`` chunk is created; if there
|
||
|
are 4-tuples then a ``tRNS`` chunk is created as well. The
|
||
|
``PLTE`` chunk will contain all the RGB triples in the same
|
||
|
sequence; the ``tRNS`` chunk will contain the alpha channel for
|
||
|
all the 4-tuples, in the same sequence. Palette entries
|
||
|
are always 8-bit.
|
||
|
|
||
|
If specified, the `transparent` and `background` parameters must
|
||
|
be a tuple with three integer values for red, green, blue, or
|
||
|
a simple integer (or singleton tuple) for a greyscale image.
|
||
|
|
||
|
If specified, the `gamma` parameter must be a positive number
|
||
|
(generally, a float). A ``gAMA`` chunk will be created. Note that
|
||
|
this will not change the values of the pixels as they appear in
|
||
|
the PNG file, they are assumed to have already been converted
|
||
|
appropriately for the gamma specified.
|
||
|
|
||
|
The `compression` argument specifies the compression level
|
||
|
to be used by the ``zlib`` module. Higher values are likely
|
||
|
to compress better, but will be slower to compress. The
|
||
|
default for this argument is ``None``; this does not mean
|
||
|
no compression, rather it means that the default from the
|
||
|
``zlib`` module is used (which is generally acceptable).
|
||
|
|
||
|
If `interlace` is true then an interlaced image is created
|
||
|
(using PNG's so far only interace method, *Adam7*). This does not
|
||
|
affect how the pixels should be presented to the encoder, rather
|
||
|
it changes how they are arranged into the PNG file. On slow
|
||
|
connexions interlaced images can be partially decoded by the
|
||
|
browser to give a rough view of the image that is successively
|
||
|
refined as more image data appears.
|
||
|
|
||
|
.. note ::
|
||
|
|
||
|
Enabling the `interlace` option requires the entire image
|
||
|
to be processed in working memory.
|
||
|
|
||
|
`chunk_limit` is used to limit the amount of memory used whilst
|
||
|
compressing the image. In order to avoid using large amounts of
|
||
|
memory, multiple ``IDAT`` chunks may be created.
|
||
|
"""
|
||
|
|
||
|
# At the moment the `planes` argument is ignored;
|
||
|
# its purpose is to act as a dummy so that
|
||
|
# ``Writer(x, y, **info)`` works, where `info` is a dictionary
|
||
|
# returned by Reader.read and friends.
|
||
|
# Ditto for `colormap`.
|
||
|
|
||
|
# A couple of helper functions come first. Best skipped if you
|
||
|
# are reading through.
|
||
|
|
||
|
def isinteger(x):
|
||
|
try:
|
||
|
return int(x) == x
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
def check_color(c, which):
|
||
|
"""Checks that a colour argument for transparent or
|
||
|
background options is the right form. Also "corrects" bare
|
||
|
integers to 1-tuples.
|
||
|
"""
|
||
|
|
||
|
if c is None:
|
||
|
return c
|
||
|
if greyscale:
|
||
|
try:
|
||
|
l = len(c)
|
||
|
except TypeError:
|
||
|
c = (c,)
|
||
|
if len(c) != 1:
|
||
|
raise ValueError("%s for greyscale must be 1-tuple" %
|
||
|
which)
|
||
|
if not isinteger(c[0]):
|
||
|
raise ValueError(
|
||
|
"%s colour for greyscale must be integer" %
|
||
|
which)
|
||
|
else:
|
||
|
if not (len(c) == 3 and
|
||
|
isinteger(c[0]) and
|
||
|
isinteger(c[1]) and
|
||
|
isinteger(c[2])):
|
||
|
raise ValueError(
|
||
|
"%s colour must be a triple of integers" %
|
||
|
which)
|
||
|
return c
|
||
|
|
||
|
if size:
|
||
|
if len(size) != 2:
|
||
|
raise ValueError(
|
||
|
"size argument should be a pair (width, height)")
|
||
|
if width is not None and width != size[0]:
|
||
|
raise ValueError(
|
||
|
"size[0] (%r) and width (%r) should match when both are used."
|
||
|
% (size[0], width))
|
||
|
if height is not None and height != size[1]:
|
||
|
raise ValueError(
|
||
|
"size[1] (%r) and height (%r) should match when both are used."
|
||
|
% (size[1], height))
|
||
|
width,height = size
|
||
|
del size
|
||
|
|
||
|
if width <= 0 or height <= 0:
|
||
|
raise ValueError("width and height must be greater than zero")
|
||
|
if not isinteger(width) or not isinteger(height):
|
||
|
raise ValueError("width and height must be integers")
|
||
|
# http://www.w3.org/TR/PNG/#7Integers-and-byte-order
|
||
|
if width > 2**32-1 or height > 2**32-1:
|
||
|
raise ValueError("width and height cannot exceed 2**32-1")
|
||
|
|
||
|
if alpha and transparent is not None:
|
||
|
raise ValueError(
|
||
|
"transparent colour not allowed with alpha channel")
|
||
|
|
||
|
if bytes_per_sample is not None:
|
||
|
warnings.warn('please use bitdepth instead of bytes_per_sample',
|
||
|
DeprecationWarning)
|
||
|
if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2):
|
||
|
raise ValueError(
|
||
|
"bytes per sample must be .125, .25, .5, 1, or 2")
|
||
|
bitdepth = int(8*bytes_per_sample)
|
||
|
del bytes_per_sample
|
||
|
if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth:
|
||
|
raise ValueError("bitdepth (%r) must be a postive integer <= 16" %
|
||
|
bitdepth)
|
||
|
|
||
|
self.rescale = None
|
||
|
if palette:
|
||
|
if bitdepth not in (1,2,4,8):
|
||
|
raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8")
|
||
|
if transparent is not None:
|
||
|
raise ValueError("transparent and palette not compatible")
|
||
|
if alpha:
|
||
|
raise ValueError("alpha and palette not compatible")
|
||
|
if greyscale:
|
||
|
raise ValueError("greyscale and palette not compatible")
|
||
|
else:
|
||
|
# No palette, check for sBIT chunk generation.
|
||
|
if alpha or not greyscale:
|
||
|
if bitdepth not in (8,16):
|
||
|
targetbitdepth = (8,16)[bitdepth > 8]
|
||
|
self.rescale = (bitdepth, targetbitdepth)
|
||
|
bitdepth = targetbitdepth
|
||
|
del targetbitdepth
|
||
|
else:
|
||
|
assert greyscale
|
||
|
assert not alpha
|
||
|
if bitdepth not in (1,2,4,8,16):
|
||
|
if bitdepth > 8:
|
||
|
targetbitdepth = 16
|
||
|
elif bitdepth == 3:
|
||
|
targetbitdepth = 4
|
||
|
else:
|
||
|
assert bitdepth in (5,6,7)
|
||
|
targetbitdepth = 8
|
||
|
self.rescale = (bitdepth, targetbitdepth)
|
||
|
bitdepth = targetbitdepth
|
||
|
del targetbitdepth
|
||
|
|
||
|
if bitdepth < 8 and (alpha or not greyscale and not palette):
|
||
|
raise ValueError(
|
||
|
"bitdepth < 8 only permitted with greyscale or palette")
|
||
|
if bitdepth > 8 and palette:
|
||
|
raise ValueError(
|
||
|
"bit depth must be 8 or less for images with palette")
|
||
|
|
||
|
transparent = check_color(transparent, 'transparent')
|
||
|
background = check_color(background, 'background')
|
||
|
|
||
|
# It's important that the true boolean values (greyscale, alpha,
|
||
|
# colormap, interlace) are converted to bool because Iverson's
|
||
|
# convention is relied upon later on.
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
self.transparent = transparent
|
||
|
self.background = background
|
||
|
self.gamma = gamma
|
||
|
self.greyscale = bool(greyscale)
|
||
|
self.alpha = bool(alpha)
|
||
|
self.colormap = bool(palette)
|
||
|
self.bitdepth = int(bitdepth)
|
||
|
self.compression = compression
|
||
|
self.chunk_limit = chunk_limit
|
||
|
self.interlace = bool(interlace)
|
||
|
self.palette = check_palette(palette)
|
||
|
|
||
|
self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap
|
||
|
assert self.color_type in (0,2,3,4,6)
|
||
|
|
||
|
self.color_planes = (3,1)[self.greyscale or self.colormap]
|
||
|
self.planes = self.color_planes + self.alpha
|
||
|
# :todo: fix for bitdepth < 8
|
||
|
self.psize = (self.bitdepth/8) * self.planes
|
||
|
|
||
|
def make_palette(self):
|
||
|
"""Create the byte sequences for a ``PLTE`` and if necessary a
|
||
|
``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be
|
||
|
``None`` if no ``tRNS`` chunk is necessary.
|
||
|
"""
|
||
|
|
||
|
p = array('B')
|
||
|
t = array('B')
|
||
|
|
||
|
for x in self.palette:
|
||
|
p.extend(x[0:3])
|
||
|
if len(x) > 3:
|
||
|
t.append(x[3])
|
||
|
p = tostring(p)
|
||
|
t = tostring(t)
|
||
|
if t:
|
||
|
return p,t
|
||
|
return p,None
|
||
|
|
||
|
def write(self, outfile, rows):
|
||
|
"""Write a PNG image to the output file. `rows` should be
|
||
|
an iterable that yields each row in boxed row flat pixel format.
|
||
|
The rows should be the rows of the original image, so there
|
||
|
should be ``self.height`` rows of ``self.width * self.planes`` values.
|
||
|
If `interlace` is specified (when creating the instance), then
|
||
|
an interlaced PNG file will be written. Supply the rows in the
|
||
|
normal image order; the interlacing is carried out internally.
|
||
|
|
||
|
.. note ::
|
||
|
|
||
|
Interlacing will require the entire image to be in working memory.
|
||
|
"""
|
||
|
|
||
|
if self.interlace:
|
||
|
fmt = 'BH'[self.bitdepth > 8]
|
||
|
a = array(fmt, itertools.chain(*rows))
|
||
|
return self.write_array(outfile, a)
|
||
|
else:
|
||
|
nrows = self.write_passes(outfile, rows)
|
||
|
if nrows != self.height:
|
||
|
raise ValueError(
|
||
|
"rows supplied (%d) does not match height (%d)" %
|
||
|
(nrows, self.height))
|
||
|
|
||
|
def write_passes(self, outfile, rows, packed=False):
|
||
|
"""
|
||
|
Write a PNG image to the output file.
|
||
|
|
||
|
Most users are expected to find the :meth:`write` or
|
||
|
:meth:`write_array` method more convenient.
|
||
|
|
||
|
The rows should be given to this method in the order that
|
||
|
they appear in the output file. For straightlaced images,
|
||
|
this is the usual top to bottom ordering, but for interlaced
|
||
|
images the rows should have already been interlaced before
|
||
|
passing them to this function.
|
||
|
|
||
|
`rows` should be an iterable that yields each row. When
|
||
|
`packed` is ``False`` the rows should be in boxed row flat pixel
|
||
|
format; when `packed` is ``True`` each row should be a packed
|
||
|
sequence of bytes.
|
||
|
|
||
|
"""
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#5PNG-file-signature
|
||
|
outfile.write(_signature)
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#11IHDR
|
||
|
write_chunk(outfile, 'IHDR',
|
||
|
struct.pack("!2I5B", self.width, self.height,
|
||
|
self.bitdepth, self.color_type,
|
||
|
0, 0, self.interlace))
|
||
|
|
||
|
# See :chunk:order
|
||
|
# http://www.w3.org/TR/PNG/#11gAMA
|
||
|
if self.gamma is not None:
|
||
|
write_chunk(outfile, 'gAMA',
|
||
|
struct.pack("!L", int(round(self.gamma*1e5))))
|
||
|
|
||
|
# See :chunk:order
|
||
|
# http://www.w3.org/TR/PNG/#11sBIT
|
||
|
if self.rescale:
|
||
|
write_chunk(outfile, 'sBIT',
|
||
|
struct.pack('%dB' % self.planes,
|
||
|
*[self.rescale[0]]*self.planes))
|
||
|
|
||
|
# :chunk:order: Without a palette (PLTE chunk), ordering is
|
||
|
# relatively relaxed. With one, gAMA chunk must precede PLTE
|
||
|
# chunk which must precede tRNS and bKGD.
|
||
|
# See http://www.w3.org/TR/PNG/#5ChunkOrdering
|
||
|
if self.palette:
|
||
|
p,t = self.make_palette()
|
||
|
write_chunk(outfile, 'PLTE', p)
|
||
|
if t:
|
||
|
# tRNS chunk is optional. Only needed if palette entries
|
||
|
# have alpha.
|
||
|
write_chunk(outfile, 'tRNS', t)
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#11tRNS
|
||
|
if self.transparent is not None:
|
||
|
if self.greyscale:
|
||
|
write_chunk(outfile, 'tRNS',
|
||
|
struct.pack("!1H", *self.transparent))
|
||
|
else:
|
||
|
write_chunk(outfile, 'tRNS',
|
||
|
struct.pack("!3H", *self.transparent))
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#11bKGD
|
||
|
if self.background is not None:
|
||
|
if self.greyscale:
|
||
|
write_chunk(outfile, 'bKGD',
|
||
|
struct.pack("!1H", *self.background))
|
||
|
else:
|
||
|
write_chunk(outfile, 'bKGD',
|
||
|
struct.pack("!3H", *self.background))
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#11IDAT
|
||
|
if self.compression is not None:
|
||
|
compressor = zlib.compressobj(self.compression)
|
||
|
else:
|
||
|
compressor = zlib.compressobj()
|
||
|
|
||
|
# Choose an extend function based on the bitdepth. The extend
|
||
|
# function packs/decomposes the pixel values into bytes and
|
||
|
# stuffs them onto the data array.
|
||
|
data = array('B')
|
||
|
if self.bitdepth == 8 or packed:
|
||
|
extend = data.extend
|
||
|
elif self.bitdepth == 16:
|
||
|
# Decompose into bytes
|
||
|
def extend(sl):
|
||
|
fmt = '!%dH' % len(sl)
|
||
|
data.extend(array('B', struct.pack(fmt, *sl)))
|
||
|
else:
|
||
|
# Pack into bytes
|
||
|
assert self.bitdepth < 8
|
||
|
# samples per byte
|
||
|
spb = int(8/self.bitdepth)
|
||
|
def extend(sl):
|
||
|
a = array('B', sl)
|
||
|
# Adding padding bytes so we can group into a whole
|
||
|
# number of spb-tuples.
|
||
|
l = float(len(a))
|
||
|
extra = math.ceil(l / float(spb))*spb - l
|
||
|
a.extend([0]*int(extra))
|
||
|
# Pack into bytes
|
||
|
l = group(a, spb)
|
||
|
l = map(lambda e: reduce(lambda x,y:
|
||
|
(x << self.bitdepth) + y, e), l)
|
||
|
data.extend(l)
|
||
|
if self.rescale:
|
||
|
oldextend = extend
|
||
|
factor = \
|
||
|
float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1)
|
||
|
def extend(sl):
|
||
|
oldextend(map(lambda x: int(round(factor*x)), sl))
|
||
|
|
||
|
# Build the first row, testing mostly to see if we need to
|
||
|
# changed the extend function to cope with NumPy integer types
|
||
|
# (they cause our ordinary definition of extend to fail, so we
|
||
|
# wrap it). See
|
||
|
# http://code.google.com/p/pypng/issues/detail?id=44
|
||
|
enumrows = enumerate(rows)
|
||
|
del rows
|
||
|
|
||
|
# First row's filter type.
|
||
|
data.append(0)
|
||
|
# :todo: Certain exceptions in the call to ``.next()`` or the
|
||
|
# following try would indicate no row data supplied.
|
||
|
# Should catch.
|
||
|
i,row = enumrows.next()
|
||
|
try:
|
||
|
# If this fails...
|
||
|
extend(row)
|
||
|
except:
|
||
|
# ... try a version that converts the values to int first.
|
||
|
# Not only does this work for the (slightly broken) NumPy
|
||
|
# types, there are probably lots of other, unknown, "nearly"
|
||
|
# int types it works for.
|
||
|
def wrapmapint(f):
|
||
|
return lambda sl: f(map(int, sl))
|
||
|
extend = wrapmapint(extend)
|
||
|
del wrapmapint
|
||
|
extend(row)
|
||
|
|
||
|
for i,row in enumrows:
|
||
|
# Add "None" filter type. Currently, it's essential that
|
||
|
# this filter type be used for every scanline as we do not
|
||
|
# mark the first row of a reduced pass image; that means we
|
||
|
# could accidentally compute the wrong filtered scanline if
|
||
|
# we used "up", "average", or "paeth" on such a line.
|
||
|
data.append(0)
|
||
|
extend(row)
|
||
|
if len(data) > self.chunk_limit:
|
||
|
compressed = compressor.compress(tostring(data))
|
||
|
if len(compressed):
|
||
|
# print >> sys.stderr, len(data), len(compressed)
|
||
|
write_chunk(outfile, 'IDAT', compressed)
|
||
|
# Because of our very witty definition of ``extend``,
|
||
|
# above, we must re-use the same ``data`` object. Hence
|
||
|
# we use ``del`` to empty this one, rather than create a
|
||
|
# fresh one (which would be my natural FP instinct).
|
||
|
del data[:]
|
||
|
if len(data):
|
||
|
compressed = compressor.compress(tostring(data))
|
||
|
else:
|
||
|
compressed = ''
|
||
|
flushed = compressor.flush()
|
||
|
if len(compressed) or len(flushed):
|
||
|
# print >> sys.stderr, len(data), len(compressed), len(flushed)
|
||
|
write_chunk(outfile, 'IDAT', compressed + flushed)
|
||
|
# http://www.w3.org/TR/PNG/#11IEND
|
||
|
write_chunk(outfile, 'IEND')
|
||
|
return i+1
|
||
|
|
||
|
def write_array(self, outfile, pixels):
|
||
|
"""
|
||
|
Write an array in flat row flat pixel format as a PNG file on
|
||
|
the output file. See also :meth:`write` method.
|
||
|
"""
|
||
|
|
||
|
if self.interlace:
|
||
|
self.write_passes(outfile, self.array_scanlines_interlace(pixels))
|
||
|
else:
|
||
|
self.write_passes(outfile, self.array_scanlines(pixels))
|
||
|
|
||
|
def write_packed(self, outfile, rows):
|
||
|
"""
|
||
|
Write PNG file to `outfile`. The pixel data comes from `rows`
|
||
|
which should be in boxed row packed format. Each row should be
|
||
|
a sequence of packed bytes.
|
||
|
|
||
|
Technically, this method does work for interlaced images but it
|
||
|
is best avoided. For interlaced images, the rows should be
|
||
|
presented in the order that they appear in the file.
|
||
|
|
||
|
This method should not be used when the source image bit depth
|
||
|
is not one naturally supported by PNG; the bit depth should be
|
||
|
1, 2, 4, 8, or 16.
|
||
|
"""
|
||
|
|
||
|
if self.rescale:
|
||
|
raise Error("write_packed method not suitable for bit depth %d" %
|
||
|
self.rescale[0])
|
||
|
return self.write_passes(outfile, rows, packed=True)
|
||
|
|
||
|
def convert_pnm(self, infile, outfile):
|
||
|
"""
|
||
|
Convert a PNM file containing raw pixel data into a PNG file
|
||
|
with the parameters set in the writer object. Works for
|
||
|
(binary) PGM, PPM, and PAM formats.
|
||
|
"""
|
||
|
|
||
|
if self.interlace:
|
||
|
pixels = array('B')
|
||
|
pixels.fromfile(infile,
|
||
|
(self.bitdepth/8) * self.color_planes *
|
||
|
self.width * self.height)
|
||
|
self.write_passes(outfile, self.array_scanlines_interlace(pixels))
|
||
|
else:
|
||
|
self.write_passes(outfile, self.file_scanlines(infile))
|
||
|
|
||
|
def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile):
|
||
|
"""
|
||
|
Convert a PPM and PGM file containing raw pixel data into a
|
||
|
PNG outfile with the parameters set in the writer object.
|
||
|
"""
|
||
|
pixels = array('B')
|
||
|
pixels.fromfile(ppmfile,
|
||
|
(self.bitdepth/8) * self.color_planes *
|
||
|
self.width * self.height)
|
||
|
apixels = array('B')
|
||
|
apixels.fromfile(pgmfile,
|
||
|
(self.bitdepth/8) *
|
||
|
self.width * self.height)
|
||
|
pixels = interleave_planes(pixels, apixels,
|
||
|
(self.bitdepth/8) * self.color_planes,
|
||
|
(self.bitdepth/8))
|
||
|
if self.interlace:
|
||
|
self.write_passes(outfile, self.array_scanlines_interlace(pixels))
|
||
|
else:
|
||
|
self.write_passes(outfile, self.array_scanlines(pixels))
|
||
|
|
||
|
def file_scanlines(self, infile):
|
||
|
"""
|
||
|
Generates boxed rows in flat pixel format, from the input file
|
||
|
`infile`. It assumes that the input file is in a "Netpbm-like"
|
||
|
binary format, and is positioned at the beginning of the first
|
||
|
pixel. The number of pixels to read is taken from the image
|
||
|
dimensions (`width`, `height`, `planes`) and the number of bytes
|
||
|
per value is implied by the image `bitdepth`.
|
||
|
"""
|
||
|
|
||
|
# Values per row
|
||
|
vpr = self.width * self.planes
|
||
|
row_bytes = vpr
|
||
|
if self.bitdepth > 8:
|
||
|
assert self.bitdepth == 16
|
||
|
row_bytes *= 2
|
||
|
fmt = '>%dH' % vpr
|
||
|
def line():
|
||
|
return array('H', struct.unpack(fmt, infile.read(row_bytes)))
|
||
|
else:
|
||
|
def line():
|
||
|
scanline = array('B', infile.read(row_bytes))
|
||
|
return scanline
|
||
|
for y in range(self.height):
|
||
|
yield line()
|
||
|
|
||
|
def array_scanlines(self, pixels):
|
||
|
"""
|
||
|
Generates boxed rows (flat pixels) from flat rows (flat pixels)
|
||
|
in an array.
|
||
|
"""
|
||
|
|
||
|
# Values per row
|
||
|
vpr = self.width * self.planes
|
||
|
stop = 0
|
||
|
for y in range(self.height):
|
||
|
start = stop
|
||
|
stop = start + vpr
|
||
|
yield pixels[start:stop]
|
||
|
|
||
|
def array_scanlines_interlace(self, pixels):
|
||
|
"""
|
||
|
Generator for interlaced scanlines from an array. `pixels` is
|
||
|
the full source image in flat row flat pixel format. The
|
||
|
generator yields each scanline of the reduced passes in turn, in
|
||
|
boxed row flat pixel format.
|
||
|
"""
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#8InterlaceMethods
|
||
|
# Array type.
|
||
|
fmt = 'BH'[self.bitdepth > 8]
|
||
|
# Value per row
|
||
|
vpr = self.width * self.planes
|
||
|
for xstart, ystart, xstep, ystep in _adam7:
|
||
|
if xstart >= self.width:
|
||
|
continue
|
||
|
# Pixels per row (of reduced image)
|
||
|
ppr = int(math.ceil((self.width-xstart)/float(xstep)))
|
||
|
# number of values in reduced image row.
|
||
|
row_len = ppr*self.planes
|
||
|
for y in range(ystart, self.height, ystep):
|
||
|
if xstep == 1:
|
||
|
offset = y * vpr
|
||
|
yield pixels[offset:offset+vpr]
|
||
|
else:
|
||
|
row = array(fmt)
|
||
|
# There's no easier way to set the length of an array
|
||
|
row.extend(pixels[0:row_len])
|
||
|
offset = y * vpr + xstart * self.planes
|
||
|
end_offset = (y+1) * vpr
|
||
|
skip = self.planes * xstep
|
||
|
for i in range(self.planes):
|
||
|
row[i::self.planes] = \
|
||
|
pixels[offset+i:end_offset:skip]
|
||
|
yield row
|
||
|
|
||
|
def write_chunk(outfile, tag, data=strtobytes('')):
|
||
|
"""
|
||
|
Write a PNG chunk to the output file, including length and
|
||
|
checksum.
|
||
|
"""
|
||
|
|
||
|
# http://www.w3.org/TR/PNG/#5Chunk-layout
|
||
|
outfile.write(struct.pack("!I", len(data)))
|
||
|
tag = strtobytes(tag)
|
||
|
outfile.write(tag)
|
||
|
outfile.write(data)
|
||
|
checksum = zlib.crc32(tag)
|
||
|
checksum = zlib.crc32(data, checksum)
|
||
|
checksum &= 2**32-1
|
||
|
outfile.write(struct.pack("!I", checksum))
|
||
|
|
||
|
def write_chunks(out, chunks):
|
||
|
"""Create a PNG file by writing out the chunks."""
|
||
|
|
||
|
out.write(_signature)
|
||
|
for chunk in chunks:
|
||
|
write_chunk(out, *chunk)
|
||
|
|
||
|
def filter_scanline(type, line, fo, prev=None):
|
||
|
"""Apply a scanline filter to a scanline. `type` specifies the
|
||
|
filter type (0 to 4); `line` specifies the current (unfiltered)
|
||
|
scanline as a sequence of bytes; `prev` specifies the previous
|
||
|
(unfiltered) scanline as a sequence of bytes. `fo` specifies the
|
||
|
filter offset; normally this is size of a pixel in bytes (the number
|
||
|
of bytes per sample times the number of channels), but when this is
|
||
|
< 1 (for bit depths < 8) then the filter offset is 1.
|
||
|
"""
|
||
|
|
||
|
assert 0 <= type < 5
|
||
|
|
||
|
# The output array. Which, pathetically, we extend one-byte at a
|
||
|
# time (fortunately this is linear).
|
||
|
out = array('B', [type])
|
||
|
|
||
|
def sub():
|
||
|
ai = -fo
|
||
|
for x in line:
|
||
|
if ai >= 0:
|
||
|
x = (x - line[ai]) & 0xff
|
||
|
out.append(x)
|
||
|
ai += 1
|
||
|
def up():
|
||
|
for i,x in enumerate(line):
|
||
|
x = (x - prev[i]) & 0xff
|
||
|
out.append(x)
|
||
|
def average():
|
||
|
ai = -fo
|
||
|
for i,x in enumerate(line):
|
||
|
if ai >= 0:
|
||
|
x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff
|
||
|
else:
|
||
|
x = (x - (prev[i] >> 1)) & 0xff
|
||
|
out.append(x)
|
||
|
ai += 1
|
||
|
def paeth():
|
||
|
# http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth
|
||
|
ai = -fo # also used for ci
|
||
|
for i,x in enumerate(line):
|
||
|
a = 0
|
||
|
b = prev[i]
|
||
|
c = 0
|
||
|
|
||
|
if ai >= 0:
|
||
|
a = line[ai]
|
||
|
c = prev[ai]
|
||
|
p = a + b - c
|
||
|
pa = abs(p - a)
|
||
|
pb = abs(p - b)
|
||
|
pc = abs(p - c)
|
||
|
if pa <= pb and pa <= pc: Pr = a
|
||
|
elif pb <= pc: Pr = b
|
||
|
else: Pr = c
|
||
|
|
||
|
x = (x - Pr) & 0xff
|
||
|
out.append(x)
|
||
|
ai += 1
|
||
|
|
||
|
if not prev:
|
||
|
# We're on the first line. Some of the filters can be reduced
|
||
|
# to simpler cases which makes handling the line "off the top"
|
||
|
# of the image simpler. "up" becomes "none"; "paeth" becomes
|
||
|
# "left" (non-trivial, but true). "average" needs to be handled
|
||
|
# specially.
|
||
|
if type == 2: # "up"
|
||
|
return line # type = 0
|
||
|
elif type == 3:
|
||
|
prev = [0]*len(line)
|
||
|
elif type == 4: # "paeth"
|
||
|
type = 1
|
||
|
if type == 0:
|
||
|
out.extend(line)
|
||
|
elif type == 1:
|
||
|
sub()
|
||
|
elif type == 2:
|
||
|
up()
|
||
|
elif type == 3:
|
||
|
average()
|
||
|
else: # type == 4
|
||
|
paeth()
|
||
|
|