2334 lines
80 KiB
Python
2334 lines
80 KiB
Python
#!/usr/bin/env python
|
|
|
|
# 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.
|
|
#
|
|
# LICENCE (MIT)
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
The ``png`` module can read and write PNG files.
|
|
|
|
Installation and Overview
|
|
-------------------------
|
|
|
|
``pip install pypng``
|
|
|
|
For help, type ``import png; help(png)`` in your python interpreter.
|
|
|
|
A good place to start is the :class:`Reader` and :class:`Writer` classes.
|
|
|
|
Coverage of PNG formats is fairly complete;
|
|
all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and
|
|
colour combinations are supported:
|
|
|
|
- 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).
|
|
|
|
Interlaced images,
|
|
which support a progressive display when downloading,
|
|
are supported for both reading and writing.
|
|
|
|
A number of optional chunks can be specified (when writing)
|
|
and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
|
|
|
|
The ``sBIT`` chunk can be used to specify precision for
|
|
non-native bit depths.
|
|
|
|
Requires Python 3.5 or higher.
|
|
Installation is trivial,
|
|
but see the ``README.txt`` file (with the source distribution) for details.
|
|
|
|
Full use of all features will need some reading of the PNG specification
|
|
http://www.w3.org/TR/2003/REC-PNG-20031110/.
|
|
|
|
The package also comes with command line utilities.
|
|
|
|
- ``pripamtopng`` converts
|
|
`Netpbm <http://netpbm.sourceforge.net/>`_ PAM/PNM files to PNG;
|
|
- ``pripngtopam`` converts PNG to file PAM/PNM.
|
|
|
|
There are a few more for simple PNG manipulations.
|
|
|
|
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.
|
|
|
|
Colour Models
|
|
-------------
|
|
|
|
The major colour models supported by PNG (and hence by PyPNG) are:
|
|
|
|
- greyscale;
|
|
- greyscale--alpha;
|
|
- RGB;
|
|
- RGB--alpha.
|
|
|
|
Also referred to using the abbreviations: L, LA, RGB, RGBA.
|
|
Each letter codes a single channel:
|
|
*L* is for Luminance or Luma or Lightness (greyscale images);
|
|
*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);
|
|
*R*, *G*, *B* stand for Red, Green, Blue (colour image).
|
|
|
|
Lists, arrays, sequences, and so on
|
|
-----------------------------------
|
|
|
|
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.
|
|
|
|
The preferred format is a sequence of *rows*,
|
|
which each row being a sequence of *values*.
|
|
In this format, the values are in pixel order,
|
|
with all the values from all the pixels in a row
|
|
being concatenated into a single sequence for that row.
|
|
|
|
Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
|
|
has RGB components:
|
|
|
|
Sequence of rows::
|
|
|
|
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 preferred 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 (``bytearray`` or ``array.array``).
|
|
|
|
To allow streaming the outer list is sometimes
|
|
an iterator rather than an explicit list.
|
|
|
|
An alternative format is a single array holding all the values.
|
|
|
|
Array of values::
|
|
|
|
[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.
|
|
|
|
The top row comes first,
|
|
and within 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 another format, which should only be used with caution.
|
|
It is 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 the same as a sequence of rows;
|
|
when the bit depth is less than 8 (1, 2 and 4),
|
|
several pixels are packed into each byte;
|
|
when the bit depth is 16 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).
|
|
"""
|
|
|
|
__version__ = "0.0.20"
|
|
|
|
import collections
|
|
import io # For io.BytesIO
|
|
import itertools
|
|
import math
|
|
# http://www.python.org/doc/2.4.4/lib/module-operator.html
|
|
import operator
|
|
import re
|
|
import struct
|
|
import sys
|
|
# http://www.python.org/doc/2.4.4/lib/module-warnings.html
|
|
import warnings
|
|
import zlib
|
|
|
|
from array import array
|
|
|
|
|
|
__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)
|
|
|
|
# The xstart, ystart, xstep, ystep for the Adam7 interlace passes.
|
|
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 adam7_generate(width, height):
|
|
"""
|
|
Generate the coordinates for the reduced scanlines
|
|
of an Adam7 interlaced image
|
|
of size `width` by `height` pixels.
|
|
|
|
Yields a generator for each pass,
|
|
and each pass generator yields a series of (x, y, xstep) triples,
|
|
each one identifying a reduced scanline consisting of
|
|
pixels starting at (x, y) and taking every xstep pixel to the right.
|
|
"""
|
|
|
|
for xstart, ystart, xstep, ystep in adam7:
|
|
if xstart >= width:
|
|
continue
|
|
yield ((xstart, y, xstep) for y in range(ystart, height, ystep))
|
|
|
|
|
|
# Models the 'pHYs' chunk (used by the Reader)
|
|
Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter')
|
|
|
|
|
|
def group(s, n):
|
|
return list(zip(* [iter(s)] * n))
|
|
|
|
|
|
def isarray(x):
|
|
return isinstance(x, array)
|
|
|
|
|
|
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 ProtocolError(
|
|
"a palette must have between 1 and 256 entries,"
|
|
" see https://www.w3.org/TR/PNG/#11PLTE")
|
|
seen_triple = False
|
|
for i, t in enumerate(p):
|
|
if len(t) not in (3, 4):
|
|
raise ProtocolError(
|
|
f"palette entry {i}: entries must be 3- or 4-tuples.")
|
|
if len(t) == 3:
|
|
seen_triple = True
|
|
if seen_triple and len(t) == 4:
|
|
raise ProtocolError(
|
|
f"palette entry {i}: all 4-tuples must precede all 3-tuples")
|
|
for x in t:
|
|
if int(x) != x or not(0 <= x <= 255):
|
|
raise ProtocolError(
|
|
f"palette entry {i}: "
|
|
"values must be integer: 0 <= x <= 255")
|
|
return p
|
|
|
|
|
|
def check_sizes(size, width, height):
|
|
"""
|
|
Check that these arguments, if supplied, are consistent.
|
|
Return a (width, height) pair.
|
|
"""
|
|
|
|
if not size:
|
|
return width, height
|
|
|
|
if len(size) != 2:
|
|
raise ProtocolError(
|
|
"size argument should be a pair (width, height)")
|
|
if width is not None and width != size[0]:
|
|
raise ProtocolError(
|
|
f"size[0] ({size[0]}) and width ({width}) should match when both are used.")
|
|
if height is not None and height != size[1]:
|
|
raise ProtocolError(
|
|
f"size[1] ({size[1]}) and height ({height}) should match when both are used.")
|
|
return size
|
|
|
|
|
|
def check_color(c, greyscale, which):
|
|
"""
|
|
Checks that a colour argument for transparent or background options
|
|
is the right form.
|
|
Returns the colour
|
|
(which, if it's a bare integer, is "corrected" to a 1-tuple).
|
|
"""
|
|
|
|
if c is None:
|
|
return c
|
|
if greyscale:
|
|
try:
|
|
len(c)
|
|
except TypeError:
|
|
c = (c,)
|
|
if len(c) != 1:
|
|
raise ProtocolError(f"{which} for greyscale must be 1-tuple")
|
|
if not is_natural(c[0]):
|
|
raise ProtocolError(
|
|
f"{which} colour for greyscale must be integer")
|
|
else:
|
|
if not (len(c) == 3 and
|
|
is_natural(c[0]) and
|
|
is_natural(c[1]) and
|
|
is_natural(c[2])):
|
|
raise ProtocolError(
|
|
f"{which} colour must be a triple of integers")
|
|
return c
|
|
|
|
|
|
class Error(Exception):
|
|
def __str__(self):
|
|
return self.__class__.__name__ + ': ' + ' '.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.
|
|
"""
|
|
|
|
|
|
class ProtocolError(Error):
|
|
"""
|
|
Problem with the way the programming interface has been used,
|
|
or the data presented to it.
|
|
"""
|
|
|
|
|
|
class ChunkError(FormatError):
|
|
pass
|
|
|
|
|
|
class Default:
|
|
"""The default for the greyscale paramter."""
|
|
|
|
|
|
class Writer:
|
|
"""
|
|
PNG encoder in pure Python.
|
|
"""
|
|
|
|
def __init__(self, width=None, height=None,
|
|
size=None,
|
|
greyscale=Default,
|
|
alpha=False,
|
|
bitdepth=8,
|
|
palette=None,
|
|
transparent=None,
|
|
background=None,
|
|
gamma=None,
|
|
compression=None,
|
|
interlace=False,
|
|
planes=None,
|
|
colormap=None,
|
|
maxval=None,
|
|
chunk_limit=2**20,
|
|
x_pixels_per_unit=None,
|
|
y_pixels_per_unit=None,
|
|
unit_is_meter=False):
|
|
"""
|
|
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
|
|
Pixels are greyscale, not RGB.
|
|
alpha
|
|
Input data has alpha channel (RGBA or LA).
|
|
bitdepth
|
|
Bit depth: from 1 to 16 (for each channel).
|
|
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: 0 (none) to 9 (more compressed);
|
|
default: -1 or None.
|
|
interlace
|
|
Create an interlaced image.
|
|
chunk_limit
|
|
Write multiple ``IDAT`` chunks to save memory.
|
|
x_pixels_per_unit
|
|
Number of pixels a unit along the x axis (write a
|
|
`pHYs` chunk).
|
|
y_pixels_per_unit
|
|
Number of pixels a unit along the y axis (write a
|
|
`pHYs` chunk). Along with `x_pixel_unit`, this gives
|
|
the pixel size ratio.
|
|
unit_is_meter
|
|
`True` to indicate that the unit (for the `pHYs`
|
|
chunk) is metre.
|
|
|
|
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*).
|
|
|
|
The `greyscale` argument indicates whether input pixels
|
|
are greyscale (when true), or colour (when false).
|
|
The default is true unless `palette=` is used.
|
|
|
|
The `alpha` argument (a boolean) specifies
|
|
whether input pixels have an alpha channel (or not).
|
|
|
|
`bitdepth` specifies the bit depth of the source pixel values.
|
|
Each channel may have a different bit depth.
|
|
Each source pixel must have values that are
|
|
an integer between 0 and ``2**bitdepth-1``, where
|
|
`bitdepth` is the bit depth for the corresponding channel.
|
|
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 (the same for all channels).
|
|
When `bitdepth` is not one of these values or where
|
|
channels have different bit depths,
|
|
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 PNG file format supports many bit depth / colour model
|
|
combinations, but not all.
|
|
The details are somewhat arcane
|
|
(refer to the PNG specification for full details).
|
|
Briefly:
|
|
Bit depths < 8 (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 use the `bitdepth` argument 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 true; `alpha` must not be true;
|
|
`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.
|
|
All the 4-tuples (if present) 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 one element for each channel in the image.
|
|
Either a 3-tuple of integer (RGB) values for a colour image, or
|
|
a 1-tuple of a single integer 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.
|
|
Values from 1 to 9 (highest) specify compression.
|
|
0 means no compression.
|
|
-1 and ``None`` both mean that the ``zlib`` module uses
|
|
the default level of compession (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 passed in,
|
|
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`.
|
|
|
|
width, height = check_sizes(size, width, height)
|
|
del size
|
|
|
|
if not is_natural(width) or not is_natural(height):
|
|
raise ProtocolError("width and height must be integers")
|
|
if width <= 0 or height <= 0:
|
|
raise ProtocolError("width and height must be greater than zero")
|
|
# http://www.w3.org/TR/PNG/#7Integers-and-byte-order
|
|
if width > 2 ** 31 - 1 or height > 2 ** 31 - 1:
|
|
raise ProtocolError("width and height cannot exceed 2**31-1")
|
|
|
|
if alpha and transparent is not None:
|
|
raise ProtocolError(
|
|
"transparent colour not allowed with alpha channel")
|
|
|
|
# bitdepth is either single integer, or tuple of integers.
|
|
# Convert to tuple.
|
|
try:
|
|
len(bitdepth)
|
|
except TypeError:
|
|
bitdepth = (bitdepth, )
|
|
for b in bitdepth:
|
|
valid = is_natural(b) and 1 <= b <= 16
|
|
if not valid:
|
|
raise ProtocolError(
|
|
f"each bitdepth {bitdepth} must be a positive integer <= 16")
|
|
|
|
# Calculate channels, and
|
|
# expand bitdepth to be one element per channel.
|
|
palette = check_palette(palette)
|
|
alpha = bool(alpha)
|
|
colormap = bool(palette)
|
|
if greyscale is Default and palette:
|
|
greyscale = False
|
|
greyscale = bool(greyscale)
|
|
if colormap:
|
|
color_planes = 1
|
|
planes = 1
|
|
else:
|
|
color_planes = (3, 1)[greyscale]
|
|
planes = color_planes + alpha
|
|
if len(bitdepth) == 1:
|
|
bitdepth *= planes
|
|
|
|
bitdepth, self.rescale = check_bitdepth_rescale(
|
|
palette,
|
|
bitdepth,
|
|
transparent, alpha, greyscale)
|
|
|
|
# These are assertions, because above logic should have
|
|
# corrected or raised all problematic cases.
|
|
if bitdepth < 8:
|
|
assert greyscale or palette
|
|
assert not alpha
|
|
if bitdepth > 8:
|
|
assert not palette
|
|
|
|
transparent = check_color(transparent, greyscale, 'transparent')
|
|
background = check_color(background, greyscale, '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 = greyscale
|
|
self.alpha = alpha
|
|
self.colormap = colormap
|
|
self.bitdepth = int(bitdepth)
|
|
self.compression = compression
|
|
self.chunk_limit = chunk_limit
|
|
self.interlace = bool(interlace)
|
|
self.palette = palette
|
|
self.x_pixels_per_unit = x_pixels_per_unit
|
|
self.y_pixels_per_unit = y_pixels_per_unit
|
|
self.unit_is_meter = bool(unit_is_meter)
|
|
|
|
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 = color_planes
|
|
self.planes = planes
|
|
# :todo: fix for bitdepth < 8
|
|
self.psize = (self.bitdepth / 8) * self.planes
|
|
|
|
def write(self, outfile, rows):
|
|
"""
|
|
Write a PNG image to the output file.
|
|
`rows` should be an iterable that yields each row
|
|
(each row is a sequence of values).
|
|
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 requires the entire image to be in working memory.
|
|
"""
|
|
|
|
# Values per row
|
|
vpr = self.width * self.planes
|
|
|
|
def check_rows(rows):
|
|
"""
|
|
Yield each row in rows,
|
|
but check each row first (for correct width).
|
|
"""
|
|
for i, row in enumerate(rows):
|
|
try:
|
|
wrong_length = len(row) != vpr
|
|
except TypeError:
|
|
# When using an itertools.ichain object or
|
|
# other generator not supporting __len__,
|
|
# we set this to False to skip the check.
|
|
wrong_length = False
|
|
if wrong_length:
|
|
# Note: row numbers start at 0.
|
|
raise ProtocolError(
|
|
f"Expected {vpr} values but got {len(row)} values, in row {i}")
|
|
yield row
|
|
|
|
if self.interlace:
|
|
fmt = 'BH'[self.bitdepth > 8]
|
|
a = array(fmt, itertools.chain(*check_rows(rows)))
|
|
return self.write_array(outfile, a)
|
|
|
|
nrows = self.write_passes(outfile, check_rows(rows))
|
|
if nrows != self.height:
|
|
raise ProtocolError(
|
|
f"rows supplied ({nrows}) does not match height ({self.height})")
|
|
return nrows
|
|
|
|
def write_passes(self, outfile, rows):
|
|
"""
|
|
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.
|
|
For interlaced images the rows should have been interlaced before
|
|
passing them to this function.
|
|
|
|
`rows` should be an iterable that yields each row
|
|
(each row being a sequence of values).
|
|
"""
|
|
|
|
# Ensure rows are scaled (to 4-/8-/16-bit),
|
|
# and packed into bytes.
|
|
|
|
if self.rescale:
|
|
rows = rescale_rows(rows, self.rescale)
|
|
|
|
if self.bitdepth < 8:
|
|
rows = pack_rows(rows, self.bitdepth)
|
|
elif self.bitdepth == 16:
|
|
rows = unpack_rows(rows)
|
|
|
|
return self.write_packed(outfile, rows)
|
|
|
|
def write_packed(self, outfile, rows):
|
|
"""
|
|
Write PNG file to `outfile`.
|
|
`rows` should be an iterator that yields each packed row;
|
|
a packed row being a sequence of packed bytes.
|
|
|
|
The rows have a filter byte prefixed and
|
|
are then compressed into one or more IDAT chunks.
|
|
They are not processed any further,
|
|
so if bitdepth is other than 1, 2, 4, 8, 16,
|
|
the pixel values should have been scaled
|
|
before passing them to this method.
|
|
|
|
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.
|
|
"""
|
|
|
|
self.write_preamble(outfile)
|
|
|
|
# http://www.w3.org/TR/PNG/#11IDAT
|
|
if self.compression is not None:
|
|
compressor = zlib.compressobj(self.compression)
|
|
else:
|
|
compressor = zlib.compressobj()
|
|
|
|
# data accumulates bytes to be compressed for the IDAT chunk;
|
|
# it's compressed when sufficiently large.
|
|
data = bytearray()
|
|
|
|
# raise i scope out of the for loop. set to -1, because the for loop
|
|
# sets i to 0 on the first pass
|
|
i = -1
|
|
for i, row in enumerate(rows):
|
|
# 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)
|
|
data.extend(row)
|
|
if len(data) > self.chunk_limit:
|
|
compressed = compressor.compress(data)
|
|
if len(compressed):
|
|
write_chunk(outfile, b'IDAT', compressed)
|
|
data = bytearray()
|
|
|
|
compressed = compressor.compress(bytes(data))
|
|
flushed = compressor.flush()
|
|
if len(compressed) or len(flushed):
|
|
write_chunk(outfile, b'IDAT', compressed + flushed)
|
|
# http://www.w3.org/TR/PNG/#11IEND
|
|
write_chunk(outfile, b'IEND')
|
|
return i + 1
|
|
|
|
def write_preamble(self, outfile):
|
|
# http://www.w3.org/TR/PNG/#5PNG-file-signature
|
|
outfile.write(signature)
|
|
|
|
# http://www.w3.org/TR/PNG/#11IHDR
|
|
write_chunk(outfile, b'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, b'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, b'sBIT',
|
|
struct.pack(f'{self.planes,* [s[0] for s in self.rescale]}B' ))
|
|
|
|
# :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 = make_palette_chunks(self.palette)
|
|
write_chunk(outfile, b'PLTE', p)
|
|
if t:
|
|
# tRNS chunk is optional;
|
|
# Only needed if palette entries have alpha.
|
|
write_chunk(outfile, b'tRNS', t)
|
|
|
|
# http://www.w3.org/TR/PNG/#11tRNS
|
|
if self.transparent is not None:
|
|
if self.greyscale:
|
|
fmt = "!1H"
|
|
else:
|
|
fmt = "!3H"
|
|
write_chunk(outfile, b'tRNS',
|
|
struct.pack(fmt, *self.transparent))
|
|
|
|
# http://www.w3.org/TR/PNG/#11bKGD
|
|
if self.background is not None:
|
|
if self.greyscale:
|
|
fmt = "!1H"
|
|
else:
|
|
fmt = "!3H"
|
|
write_chunk(outfile, b'bKGD',
|
|
struct.pack(fmt, *self.background))
|
|
|
|
# http://www.w3.org/TR/PNG/#11pHYs
|
|
if (self.x_pixels_per_unit is not None and
|
|
self.y_pixels_per_unit is not None):
|
|
tup = (self.x_pixels_per_unit,
|
|
self.y_pixels_per_unit,
|
|
int(self.unit_is_meter))
|
|
write_chunk(outfile, b'pHYs', struct.pack("!LLB", *tup))
|
|
|
|
def write_array(self, outfile, pixels):
|
|
"""
|
|
Write an array that holds all the image values
|
|
as a PNG file on the output file.
|
|
See also :meth:`write` method.
|
|
"""
|
|
|
|
if self.interlace:
|
|
if type(pixels) != array:
|
|
# Coerce to array type
|
|
fmt = 'BH'[self.bitdepth > 8]
|
|
pixels = array(fmt, pixels)
|
|
return self.write_passes(
|
|
outfile,
|
|
self.array_scanlines_interlace(pixels)
|
|
)
|
|
else:
|
|
return self.write_passes(
|
|
outfile,
|
|
self.array_scanlines(pixels)
|
|
)
|
|
|
|
def array_scanlines(self, pixels):
|
|
"""
|
|
Generates rows (each a sequence of values) from
|
|
a single array of values.
|
|
"""
|
|
|
|
# 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 as a single array of values.
|
|
The generator yields each scanline of the reduced passes in turn,
|
|
each scanline being a sequence of values.
|
|
"""
|
|
|
|
# http://www.w3.org/TR/PNG/#8InterlaceMethods
|
|
# Array type.
|
|
fmt = 'BH'[self.bitdepth > 8]
|
|
# Value per row
|
|
vpr = self.width * self.planes
|
|
|
|
# Each iteration generates a scanline starting at (x, y)
|
|
# and consisting of every xstep pixels.
|
|
for lines in adam7_generate(self.width, self.height):
|
|
for x, y, xstep in lines:
|
|
# Pixels per row (of reduced image)
|
|
ppr = int(math.ceil((self.width - x) / float(xstep)))
|
|
# Values per row (of reduced image)
|
|
reduced_row_len = ppr * self.planes
|
|
if xstep == 1:
|
|
# Easy case: line is a simple slice.
|
|
offset = y * vpr
|
|
yield pixels[offset: offset + vpr]
|
|
continue
|
|
# We have to step by xstep,
|
|
# which we can do one plane at a time
|
|
# using the step in Python slices.
|
|
row = array(fmt)
|
|
# There's no easier way to set the length of an array
|
|
row.extend(pixels[0:reduced_row_len])
|
|
offset = y * vpr + x * 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=b''):
|
|
"""
|
|
Write a PNG chunk to the output file, including length and
|
|
checksum.
|
|
"""
|
|
|
|
data = bytes(data)
|
|
# http://www.w3.org/TR/PNG/#5Chunk-layout
|
|
outfile.write(struct.pack("!I", len(data)))
|
|
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 rescale_rows(rows, rescale):
|
|
"""
|
|
Take each row in rows (an iterator) and yield
|
|
a fresh row with the pixels scaled according to
|
|
the rescale parameters in the list `rescale`.
|
|
Each element of `rescale` is a tuple of
|
|
(source_bitdepth, target_bitdepth),
|
|
with one element per channel.
|
|
"""
|
|
|
|
# One factor for each channel
|
|
fs = [float(2 ** s[1] - 1)/float(2 ** s[0] - 1)
|
|
for s in rescale]
|
|
|
|
# Assume all target_bitdepths are the same
|
|
target_bitdepths = set(s[1] for s in rescale)
|
|
assert len(target_bitdepths) == 1
|
|
(target_bitdepth, ) = target_bitdepths
|
|
typecode = 'BH'[target_bitdepth > 8]
|
|
|
|
# Number of channels
|
|
n_chans = len(rescale)
|
|
|
|
for row in rows:
|
|
rescaled_row = array(typecode, iter(row))
|
|
for i in range(n_chans):
|
|
channel = array(
|
|
typecode,
|
|
(int(round(fs[i] * x)) for x in row[i::n_chans]))
|
|
rescaled_row[i::n_chans] = channel
|
|
yield rescaled_row
|
|
|
|
|
|
def pack_rows(rows, bitdepth):
|
|
"""Yield packed rows that are a byte array.
|
|
Each byte is packed with the values from several pixels.
|
|
"""
|
|
|
|
assert bitdepth < 8
|
|
assert 8 % bitdepth == 0
|
|
|
|
# samples per byte
|
|
spb = int(8 / bitdepth)
|
|
|
|
def make_byte(block):
|
|
"""Take a block of (2, 4, or 8) values,
|
|
and pack them into a single byte.
|
|
"""
|
|
|
|
res = 0
|
|
for v in block:
|
|
res = (res << bitdepth) + v
|
|
return res
|
|
|
|
for row in rows:
|
|
a = bytearray(row)
|
|
# Adding padding bytes so we can group into a whole
|
|
# number of spb-tuples.
|
|
n = float(len(a))
|
|
extra = math.ceil(n / spb) * spb - n
|
|
a.extend([0] * int(extra))
|
|
# Pack into bytes.
|
|
# Each block is the samples for one byte.
|
|
blocks = group(a, spb)
|
|
yield bytearray(make_byte(block) for block in blocks)
|
|
|
|
|
|
def unpack_rows(rows):
|
|
"""Unpack each row from being 16-bits per value,
|
|
to being a sequence of bytes.
|
|
"""
|
|
for row in rows:
|
|
fmt = f'!{len(row)}'
|
|
yield bytearray(struct.pack(fmt, *row))
|
|
|
|
|
|
def make_palette_chunks(palette):
|
|
"""
|
|
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 = bytearray()
|
|
t = bytearray()
|
|
|
|
for x in palette:
|
|
p.extend(x[0:3])
|
|
if len(x) > 3:
|
|
t.append(x[3])
|
|
if t:
|
|
return p, t
|
|
return p, None
|
|
|
|
|
|
def check_bitdepth_rescale(
|
|
palette, bitdepth, transparent, alpha, greyscale):
|
|
"""
|
|
Returns (bitdepth, rescale) pair.
|
|
"""
|
|
|
|
if palette:
|
|
if len(bitdepth) != 1:
|
|
raise ProtocolError(
|
|
"with palette, only a single bitdepth may be used")
|
|
(bitdepth, ) = bitdepth
|
|
if bitdepth not in (1, 2, 4, 8):
|
|
raise ProtocolError(
|
|
"with palette, bitdepth must be 1, 2, 4, or 8")
|
|
if transparent is not None:
|
|
raise ProtocolError("transparent and palette not compatible")
|
|
if alpha:
|
|
raise ProtocolError("alpha and palette not compatible")
|
|
if greyscale:
|
|
raise ProtocolError("greyscale and palette not compatible")
|
|
return bitdepth, None
|
|
|
|
# No palette, check for sBIT chunk generation.
|
|
|
|
if greyscale and not alpha:
|
|
# Single channel, L.
|
|
(bitdepth,) = bitdepth
|
|
if bitdepth in (1, 2, 4, 8, 16):
|
|
return bitdepth, None
|
|
if bitdepth > 8:
|
|
targetbitdepth = 16
|
|
elif bitdepth == 3:
|
|
targetbitdepth = 4
|
|
else:
|
|
assert bitdepth in (5, 6, 7)
|
|
targetbitdepth = 8
|
|
return targetbitdepth, [(bitdepth, targetbitdepth)]
|
|
|
|
assert alpha or not greyscale
|
|
|
|
depth_set = tuple(set(bitdepth))
|
|
if depth_set in [(8,), (16,)]:
|
|
# No sBIT required.
|
|
(bitdepth, ) = depth_set
|
|
return bitdepth, None
|
|
|
|
targetbitdepth = (8, 16)[max(bitdepth) > 8]
|
|
return targetbitdepth, [(b, targetbitdepth) for b in bitdepth]
|
|
|
|
|
|
# Regex for decoding mode string
|
|
RegexModeDecode = re.compile("(LA?|RGBA?);?([0-9]*)", flags=re.IGNORECASE)
|
|
|
|
|
|
def from_array(a, mode=None, info={}):
|
|
"""
|
|
Create a PNG :class:`Image` object from a 2-dimensional array.
|
|
One application of this function is easy PIL-style saving:
|
|
``png.from_array(pixels, 'L').save('foo.png')``.
|
|
|
|
Unless they are specified using the *info* parameter,
|
|
the PNG's height and width are taken from the array size.
|
|
The first axis is the height; the second axis is the
|
|
ravelled width and channel index.
|
|
The array is treated is a sequence of rows,
|
|
each row being a sequence of values (``width*channels`` in number).
|
|
So an RGB image that is 16 pixels high and 8 wide will
|
|
occupy a 2-dimensional array that is 16x24
|
|
(each row will be 8*3 = 24 sample values).
|
|
|
|
*mode* is a string that specifies the image colour format in a
|
|
PIL-style mode. It can be:
|
|
|
|
``'L'``
|
|
greyscale (1 channel)
|
|
``'LA'``
|
|
greyscale with alpha (2 channel)
|
|
``'RGB'``
|
|
colour image (3 channel)
|
|
``'RGBA'``
|
|
colour image with alpha (4 channel)
|
|
|
|
The mode string can also specify the bit depth
|
|
(overriding how this function normally derives the bit depth,
|
|
see below).
|
|
Appending ``';16'`` to the mode will cause the PNG to be
|
|
16 bits per channel;
|
|
any decimal from 1 to 16 can be used to specify the bit depth.
|
|
|
|
When a 2-dimensional array is used *mode* determines how many
|
|
channels the image has, and so allows the width to be derived from
|
|
the second array dimension.
|
|
|
|
The array is expected to be a ``numpy`` array,
|
|
but it can be any suitable Python sequence.
|
|
For example, a list of lists can be used:
|
|
``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``.
|
|
The exact rules are: ``len(a)`` gives the first dimension, height;
|
|
``len(a[0])`` gives the second dimension.
|
|
It's slightly more complicated than that because
|
|
an iterator of rows can be used, and it all still works.
|
|
Using an iterator allows data to be streamed efficiently.
|
|
|
|
The bit depth of the PNG is normally taken from
|
|
the array element's datatype
|
|
(but if *mode* specifies a bitdepth then that is used instead).
|
|
The array element's datatype is determined in a way which
|
|
is supposed to work both for ``numpy`` arrays and for Python
|
|
``array.array`` objects.
|
|
A 1 byte datatype will give a bit depth of 8,
|
|
a 2 byte datatype will give a bit depth of 16.
|
|
If the datatype does not have an implicit size,
|
|
like the above example where it is a plain Python list of lists,
|
|
then a default of 8 is used.
|
|
|
|
The *info* parameter is a dictionary that can
|
|
be used to specify metadata (in the same style as
|
|
the arguments to the :class:`png.Writer` class).
|
|
For this function the keys that are useful are:
|
|
|
|
height
|
|
overrides the height derived from the array dimensions and
|
|
allows *a* to be an iterable.
|
|
width
|
|
overrides the width derived from the array dimensions.
|
|
bitdepth
|
|
overrides the bit depth derived from the element datatype
|
|
(but must match *mode* if that also specifies a bit depth).
|
|
|
|
Generally anything specified in the *info* dictionary will
|
|
override any implicit choices that this function would otherwise make,
|
|
but must match any explicit ones.
|
|
For example, if the *info* dictionary has a ``greyscale`` key then
|
|
this must be true when mode is ``'L'`` or ``'LA'`` and
|
|
false when mode is ``'RGB'`` or ``'RGBA'``.
|
|
"""
|
|
|
|
# We abuse the *info* parameter by modifying it. Take a copy here.
|
|
# (Also typechecks *info* to some extent).
|
|
info = dict(info)
|
|
|
|
# Syntax check mode string.
|
|
match = RegexModeDecode.match(mode)
|
|
if not match:
|
|
raise Error("mode string should be 'RGB' or 'L;16' or similar.")
|
|
|
|
mode, bitdepth = match.groups()
|
|
if bitdepth:
|
|
bitdepth = int(bitdepth)
|
|
|
|
# Colour format.
|
|
if 'greyscale' in info:
|
|
if bool(info['greyscale']) != ('L' in mode):
|
|
raise ProtocolError("info['greyscale'] should match mode.")
|
|
info['greyscale'] = 'L' in mode
|
|
|
|
alpha = 'A' in mode
|
|
if 'alpha' in info:
|
|
if bool(info['alpha']) != alpha:
|
|
raise ProtocolError("info['alpha'] should match mode.")
|
|
info['alpha'] = alpha
|
|
|
|
# Get bitdepth from *mode* if possible.
|
|
if bitdepth:
|
|
if info.get("bitdepth") and bitdepth != info['bitdepth']:
|
|
raise ProtocolError(
|
|
f"bitdepth ({bitdepth}) should match bitdepth of info ({info[bitdepth]}).")
|
|
info['bitdepth'] = bitdepth
|
|
|
|
# Fill in and/or check entries in *info*.
|
|
# Dimensions.
|
|
width, height = check_sizes(
|
|
info.get("size"),
|
|
info.get("width"),
|
|
info.get("height"))
|
|
if width:
|
|
info["width"] = width
|
|
if height:
|
|
info["height"] = height
|
|
|
|
if "height" not in info:
|
|
try:
|
|
info['height'] = len(a)
|
|
except TypeError:
|
|
raise ProtocolError(
|
|
"len(a) does not work, supply info['height'] instead.")
|
|
|
|
planes = len(mode)
|
|
if 'planes' in info:
|
|
if info['planes'] != planes:
|
|
raise Error("info['planes'] should match mode.")
|
|
|
|
# In order to work out whether we the array is 2D or 3D we need its
|
|
# first row, which requires that we take a copy of its iterator.
|
|
# We may also need the first row to derive width and bitdepth.
|
|
a, t = itertools.tee(a)
|
|
row = next(t)
|
|
del t
|
|
|
|
testelement = row
|
|
if 'width' not in info:
|
|
width = len(row) // planes
|
|
info['width'] = width
|
|
|
|
if 'bitdepth' not in info:
|
|
try:
|
|
dtype = testelement.dtype
|
|
# goto the "else:" clause. Sorry.
|
|
except AttributeError:
|
|
try:
|
|
# Try a Python array.array.
|
|
bitdepth = 8 * testelement.itemsize
|
|
except AttributeError:
|
|
# We can't determine it from the array element's datatype,
|
|
# use a default of 8.
|
|
bitdepth = 8
|
|
else:
|
|
# If we got here without exception,
|
|
# we now assume that the array is a numpy array.
|
|
if dtype.kind == 'b':
|
|
bitdepth = 1
|
|
else:
|
|
bitdepth = 8 * dtype.itemsize
|
|
info['bitdepth'] = bitdepth
|
|
|
|
for thing in ["width", "height", "bitdepth", "greyscale", "alpha"]:
|
|
assert thing in info
|
|
|
|
return Image(a, info)
|
|
|
|
|
|
# So that refugee's from PIL feel more at home. Not documented.
|
|
fromarray = from_array
|
|
|
|
|
|
class Image:
|
|
"""A PNG image. You can create an :class:`Image` object from
|
|
an array of pixels by calling :meth:`png.from_array`. It can be
|
|
saved to disk with the :meth:`save` method.
|
|
"""
|
|
|
|
def __init__(self, rows, info):
|
|
"""
|
|
.. note ::
|
|
|
|
The constructor is not public. Please do not call it.
|
|
"""
|
|
|
|
self.rows = rows
|
|
self.info = info
|
|
|
|
def save(self, file):
|
|
"""Save the image to the named *file*.
|
|
|
|
See `.write()` if you already have an open file object.
|
|
|
|
In general, you can only call this method once;
|
|
after it has been called the first time the PNG image is written,
|
|
the source data will have been streamed, and
|
|
cannot be streamed again.
|
|
"""
|
|
|
|
w = Writer(**self.info)
|
|
|
|
with open(file, 'wb') as fd:
|
|
w.write(fd, self.rows)
|
|
|
|
def write(self, file):
|
|
"""Write the image to the open file object.
|
|
|
|
See `.save()` if you have a filename.
|
|
|
|
In general, you can only call this method once;
|
|
after it has been called the first time the PNG image is written,
|
|
the source data will have been streamed, and
|
|
cannot be streamed again.
|
|
"""
|
|
|
|
w = Writer(**self.info)
|
|
w.write(file, self.rows)
|
|
|
|
|
|
class Reader:
|
|
"""
|
|
Pure Python PNG decoder in pure Python.
|
|
"""
|
|
|
|
def __init__(self, _guess=None, filename=None, file=None, bytes=None):
|
|
"""
|
|
The constructor expects exactly one keyword argument.
|
|
If you supply a positional argument instead,
|
|
it will guess the input type.
|
|
Choose from the following keyword arguments:
|
|
|
|
filename
|
|
Name of input file (a PNG file).
|
|
file
|
|
A file-like object (object with a read() method).
|
|
bytes
|
|
``bytes`` or ``bytearray`` with PNG data.
|
|
|
|
"""
|
|
keywords_supplied = (
|
|
(_guess is not None) +
|
|
(filename is not None) +
|
|
(file is not None) +
|
|
(bytes is not None))
|
|
if keywords_supplied != 1:
|
|
raise TypeError("Reader() takes exactly 1 argument")
|
|
|
|
# Will be the first 8 bytes, later on. See validate_signature.
|
|
self.signature = None
|
|
self.transparent = None
|
|
# A pair of (len,type) if a chunk has been read but its data and
|
|
# checksum have not (in other words the file position is just
|
|
# past the 4 bytes that specify the chunk type).
|
|
# See preamble method for how this is used.
|
|
self.atchunk = None
|
|
|
|
if _guess is not None:
|
|
if isarray(_guess):
|
|
bytes = _guess
|
|
elif isinstance(_guess, str):
|
|
filename = _guess
|
|
elif hasattr(_guess, 'read'):
|
|
file = _guess
|
|
|
|
if bytes is not None:
|
|
self.file = io.BytesIO(bytes)
|
|
elif filename is not None:
|
|
self.file = open(filename, "rb")
|
|
elif file is not None:
|
|
self.file = file
|
|
else:
|
|
raise ProtocolError("expecting filename, file or bytes array")
|
|
|
|
def chunk(self, lenient=False):
|
|
"""
|
|
Read the next PNG chunk from the input file;
|
|
returns a (*type*, *data*) tuple.
|
|
*type* is the chunk's type as a byte string
|
|
(all PNG chunk types are 4 bytes long).
|
|
*data* is the chunk's data content, as a byte string.
|
|
|
|
If the optional `lenient` argument evaluates to `True`,
|
|
checksum failures will raise warnings rather than exceptions.
|
|
"""
|
|
|
|
self.validate_signature()
|
|
|
|
# http://www.w3.org/TR/PNG/#5Chunk-layout
|
|
if not self.atchunk:
|
|
self.atchunk = self._chunk_len_type()
|
|
if not self.atchunk:
|
|
raise ChunkError("No more chunks.")
|
|
length, type = self.atchunk
|
|
self.atchunk = None
|
|
|
|
data = self.file.read(length)
|
|
if len(data) != length:
|
|
raise ChunkError(
|
|
f'Chunk {type} too short for required {length} octets.')
|
|
checksum = self.file.read(4)
|
|
if len(checksum) != 4:
|
|
raise ChunkError(f'Chunk {type} too short for checksum.')
|
|
verify = zlib.crc32(type)
|
|
verify = zlib.crc32(data, verify)
|
|
verify = struct.pack('!I', verify)
|
|
if checksum != verify:
|
|
(a, ) = struct.unpack('!I', checksum)
|
|
(b, ) = struct.unpack('!I', verify)
|
|
message = f"Checksum error in {type.decode('ascii')} chunk: 0x{a:08X} != 0x{b:08X}."
|
|
if lenient:
|
|
warnings.warn(message, RuntimeWarning)
|
|
else:
|
|
raise ChunkError(message)
|
|
return type, data
|
|
|
|
def chunks(self):
|
|
"""Return an iterator that will yield each chunk as a
|
|
(*chunktype*, *content*) pair.
|
|
"""
|
|
|
|
while True:
|
|
t, v = self.chunk()
|
|
yield t, v
|
|
if t == b'IEND':
|
|
break
|
|
|
|
def undo_filter(self, filter_type, scanline, previous):
|
|
"""
|
|
Undo the filter for a scanline.
|
|
`scanline` is a sequence of bytes that
|
|
does not include the initial filter type byte.
|
|
`previous` is decoded previous scanline
|
|
(for straightlaced images this is the previous pixel row,
|
|
but for interlaced images, it is
|
|
the previous scanline in the reduced image,
|
|
which in general is not the previous pixel row in the final image).
|
|
When there is no previous scanline
|
|
(the first row of a straightlaced image,
|
|
or the first row in one of the passes in an interlaced image),
|
|
then this argument should be ``None``.
|
|
|
|
The scanline will have the effects of filtering removed;
|
|
the result will be returned as a fresh sequence of bytes.
|
|
"""
|
|
|
|
# :todo: Would it be better to update scanline in place?
|
|
result = scanline
|
|
|
|
if filter_type == 0:
|
|
return result
|
|
|
|
if filter_type not in (1, 2, 3, 4):
|
|
raise FormatError(
|
|
'Invalid PNG Filter Type. '
|
|
'See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .')
|
|
|
|
# Filter unit. The stride from one pixel to the corresponding
|
|
# byte from the previous pixel. Normally this is the pixel
|
|
# size in bytes, but when this is smaller than 1, the previous
|
|
# byte is used instead.
|
|
fu = max(1, self.psize)
|
|
|
|
# For the first line of a pass, synthesize a dummy previous
|
|
# line. An alternative approach would be to observe that on the
|
|
# first line 'up' is the same as 'null', 'paeth' is the same
|
|
# as 'sub', with only 'average' requiring any special case.
|
|
if not previous:
|
|
previous = bytearray([0] * len(scanline))
|
|
|
|
# Call appropriate filter algorithm. Note that 0 has already
|
|
# been dealt with.
|
|
fn = (None,
|
|
undo_filter_sub,
|
|
undo_filter_up,
|
|
undo_filter_average,
|
|
undo_filter_paeth)[filter_type]
|
|
fn(fu, scanline, previous, result)
|
|
return result
|
|
|
|
def _deinterlace(self, raw):
|
|
"""
|
|
Read raw pixel data, undo filters, deinterlace, and flatten.
|
|
Return a single array of values.
|
|
"""
|
|
|
|
# Values per row (of the target image)
|
|
vpr = self.width * self.planes
|
|
|
|
# Values per image
|
|
vpi = vpr * self.height
|
|
# Interleaving writes to the output array randomly
|
|
# (well, not quite), so the entire output array must be in memory.
|
|
# Make a result array, and make it big enough.
|
|
if self.bitdepth > 8:
|
|
a = array('H', [0] * vpi)
|
|
else:
|
|
a = bytearray([0] * vpi)
|
|
source_offset = 0
|
|
|
|
for lines in adam7_generate(self.width, self.height):
|
|
# The previous (reconstructed) scanline.
|
|
# `None` at the beginning of a pass
|
|
# to indicate that there is no previous line.
|
|
recon = None
|
|
for x, y, xstep in lines:
|
|
# Pixels per row (reduced pass image)
|
|
ppr = int(math.ceil((self.width - x) / float(xstep)))
|
|
# Row size in bytes for this pass.
|
|
row_size = int(math.ceil(self.psize * ppr))
|
|
|
|
filter_type = raw[source_offset]
|
|
source_offset += 1
|
|
scanline = raw[source_offset: source_offset + row_size]
|
|
source_offset += row_size
|
|
recon = self.undo_filter(filter_type, scanline, recon)
|
|
# Convert so that there is one element per pixel value
|
|
flat = self._bytes_to_values(recon, width=ppr)
|
|
if xstep == 1:
|
|
assert x == 0
|
|
offset = y * vpr
|
|
a[offset: offset + vpr] = flat
|
|
else:
|
|
offset = y * vpr + x * self.planes
|
|
end_offset = (y + 1) * vpr
|
|
skip = self.planes * xstep
|
|
for i in range(self.planes):
|
|
a[offset + i: end_offset: skip] = \
|
|
flat[i:: self.planes]
|
|
|
|
return a
|
|
|
|
def _iter_bytes_to_values(self, byte_rows):
|
|
"""
|
|
Iterator that yields each scanline;
|
|
each scanline being a sequence of values.
|
|
`byte_rows` should be an iterator that yields
|
|
the bytes of each row in turn.
|
|
"""
|
|
|
|
for row in byte_rows:
|
|
yield self._bytes_to_values(row)
|
|
|
|
def _bytes_to_values(self, bs, width=None):
|
|
"""Convert a packed row of bytes into a row of values.
|
|
Result will be a freshly allocated object,
|
|
not shared with the argument.
|
|
"""
|
|
|
|
if self.bitdepth == 8:
|
|
return bytearray(bs)
|
|
if self.bitdepth == 16:
|
|
return array('H',
|
|
struct.unpack(f'!{(len(bs) // 2)}H' , bs))
|
|
|
|
assert self.bitdepth < 8
|
|
if width is None:
|
|
width = self.width
|
|
# Samples per byte
|
|
spb = 8 // self.bitdepth
|
|
out = bytearray()
|
|
mask = 2**self.bitdepth - 1
|
|
shifts = [self.bitdepth * i
|
|
for i in reversed(list(range(spb)))]
|
|
for o in bs:
|
|
out.extend([mask & (o >> i) for i in shifts])
|
|
return out[:width]
|
|
|
|
def _iter_straight_packed(self, byte_blocks):
|
|
"""Iterator that undoes the effect of filtering;
|
|
yields each row as a sequence of packed bytes.
|
|
Assumes input is straightlaced.
|
|
`byte_blocks` should be an iterable that yields the raw bytes
|
|
in blocks of arbitrary size.
|
|
"""
|
|
|
|
# length of row, in bytes
|
|
rb = self.row_bytes
|
|
a = bytearray()
|
|
# The previous (reconstructed) scanline.
|
|
# None indicates first line of image.
|
|
recon = None
|
|
for some_bytes in byte_blocks:
|
|
a.extend(some_bytes)
|
|
while len(a) >= rb + 1:
|
|
filter_type = a[0]
|
|
scanline = a[1: rb + 1]
|
|
del a[: rb + 1]
|
|
recon = self.undo_filter(filter_type, scanline, recon)
|
|
yield recon
|
|
if len(a) != 0:
|
|
# :file:format We get here with a file format error:
|
|
# when the available bytes (after decompressing) do not
|
|
# pack into exact rows.
|
|
raise FormatError('Wrong size for decompressed IDAT chunk.')
|
|
assert len(a) == 0
|
|
|
|
def validate_signature(self):
|
|
"""
|
|
If signature (header) has not been read then read and
|
|
validate it; otherwise do nothing.
|
|
"""
|
|
|
|
if self.signature:
|
|
return
|
|
self.signature = self.file.read(8)
|
|
if self.signature != signature:
|
|
raise FormatError("PNG file has invalid signature.")
|
|
|
|
def preamble(self, lenient=False):
|
|
"""
|
|
Extract the image metadata by reading
|
|
the initial part of the PNG file up to
|
|
the start of the ``IDAT`` chunk.
|
|
All the chunks that precede the ``IDAT`` chunk are
|
|
read and either processed for metadata or discarded.
|
|
|
|
If the optional `lenient` argument evaluates to `True`,
|
|
checksum failures will raise warnings rather than exceptions.
|
|
"""
|
|
|
|
self.validate_signature()
|
|
|
|
while True:
|
|
if not self.atchunk:
|
|
self.atchunk = self._chunk_len_type()
|
|
if self.atchunk is None:
|
|
raise FormatError('This PNG file has no IDAT chunks.')
|
|
if self.atchunk[1] == b'IDAT':
|
|
return
|
|
self.process_chunk(lenient=lenient)
|
|
|
|
def _chunk_len_type(self):
|
|
"""
|
|
Reads just enough of the input to
|
|
determine the next chunk's length and type;
|
|
return a (*length*, *type*) pair where *type* is a byte sequence.
|
|
If there are no more chunks, ``None`` is returned.
|
|
"""
|
|
|
|
x = self.file.read(8)
|
|
if not x:
|
|
return None
|
|
if len(x) != 8:
|
|
raise FormatError(
|
|
'End of file whilst reading chunk length and type.')
|
|
length, type = struct.unpack('!I4s', x)
|
|
if length > 2 ** 31 - 1:
|
|
raise FormatError(f'Chunk {type} is too large: {length}.')
|
|
# Check that all bytes are in valid ASCII range.
|
|
# https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout
|
|
type_bytes = set(bytearray(type))
|
|
if not(type_bytes <= set(range(65, 91)) | set(range(97, 123))):
|
|
raise FormatError(
|
|
f'Chunk {list(type)} has invalid Chunk Type.')
|
|
return length, type
|
|
|
|
def process_chunk(self, lenient=False):
|
|
"""
|
|
Process the next chunk and its data.
|
|
This only processes the following chunk types:
|
|
``IHDR``, ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``, ``pHYs``.
|
|
All other chunk types are ignored.
|
|
|
|
If the optional `lenient` argument evaluates to `True`,
|
|
checksum failures will raise warnings rather than exceptions.
|
|
"""
|
|
|
|
type, data = self.chunk(lenient=lenient)
|
|
method = '_process_' + type.decode('ascii')
|
|
m = getattr(self, method, None)
|
|
if m:
|
|
m(data)
|
|
|
|
def _process_IHDR(self, data):
|
|
# http://www.w3.org/TR/PNG/#11IHDR
|
|
if len(data) != 13:
|
|
raise FormatError('IHDR chunk has incorrect length.')
|
|
(self.width, self.height, self.bitdepth, self.color_type,
|
|
self.compression, self.filter,
|
|
self.interlace) = struct.unpack("!2I5B", data)
|
|
|
|
check_bitdepth_colortype(self.bitdepth, self.color_type)
|
|
|
|
if self.compression != 0:
|
|
raise FormatError(
|
|
f"Unknown compression method {self.compression}")
|
|
if self.filter != 0:
|
|
raise FormatError(
|
|
f"Unknown filter method {self.filter},"
|
|
" see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
|
|
)
|
|
if self.interlace not in (0, 1):
|
|
raise FormatError(
|
|
f"Unknown interlace method {self.interlace}, see "
|
|
"http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods"
|
|
" .")
|
|
|
|
# Derived values
|
|
# http://www.w3.org/TR/PNG/#6Colour-values
|
|
colormap = bool(self.color_type & 1)
|
|
greyscale = not(self.color_type & 2)
|
|
alpha = bool(self.color_type & 4)
|
|
color_planes = (3, 1)[greyscale or colormap]
|
|
planes = color_planes + alpha
|
|
|
|
self.colormap = colormap
|
|
self.greyscale = greyscale
|
|
self.alpha = alpha
|
|
self.color_planes = color_planes
|
|
self.planes = planes
|
|
self.psize = float(self.bitdepth) / float(8) * planes
|
|
if int(self.psize) == self.psize:
|
|
self.psize = int(self.psize)
|
|
self.row_bytes = int(math.ceil(self.width * self.psize))
|
|
# Stores PLTE chunk if present, and is used to check
|
|
# chunk ordering constraints.
|
|
self.plte = None
|
|
# Stores tRNS chunk if present, and is used to check chunk
|
|
# ordering constraints.
|
|
self.trns = None
|
|
# Stores sBIT chunk if present.
|
|
self.sbit = None
|
|
|
|
def _process_PLTE(self, data):
|
|
# http://www.w3.org/TR/PNG/#11PLTE
|
|
if self.plte:
|
|
warnings.warn("Multiple PLTE chunks present.")
|
|
self.plte = data
|
|
if len(data) % 3 != 0:
|
|
raise FormatError(
|
|
"PLTE chunk's length should be a multiple of 3.")
|
|
if len(data) > (2 ** self.bitdepth) * 3:
|
|
raise FormatError("PLTE chunk is too long.")
|
|
if len(data) == 0:
|
|
raise FormatError("Empty PLTE is not allowed.")
|
|
|
|
def _process_bKGD(self, data):
|
|
try:
|
|
if self.colormap:
|
|
if not self.plte:
|
|
warnings.warn(
|
|
"PLTE chunk is required before bKGD chunk.")
|
|
self.background = struct.unpack('B', data)
|
|
else:
|
|
self.background = struct.unpack(f"!{self.color_planes}",
|
|
data)
|
|
except struct.error:
|
|
raise FormatError("bKGD chunk has incorrect length.")
|
|
|
|
def _process_tRNS(self, data):
|
|
# http://www.w3.org/TR/PNG/#11tRNS
|
|
self.trns = data
|
|
if self.colormap:
|
|
if not self.plte:
|
|
warnings.warn("PLTE chunk is required before tRNS chunk.")
|
|
else:
|
|
if len(data) > len(self.plte) / 3:
|
|
# Was warning, but promoted to Error as it
|
|
# would otherwise cause pain later on.
|
|
raise FormatError("tRNS chunk is too long.")
|
|
else:
|
|
if self.alpha:
|
|
raise FormatError(
|
|
f"tRNS chunk is not valid with colour type {self.color_type}.")
|
|
try:
|
|
self.transparent = \
|
|
struct.unpack(f"!{self.color_planes}", data)
|
|
except struct.error:
|
|
raise FormatError("tRNS chunk has incorrect length.")
|
|
|
|
def _process_gAMA(self, data):
|
|
try:
|
|
self.gamma = struct.unpack("!L", data)[0] / 100000.0
|
|
except struct.error:
|
|
raise FormatError("gAMA chunk has incorrect length.")
|
|
|
|
def _process_sBIT(self, data):
|
|
self.sbit = data
|
|
if (self.colormap and len(data) != 3 or
|
|
not self.colormap and len(data) != self.planes):
|
|
raise FormatError("sBIT chunk has incorrect length.")
|
|
|
|
def _process_pHYs(self, data):
|
|
# http://www.w3.org/TR/PNG/#11pHYs
|
|
self.phys = data
|
|
fmt = "!LLB"
|
|
if len(data) != struct.calcsize(fmt):
|
|
raise FormatError("pHYs chunk has incorrect length.")
|
|
self.x_pixels_per_unit, self.y_pixels_per_unit, unit = \
|
|
struct.unpack(fmt, data)
|
|
self.unit_is_meter = bool(unit)
|
|
|
|
def read(self, lenient=False):
|
|
"""
|
|
Read the PNG file and decode it.
|
|
Returns (`width`, `height`, `rows`, `info`).
|
|
|
|
May use excessive memory.
|
|
|
|
`rows` is a sequence of rows;
|
|
each row is a sequence of values.
|
|
|
|
If the optional `lenient` argument evaluates to True,
|
|
checksum failures will raise warnings rather than exceptions.
|
|
"""
|
|
|
|
def iteridat():
|
|
"""Iterator that yields all the ``IDAT`` chunks as strings."""
|
|
while True:
|
|
type, data = self.chunk(lenient=lenient)
|
|
if type == b'IEND':
|
|
# http://www.w3.org/TR/PNG/#11IEND
|
|
break
|
|
if type != b'IDAT':
|
|
continue
|
|
# type == b'IDAT'
|
|
# http://www.w3.org/TR/PNG/#11IDAT
|
|
if self.colormap and not self.plte:
|
|
warnings.warn("PLTE chunk is required before IDAT chunk")
|
|
yield data
|
|
|
|
self.preamble(lenient=lenient)
|
|
raw = decompress(iteridat())
|
|
|
|
if self.interlace:
|
|
def rows_from_interlace():
|
|
"""Yield each row from an interlaced PNG."""
|
|
# It's important that this iterator doesn't read
|
|
# IDAT chunks until it yields the first row.
|
|
bs = bytearray(itertools.chain(*raw))
|
|
arraycode = 'BH'[self.bitdepth > 8]
|
|
# Like :meth:`group` but
|
|
# producing an array.array object for each row.
|
|
values = self._deinterlace(bs)
|
|
vpr = self.width * self.planes
|
|
for i in range(0, len(values), vpr):
|
|
row = array(arraycode, values[i:i+vpr])
|
|
yield row
|
|
rows = rows_from_interlace()
|
|
else:
|
|
rows = self._iter_bytes_to_values(self._iter_straight_packed(raw))
|
|
info = dict()
|
|
for attr in 'greyscale alpha planes bitdepth interlace'.split():
|
|
info[attr] = getattr(self, attr)
|
|
info['size'] = (self.width, self.height)
|
|
for attr in 'gamma transparent background'.split():
|
|
a = getattr(self, attr, None)
|
|
if a is not None:
|
|
info[attr] = a
|
|
if getattr(self, 'x_pixels_per_unit', None):
|
|
info['physical'] = Resolution(self.x_pixels_per_unit,
|
|
self.y_pixels_per_unit,
|
|
self.unit_is_meter)
|
|
if self.plte:
|
|
info['palette'] = self.palette()
|
|
return self.width, self.height, rows, info
|
|
|
|
def read_flat(self):
|
|
"""
|
|
Read a PNG file and decode it into a single array of values.
|
|
Returns (*width*, *height*, *values*, *info*).
|
|
|
|
May use excessive memory.
|
|
|
|
`values` is a single array.
|
|
|
|
The :meth:`read` method is more stream-friendly than this,
|
|
because it returns a sequence of rows.
|
|
"""
|
|
|
|
x, y, pixel, info = self.read()
|
|
arraycode = 'BH'[info['bitdepth'] > 8]
|
|
pixel = array(arraycode, itertools.chain(*pixel))
|
|
return x, y, pixel, info
|
|
|
|
def palette(self, alpha='natural'):
|
|
"""
|
|
Returns a palette that is a sequence of 3-tuples or 4-tuples,
|
|
synthesizing it from the ``PLTE`` and ``tRNS`` chunks.
|
|
These chunks should have already been processed (for example,
|
|
by calling the :meth:`preamble` method).
|
|
All the tuples are the same size:
|
|
3-tuples if there is no ``tRNS`` chunk,
|
|
4-tuples when there is a ``tRNS`` chunk.
|
|
|
|
Assumes that the image is colour type
|
|
3 and therefore a ``PLTE`` chunk is required.
|
|
|
|
If the `alpha` argument is ``'force'`` then an alpha channel is
|
|
always added, forcing the result to be a sequence of 4-tuples.
|
|
"""
|
|
|
|
if not self.plte:
|
|
raise FormatError(
|
|
"Required PLTE chunk is missing in colour type 3 image.")
|
|
plte = group(array('B', self.plte), 3)
|
|
if self.trns or alpha == 'force':
|
|
trns = array('B', self.trns or [])
|
|
trns.extend([255] * (len(plte) - len(trns)))
|
|
plte = list(map(operator.add, plte, group(trns, 1)))
|
|
return plte
|
|
|
|
def asDirect(self):
|
|
"""
|
|
Returns the image data as a direct representation of
|
|
an ``x * y * planes`` array.
|
|
This removes the need for callers to deal with
|
|
palettes and transparency themselves.
|
|
Images with a palette (colour type 3) are converted to RGB or RGBA;
|
|
images with transparency (a ``tRNS`` chunk) are converted to
|
|
LA or RGBA as appropriate.
|
|
When returned in this format the pixel values represent
|
|
the colour value directly without needing to refer
|
|
to palettes or transparency information.
|
|
|
|
Like the :meth:`read` method this method returns a 4-tuple:
|
|
|
|
(*width*, *height*, *rows*, *info*)
|
|
|
|
This method normally returns pixel values with
|
|
the bit depth they have in the source image, but
|
|
when the source PNG has an ``sBIT`` chunk it is inspected and
|
|
can reduce the bit depth of the result pixels;
|
|
pixel values will be reduced according to the bit depth
|
|
specified in the ``sBIT`` chunk.
|
|
PNG nerds should note a single result bit depth is
|
|
used for all channels:
|
|
the maximum of the ones specified in the ``sBIT`` chunk.
|
|
An RGB565 image will be rescaled to 6-bit RGB666.
|
|
|
|
The *info* dictionary that is returned reflects
|
|
the `direct` format and not the original source image.
|
|
For example, an RGB source image with a ``tRNS`` chunk
|
|
to represent a transparent colour,
|
|
will start with ``planes=3`` and ``alpha=False`` for the
|
|
source image,
|
|
but the *info* dictionary returned by this method
|
|
will have ``planes=4`` and ``alpha=True`` because
|
|
an alpha channel is synthesized and added.
|
|
|
|
*rows* is a sequence of rows;
|
|
each row being a sequence of values
|
|
(like the :meth:`read` method).
|
|
|
|
All the other aspects of the image data are not changed.
|
|
"""
|
|
|
|
self.preamble()
|
|
|
|
# Simple case, no conversion necessary.
|
|
if not self.colormap and not self.trns and not self.sbit:
|
|
return self.read()
|
|
|
|
x, y, pixels, info = self.read()
|
|
|
|
if self.colormap:
|
|
info['colormap'] = False
|
|
info['alpha'] = bool(self.trns)
|
|
info['bitdepth'] = 8
|
|
info['planes'] = 3 + bool(self.trns)
|
|
plte = self.palette()
|
|
|
|
def iterpal(pixels):
|
|
for row in pixels:
|
|
row = [plte[x] for x in row]
|
|
yield array('B', itertools.chain(*row))
|
|
pixels = iterpal(pixels)
|
|
elif self.trns:
|
|
# It would be nice if there was some reasonable way
|
|
# of doing this without generating a whole load of
|
|
# intermediate tuples. But tuples does seem like the
|
|
# easiest way, with no other way clearly much simpler or
|
|
# much faster. (Actually, the L to LA conversion could
|
|
# perhaps go faster (all those 1-tuples!), but I still
|
|
# wonder whether the code proliferation is worth it)
|
|
it = self.transparent
|
|
maxval = 2 ** info['bitdepth'] - 1
|
|
planes = info['planes']
|
|
info['alpha'] = True
|
|
info['planes'] += 1
|
|
typecode = 'BH'[info['bitdepth'] > 8]
|
|
|
|
def itertrns(pixels):
|
|
for row in pixels:
|
|
# For each row we group it into pixels, then form a
|
|
# characterisation vector that says whether each
|
|
# pixel is opaque or not. Then we convert
|
|
# True/False to 0/maxval (by multiplication),
|
|
# and add it as the extra channel.
|
|
row = group(row, planes)
|
|
opa = map(it.__ne__, row)
|
|
opa = map(maxval.__mul__, opa)
|
|
opa = list(zip(opa)) # convert to 1-tuples
|
|
yield array(
|
|
typecode,
|
|
itertools.chain(*map(operator.add, row, opa)))
|
|
pixels = itertrns(pixels)
|
|
targetbitdepth = None
|
|
if self.sbit:
|
|
sbit = struct.unpack(f'{len(self.sbit)}', self.sbit)
|
|
targetbitdepth = max(sbit)
|
|
if targetbitdepth > info['bitdepth']:
|
|
raise Error(f'sBIT chunk {sbit!r} exceeds bitdepth {self.bitdepth}')
|
|
if min(sbit) <= 0:
|
|
raise Error(f'sBIT chunk {sbit} has a 0-entry')
|
|
if targetbitdepth:
|
|
shift = info['bitdepth'] - targetbitdepth
|
|
info['bitdepth'] = targetbitdepth
|
|
|
|
def itershift(pixels):
|
|
for row in pixels:
|
|
yield [p >> shift for p in row]
|
|
pixels = itershift(pixels)
|
|
return x, y, pixels, info
|
|
|
|
def _as_rescale(self, get, targetbitdepth):
|
|
"""Helper used by :meth:`asRGB8` and :meth:`asRGBA8`."""
|
|
|
|
width, height, pixels, info = get()
|
|
maxval = 2**info['bitdepth'] - 1
|
|
targetmaxval = 2**targetbitdepth - 1
|
|
factor = float(targetmaxval) / float(maxval)
|
|
info['bitdepth'] = targetbitdepth
|
|
|
|
def iterscale():
|
|
for row in pixels:
|
|
yield [int(round(x * factor)) for x in row]
|
|
if maxval == targetmaxval:
|
|
return width, height, pixels, info
|
|
else:
|
|
return width, height, iterscale(), info
|
|
|
|
def asRGB8(self):
|
|
"""
|
|
Return the image data as an RGB pixels with 8-bits per sample.
|
|
This is like the :meth:`asRGB` method except that
|
|
this method additionally rescales the values so that
|
|
they are all between 0 and 255 (8-bit).
|
|
In the case where the source image has a bit depth < 8
|
|
the transformation preserves all the information;
|
|
where the source image has bit depth > 8, then
|
|
rescaling to 8-bit values loses precision.
|
|
No dithering is performed.
|
|
Like :meth:`asRGB`,
|
|
an alpha channel in the source image will raise an exception.
|
|
|
|
This function returns a 4-tuple:
|
|
(*width*, *height*, *rows*, *info*).
|
|
*width*, *height*, *info* are as per the :meth:`read` method.
|
|
|
|
*rows* is the pixel data as a sequence of rows.
|
|
"""
|
|
|
|
return self._as_rescale(self.asRGB, 8)
|
|
|
|
def asRGBA8(self):
|
|
"""
|
|
Return the image data as RGBA pixels with 8-bits per sample.
|
|
This method is similar to :meth:`asRGB8` and :meth:`asRGBA`:
|
|
The result pixels have an alpha channel, *and*
|
|
values are rescaled to the range 0 to 255.
|
|
The alpha channel is synthesized if necessary
|
|
(with a small speed penalty).
|
|
"""
|
|
|
|
return self._as_rescale(self.asRGBA, 8)
|
|
|
|
def asRGB(self):
|
|
"""
|
|
Return image as RGB pixels.
|
|
RGB colour images are passed through unchanged;
|
|
greyscales are expanded into RGB triplets
|
|
(there is a small speed overhead for doing this).
|
|
|
|
An alpha channel in the source image will raise an exception.
|
|
|
|
The return values are as for the :meth:`read` method except that
|
|
the *info* reflect the returned pixels, not the source image.
|
|
In particular,
|
|
for this method ``info['greyscale']`` will be ``False``.
|
|
"""
|
|
|
|
width, height, pixels, info = self.asDirect()
|
|
if info['alpha']:
|
|
raise Error("will not convert image with alpha channel to RGB")
|
|
if not info['greyscale']:
|
|
return width, height, pixels, info
|
|
info['greyscale'] = False
|
|
info['planes'] = 3
|
|
|
|
if info['bitdepth'] > 8:
|
|
def newarray():
|
|
return array('H', [0])
|
|
else:
|
|
def newarray():
|
|
return bytearray([0])
|
|
|
|
def iterrgb():
|
|
for row in pixels:
|
|
a = newarray() * 3 * width
|
|
for i in range(3):
|
|
a[i::3] = row
|
|
yield a
|
|
return width, height, iterrgb(), info
|
|
|
|
def asRGBA(self):
|
|
"""
|
|
Return image as RGBA pixels.
|
|
Greyscales are expanded into RGB triplets;
|
|
an alpha channel is synthesized if necessary.
|
|
The return values are as for the :meth:`read` method except that
|
|
the *info* reflect the returned pixels, not the source image.
|
|
In particular, for this method
|
|
``info['greyscale']`` will be ``False``, and
|
|
``info['alpha']`` will be ``True``.
|
|
"""
|
|
|
|
width, height, pixels, info = self.asDirect()
|
|
if info['alpha'] and not info['greyscale']:
|
|
return width, height, pixels, info
|
|
typecode = 'BH'[info['bitdepth'] > 8]
|
|
maxval = 2**info['bitdepth'] - 1
|
|
maxbuffer = struct.pack('=' + typecode, maxval) * 4 * width
|
|
|
|
if info['bitdepth'] > 8:
|
|
def newarray():
|
|
return array('H', maxbuffer)
|
|
else:
|
|
def newarray():
|
|
return bytearray(maxbuffer)
|
|
|
|
if info['alpha'] and info['greyscale']:
|
|
# LA to RGBA
|
|
def convert():
|
|
for row in pixels:
|
|
# Create a fresh target row, then copy L channel
|
|
# into first three target channels, and A channel
|
|
# into fourth channel.
|
|
a = newarray()
|
|
convert_la_to_rgba(row, a)
|
|
yield a
|
|
elif info['greyscale']:
|
|
# L to RGBA
|
|
def convert():
|
|
for row in pixels:
|
|
a = newarray()
|
|
convert_l_to_rgba(row, a)
|
|
yield a
|
|
else:
|
|
assert not info['alpha'] and not info['greyscale']
|
|
# RGB to RGBA
|
|
|
|
def convert():
|
|
for row in pixels:
|
|
a = newarray()
|
|
convert_rgb_to_rgba(row, a)
|
|
yield a
|
|
info['alpha'] = True
|
|
info['greyscale'] = False
|
|
info['planes'] = 4
|
|
return width, height, convert(), info
|
|
|
|
|
|
def decompress(data_blocks):
|
|
"""
|
|
`data_blocks` should be an iterable that
|
|
yields the compressed data (from the ``IDAT`` chunks).
|
|
This yields decompressed byte strings.
|
|
"""
|
|
|
|
# Currently, with no max_length parameter to decompress,
|
|
# this routine will do one yield per IDAT chunk: Not very
|
|
# incremental.
|
|
d = zlib.decompressobj()
|
|
# Each IDAT chunk is passed to the decompressor, then any
|
|
# remaining state is decompressed out.
|
|
for data in data_blocks:
|
|
# :todo: add a max_length argument here to limit output size.
|
|
yield bytearray(d.decompress(data))
|
|
yield bytearray(d.flush())
|
|
|
|
|
|
def check_bitdepth_colortype(bitdepth, colortype):
|
|
"""
|
|
Check that `bitdepth` and `colortype` are both valid,
|
|
and specified in a valid combination.
|
|
Returns (None) if valid, raise an Exception if not valid.
|
|
"""
|
|
|
|
if bitdepth not in (1, 2, 4, 8, 16):
|
|
raise FormatError(f"invalid bit depth {bitdepth}")
|
|
if colortype not in (0, 2, 3, 4, 6):
|
|
raise FormatError(f"invalid colour type {colortype}")
|
|
# Check indexed (palettized) images have 8 or fewer bits
|
|
# per pixel; check only indexed or greyscale images have
|
|
# fewer than 8 bits per pixel.
|
|
if colortype & 1 and bitdepth > 8:
|
|
raise FormatError(
|
|
f"Indexed images (colour type {bitdepth}) cannot"
|
|
f" have bitdepth > 8 (bit depth {colortype})."
|
|
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
|
|
)
|
|
if bitdepth < 8 and colortype not in (0, 3):
|
|
raise FormatError(
|
|
f"Illegal combination of bit depth ({bitdepth})"
|
|
f" and colour type ({colortype})."
|
|
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 .")
|
|
|
|
|
|
def is_natural(x):
|
|
"""A non-negative integer."""
|
|
try:
|
|
is_integer = int(x) == x
|
|
except (TypeError, ValueError):
|
|
return False
|
|
return is_integer and x >= 0
|
|
|
|
|
|
def undo_filter_sub(filter_unit, scanline, previous, result):
|
|
"""Undo sub filter."""
|
|
|
|
ai = 0
|
|
# Loops starts at index fu. Observe that the initial part
|
|
# of the result is already filled in correctly with
|
|
# scanline.
|
|
for i in range(filter_unit, len(result)):
|
|
x = scanline[i]
|
|
a = result[ai]
|
|
result[i] = (x + a) & 0xff
|
|
ai += 1
|
|
|
|
|
|
def undo_filter_up(filter_unit, scanline, previous, result):
|
|
"""Undo up filter."""
|
|
|
|
for i in range(len(result)):
|
|
x = scanline[i]
|
|
b = previous[i]
|
|
result[i] = (x + b) & 0xff
|
|
|
|
|
|
def undo_filter_average(filter_unit, scanline, previous, result):
|
|
"""Undo up filter."""
|
|
|
|
ai = -filter_unit
|
|
for i in range(len(result)):
|
|
x = scanline[i]
|
|
if ai < 0:
|
|
a = 0
|
|
else:
|
|
a = result[ai]
|
|
b = previous[i]
|
|
result[i] = (x + ((a + b) >> 1)) & 0xff
|
|
ai += 1
|
|
|
|
|
|
def undo_filter_paeth(filter_unit, scanline, previous, result):
|
|
"""Undo Paeth filter."""
|
|
|
|
# Also used for ci.
|
|
ai = -filter_unit
|
|
for i in range(len(result)):
|
|
x = scanline[i]
|
|
if ai < 0:
|
|
a = c = 0
|
|
else:
|
|
a = result[ai]
|
|
c = previous[ai]
|
|
b = previous[i]
|
|
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
|
|
result[i] = (x + pr) & 0xff
|
|
ai += 1
|
|
|
|
|
|
def convert_la_to_rgba(row, result):
|
|
for i in range(3):
|
|
result[i::4] = row[0::2]
|
|
result[3::4] = row[1::2]
|
|
|
|
|
|
def convert_l_to_rgba(row, result):
|
|
"""
|
|
Convert a grayscale image to RGBA.
|
|
This method assumes the alpha channel in result is
|
|
already correctly initialized.
|
|
"""
|
|
for i in range(3):
|
|
result[i::4] = row
|
|
|
|
|
|
def convert_rgb_to_rgba(row, result):
|
|
"""
|
|
Convert an RGB image to RGBA.
|
|
This method assumes the alpha channel in result is
|
|
already correctly initialized.
|
|
"""
|
|
for i in range(3):
|
|
result[i::4] = row[i::3]
|
|
|
|
|
|
# Only reason to include this in this module is that
|
|
# several utilities need it, and it is small.
|
|
def binary_stdin():
|
|
"""
|
|
A sys.stdin that returns bytes.
|
|
"""
|
|
|
|
return sys.stdin.buffer
|
|
|
|
|
|
def binary_stdout():
|
|
"""
|
|
A sys.stdout that accepts bytes.
|
|
"""
|
|
|
|
stdout = sys.stdout.buffer
|
|
|
|
# On Windows the C runtime file orientation needs changing.
|
|
if sys.platform == "win32":
|
|
import msvcrt
|
|
import os
|
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
|
|
return stdout
|
|
|
|
|
|
def cli_open(path):
|
|
if path == "-":
|
|
return binary_stdin()
|
|
return open(path, "rb")
|
|
|
|
|
|
def main(argv):
|
|
"""
|
|
Run command line PNG.
|
|
"""
|
|
print("What should the command line tool do?", file=sys.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main(sys.argv)
|
|
except Error as e:
|
|
print(e, file=sys.stderr)
|