Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- # -*- coding: utf-8 -*-
- from __future__ import print_function
- '''
- STANDALONE SPRITE CONVERTER
- HOW TO RUN:
- python makeimg_standalone.py <input_sprite> -> DAT/CBB/2bpp > PNG
- python makeimg_standalone.py -H <input_sprite> -> Text sprite > PNG
- python makeimg_standalone.py -C <input_sprite> -> Compressed binary sprite > PNG
- python makeimg_standalone.py -HC <input_sprite> -> Compressed text sprite > PNG
- python makeimg_standalone.py -HC -d 2 -l grayhir -q <input_sprite> <output_png>
- '''
- import os, sys, argparse, textwrap, re, math, subprocess, itertools, struct, warnings, zlib, collections
- sys.dont_write_bytecode = True
- from array import array
- from functools import reduce
- sqrt = math.sqrt
- if sys.version_info > (3,0):
- global xrange
- xrange = range
- # hex_char_encoding = 'utf-8'
- hex_char_encoding = 'shift_jis'
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # 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.4 or higher (or Python 2.7).
- 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"
- __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(
- "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 ProtocolError(
- "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 ProtocolError(
- "palette entry %d: "
- "values must be integer: 0 <= x <= 255" % i)
- 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(
- "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 ProtocolError(
- "size[1] (%r) and height (%r) should match when both are used."
- % (size[1], height))
- 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("%s for greyscale must be 1-tuple" % which)
- if not is_natural(c[0]):
- raise ProtocolError(
- "%s colour for greyscale must be integer" % which)
- else:
- if not (len(c) == 3 and
- is_natural(c[0]) and
- is_natural(c[1]) and
- is_natural(c[2])):
- raise ProtocolError(
- "%s colour must be a triple of integers" % which)
- 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(
- "each bitdepth %r must be a positive integer <= 16" %
- (bitdepth,))
- # 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(
- "Expected %d values but got %d value, in row %d" %
- (vpr, len(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(
- "rows supplied (%d) does not match height (%d)" %
- (nrows, self.height))
- 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()
- 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:
- # :todo: bytes() only necessary in Python 2
- compressed = compressor.compress(bytes(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('%dB' % self.planes,
- * [s[0] for s in self.rescale]))
- # :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)
- self.write_passes(outfile, self.array_scanlines_interlace(pixels))
- else:
- 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 = '!%dH' % 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(
- "bitdepth (%d) should match bitdepth of info (%d)." %
- (bitdepth, 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(
- 'Chunk %s too short for required %i octets.'
- % (type, length))
- checksum = self.file.read(4)
- if len(checksum) != 4:
- raise ChunkError('Chunk %s too short for checksum.' % type)
- verify = zlib.crc32(type)
- verify = zlib.crc32(data, verify)
- # Whether the output from zlib.crc32 is signed or not varies
- # according to hideous implementation details, see
- # http://bugs.python.org/issue1202 .
- # We coerce it to be positive here (in a way which works on
- # Python 2.3 and older).
- verify &= 2**32 - 1
- verify = struct.pack('!I', verify)
- if checksum != verify:
- (a, ) = struct.unpack('!I', checksum)
- (b, ) = struct.unpack('!I', verify)
- message = ("Checksum error in %s chunk: 0x%08X != 0x%08X."
- % (type.decode('ascii'), a, b))
- 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('!%dH' % (len(bs) // 2), 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('Chunk %s is too large: %d.' % (type, 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(
- 'Chunk %r has invalid Chunk Type.'
- % list(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(
- "Unknown compression method %d" % self.compression)
- if self.filter != 0:
- raise FormatError(
- "Unknown filter method %d,"
- " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ."
- % self.filter)
- if self.interlace not in (0, 1):
- raise FormatError(
- "Unknown interlace method %d, see "
- "http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods"
- " ."
- % self.interlace)
- # 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("!%dH" % 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(
- "tRNS chunk is not valid with colour type %d." %
- self.color_type)
- try:
- self.transparent = \
- struct.unpack("!%dH" % 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('%dB' % len(self.sbit), self.sbit)
- targetbitdepth = max(sbit)
- if targetbitdepth > info['bitdepth']:
- raise Error('sBIT chunk %r exceeds bitdepth %d' %
- (sbit, self.bitdepth))
- if min(sbit) <= 0:
- raise Error('sBIT chunk %r has a 0-entry' % sbit)
- 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("invalid bit depth %d" % bitdepth)
- if colortype not in (0, 2, 3, 4, 6):
- raise FormatError("invalid colour type %d" % 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(
- "Indexed images (colour type %d) cannot"
- " have bitdepth > 8 (bit depth %d)."
- " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
- % (bitdepth, colortype))
- if bitdepth < 8 and colortype not in (0, 3):
- raise FormatError(
- "Illegal combination of bit depth (%d)"
- " and colour type (%d)."
- " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ."
- % (bitdepth, colortype))
- 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]
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- # -------------------------------------------------------------------------------------------
- """
- Pokemon Crystal data de/compression.
- """
- """
- A rundown of Pokemon Crystal's compression scheme:
- Control commands occupy bits 5-7.
- Bits 0-4 serve as the first parameter <n> for each command.
- """
- lz_commands = {
- 'literal': 0, # n values for n bytes
- 'iterate': 1, # one value for n bytes
- 'alternate': 2, # alternate two values for n bytes
- 'blank': 3, # zero for n bytes
- }
- """
- Repeater commands repeat any data that was just decompressed.
- They take an additional signed parameter <s> to mark a relative starting point.
- These wrap around (positive from the start, negative from the current position).
- """
- lz_commands.update({
- 'repeat': 4, # n bytes starting from s
- 'flip': 5, # n bytes in reverse bit order starting from s
- 'reverse': 6, # n bytes backwards starting from s
- })
- """
- The long command is used when 5 bits aren't enough. Bits 2-4 contain a new control code.
- Bits 0-1 are appended to a new byte as 8-9, allowing a 10-bit parameter.
- """
- lz_commands.update({
- 'long': 7, # n is now 10 bits for a new control code
- })
- max_length = 1 << 10 # can't go higher than 10 bits
- lowmax = 1 << 5 # standard 5-bit param
- """
- If 0xff is encountered instead of a command, decompression ends.
- """
- lz_end = 0xff
- bit_flipped = [
- sum(((byte >> i) & 1) << (7 - i) for i in xrange(8))
- for byte in xrange(0x100)
- ]
- def fbitstream(f):
- while 1:
- char = f.read(1)
- if not char:
- break
- byte = ord(char)
- for i in range(7, -1, -1):
- yield (byte >> i) & 1
- def bitstream(b):
- for byte in b:
- for i in range(7, -1, -1):
- yield (byte >> i) & 1
- def readint(bs, count):
- n = 0
- while count:
- n <<= 1
- n |= next(bs)
- count -= 1
- return n
- def bitgroups_to_bytes(bits):
- l = []
- for i in range(0, len(bits) - 3, 4):
- n = ((bits[i + 0] << 6)
- | (bits[i + 1] << 4)
- | (bits[i + 2] << 2)
- | (bits[i + 3] << 0))
- l.append(n)
- return bytearray(l)
- def bytes_to_bits(bytelist):
- return list(bitstream(bytelist))
- class Compressed:
- """
- Usage:
- lz = Compressed(data).output
- or
- lz = Compressed().compress(data)
- or
- c = Compressed()
- c.data = data
- lz = c.compress()
- There are some issues with reproducing the target compressor.
- Some notes are listed here:
- - the criteria for detecting a lookback is inconsistent
- - sometimes lookbacks that are mostly 0s are pruned, sometimes not
- - target appears to skip ahead if it can use a lookback soon, stopping the current command short or in some cases truncating it with literals.
- - this has been implemented, but the specifics are unknown
- - self.min_scores: It's unknown if blank's minimum score should be 1 or 2. Most likely it's 1, with some other hack to account for edge cases.
- - may be related to the above
- - target does not appear to compress backwards
- """
- def __init__(self, *args, **kwargs):
- self.min_scores = {
- 'blank': 1,
- 'iterate': 2,
- 'alternate': 3,
- 'repeat': 3,
- 'reverse': 3,
- 'flip': 3,
- }
- self.preference = [
- 'repeat',
- 'blank',
- 'flip',
- 'reverse',
- 'iterate',
- 'alternate',
- #'literal',
- ]
- self.lookback_methods = 'repeat', 'reverse', 'flip'
- self.__dict__.update({
- 'data': None,
- 'commands': lz_commands,
- 'debug': False,
- 'literal_only': False,
- })
- self.arg_names = 'data', 'commands', 'debug', 'literal_only'
- self.__dict__.update(kwargs)
- self.__dict__.update(dict(zip(self.arg_names, args)))
- if self.data is not None:
- self.compress()
- def compress(self, data=None):
- if data is not None:
- self.data = data
- self.data = list(bytearray(self.data))
- self.indexes = {}
- self.lookbacks = {}
- for method in self.lookback_methods:
- self.lookbacks[method] = {}
- self.address = 0
- self.end = len(self.data)
- self.output = []
- self.literal = None
- while self.address < self.end:
- if self.score():
- self.do_literal()
- self.do_winner()
- else:
- if self.literal == None:
- self.literal = self.address
- self.address += 1
- self.do_literal()
- self.output += [lz_end]
- return self.output
- def reset_scores(self):
- self.scores = {}
- self.offsets = {}
- self.helpers = {}
- for method in self.min_scores.iterkeys():
- self.scores[method] = 0
- def bit_flip(self, byte):
- return bit_flipped[byte]
- def do_literal(self):
- if self.literal != None:
- length = abs(self.address - self.literal)
- start = min(self.literal, self.address + 1)
- self.helpers['literal'] = self.data[start:start+length]
- self.do_cmd('literal', length)
- self.literal = None
- def score(self):
- self.reset_scores()
- map(self.score_literal, ['iterate', 'alternate', 'blank'])
- for method in self.lookback_methods:
- self.scores[method], self.offsets[method] = self.find_lookback(method, self.address)
- self.stop_short()
- return any(
- score
- > self.min_scores[method] + int(score > lowmax)
- for method, score in self.scores.iteritems()
- )
- def stop_short(self):
- """
- If a lookback is close, reduce the scores of other commands.
- """
- best_method, best_score = max(
- self.scores.items(),
- key = lambda x: (
- x[1],
- -self.preference.index(x[0])
- )
- )
- for method in self.lookback_methods:
- min_score = self.min_scores[method]
- for address in xrange(self.address+1, self.address+best_score):
- length, index = self.find_lookback(method, address)
- if length > max(min_score, best_score):
- # BUG: lookbacks can reduce themselves. This appears to be a bug in the target also.
- for m, score in self.scores.items():
- self.scores[m] = min(score, address - self.address)
- def read(self, address=None):
- if address is None:
- address = self.address
- if 0 <= address < len(self.data):
- return self.data[address]
- return None
- def find_all_lookbacks(self):
- for method in self.lookback_methods:
- for address, byte in enumerate(self.data):
- self.find_lookback(method, address)
- def find_lookback(self, method, address=None):
- #Temporarily stubbed, because the real function doesn't run in polynomial time.
- return (0, None)
- def broken_find_lookback(self, method, address=None):
- if address is None:
- address = self.address
- existing = self.lookbacks.get(method, {}).get(address)
- if existing != None:
- return existing
- lookback = 0, None
- # Better to not carelessly optimize at the moment.
- """
- if address < 2:
- return lookback
- """
- byte = self.read(address)
- if byte is None:
- return lookback
- direction, mutate = {
- 'repeat': ( 1, int),
- 'reverse': (-1, int),
- 'flip': ( 1, self.bit_flip),
- }[method]
- # Doesn't seem to help
- """
- if mutate == self.bit_flip:
- if byte == 0:
- self.lookbacks[method][address] = lookback
- return lookback
- """
- data_len = len(self.data)
- is_two_byte_index = lambda index: int(index < address - 0x7f)
- for index in self.get_indexes(mutate(byte)):
- if index >= address:
- break
- old_length, old_index = lookback
- if direction == 1:
- if old_length > data_len - index: break
- else:
- if old_length > index: continue
- if self.read(index) in [None]: continue
- length = 1 # we know there's at least one match, or we wouldn't be checking this index
- while 1:
- this_byte = self.read(address + length)
- that_byte = self.read(index + length * direction)
- if that_byte == None or this_byte != mutate(that_byte):
- break
- length += 1
- score = length - is_two_byte_index(index)
- old_score = old_length - is_two_byte_index(old_index)
- if score >= old_score or (score == old_score and length > old_length):
- # XXX maybe avoid two-byte indexes when possible
- if score >= lookback[0] - is_two_byte_index(lookback[1]):
- lookback = length, index
- self.lookbacks[method][address] = lookback
- return lookback
- def get_indexes(self, byte):
- if not self.indexes.has_key(byte):
- self.indexes[byte] = []
- index = -1
- while 1:
- try:
- index = self.data.index(byte, index + 1)
- except ValueError:
- break
- self.indexes[byte].append(index)
- return self.indexes[byte]
- def score_literal(self, method):
- address = self.address
- compare = {
- 'blank': [0],
- 'iterate': [self.read(address)],
- 'alternate': [self.read(address), self.read(address + 1)],
- }[method]
- # XXX may or may not be correct
- if method == 'alternate' and compare[0] == 0:
- return
- length = 0
- while self.read(address + length) == compare[length % len(compare)]:
- length += 1
- self.scores[method] = length
- self.helpers[method] = compare
- def do_winner(self):
- winners = filter(
- lambda method, score:
- score
- > self.min_scores[method] + int(score > lowmax),
- self.scores.iteritems()
- )
- winners.sort(
- key = lambda method, score: (
- -(score - self.min_scores[method] - int(score > lowmax)),
- self.preference.index(method)
- )
- )
- winner, score = winners[0]
- length = min(score, max_length)
- self.do_cmd(winner, length)
- self.address += length
- def do_cmd(self, cmd, length):
- start_address = self.address
- cmd_length = length - 1
- output = []
- if length > lowmax:
- output.append(
- (self.commands['long'] << 5)
- + (self.commands[cmd] << 2)
- + (cmd_length >> 8)
- )
- output.append(
- cmd_length & 0xff
- )
- else:
- output.append(
- (self.commands[cmd] << 5)
- + cmd_length
- )
- self.helpers['blank'] = [] # quick hack
- output += self.helpers.get(cmd, [])
- if cmd in self.lookback_methods:
- offset = self.offsets[cmd]
- # Negative offsets are one byte.
- # Positive offsets are two.
- if 0 < start_address - offset - 1 <= 0x7f:
- offset = (start_address - offset - 1) | 0x80
- output += [offset]
- else:
- output += [offset / 0x100, offset % 0x100] # big endian
- if self.debug:
- print(' '.join(map(str, [
- cmd, length, '\t',
- ' '.join(map('{:02x}'.format, output)),
- self.data[start_address:start_address+length] if cmd in self.lookback_methods else '',
- ])))
- self.output += output
- class Decompressed:
- """
- Interpret and decompress lz-compressed data, usually 2bpp.
- """
- """
- Usage:
- data = Decompressed(lz).output
- or
- data = Decompressed().decompress(lz)
- or
- d = Decompressed()
- d.lz = lz
- data = d.decompress()
- To decompress from offset 0x80000 in a rom:
- data = Decompressed(rom, start=0x80000).output
- """
- lz = None
- start = 0
- commands = lz_commands
- debug = False
- arg_names = 'lz', 'start', 'commands', 'debug'
- def __init__(self, *args, **kwargs):
- self.__dict__.update(dict(zip(self.arg_names, args)))
- self.__dict__.update(kwargs)
- self.command_names = dict(map(reversed, self.commands.items()))
- self.address = self.start
- if self.lz is not None:
- self.decompress()
- if self.debug: print( self.command_list() )
- def command_list(self):
- """
- Print a list of commands that were used. Useful for debugging.
- """
- text = ''
- output_address = 0
- for name, attrs in self.used_commands:
- length = attrs['length']
- address = attrs['address']
- offset = attrs['offset']
- direction = attrs['direction']
- text += '{2:03x} {0}: {1}'.format(name, length, output_address)
- text += '\t' + ' '.join(
- '{:02x}'.format(int(byte))
- for byte in self.lz[ address : address + attrs['cmd_length'] ]
- )
- if offset is not None:
- repeated_data = self.output[ offset : offset + length * direction : direction ]
- if name == 'flip':
- repeated_data = map(bit_flipped.__getitem__, repeated_data)
- text += ' [' + ' '.join(map('{:02x}'.format, repeated_data)) + ']'
- text += '\n'
- output_address += length
- return text
- def decompress(self, lz=None):
- if lz is not None:
- self.lz = lz
- self.lz = bytearray(self.lz)
- self.used_commands = []
- self.output = []
- while 1:
- cmd_address = self.address
- self.offset = None
- self.direction = None
- if (self.byte == lz_end):
- self.next()
- break
- self.cmd = (self.byte & 0b11100000) >> 5
- if self.cmd_name == 'long':
- # 10-bit length
- self.cmd = (self.byte & 0b00011100) >> 2
- self.length = (self.next() & 0b00000011) * 0x100
- self.length += self.next() + 1
- else:
- # 5-bit length
- self.length = (self.next() & 0b00011111) + 1
- self.__class__.__dict__[self.cmd_name](self)
- self.used_commands += [(
- self.cmd_name,
- {
- 'length': self.length,
- 'address': cmd_address,
- 'offset': self.offset,
- 'cmd_length': self.address - cmd_address,
- 'direction': self.direction,
- }
- )]
- # Keep track of the data we just decompressed.
- self.compressed_data = self.lz[self.start : self.address]
- @property
- def byte(self):
- return self.lz[ self.address ]
- def next(self):
- byte = self.byte
- self.address += 1
- return byte
- @property
- def cmd_name(self):
- return self.command_names.get(self.cmd)
- def get_offset(self):
- if self.byte >= 0x80: # negative
- # negative
- offset = self.next() & 0x7f
- offset = len(self.output) - offset - 1
- else:
- # positive
- offset = self.next() * 0x100
- offset += self.next()
- self.offset = offset
- def literal(self):
- """
- Copy data directly.
- """
- self.output += self.lz[ self.address : self.address + self.length ]
- self.address += self.length
- def iterate(self):
- """
- Write one byte repeatedly.
- """
- self.output += [self.next()] * self.length
- def alternate(self):
- """
- Write alternating bytes.
- """
- alts = [self.next(), self.next()]
- self.output += [ alts[x & 1] for x in xrange(self.length) ]
- def blank(self):
- """
- Write zeros.
- """
- self.output += [0] * self.length
- def flip(self):
- """
- Repeat flipped bytes from output.
- Example: 11100100 -> 00100111
- """
- self._repeat(table=bit_flipped)
- def reverse(self):
- """
- Repeat reversed bytes from output.
- """
- self._repeat(direction=-1)
- def repeat(self):
- """
- Repeat bytes from output.
- """
- self._repeat()
- def _repeat(self, direction=1, table=None):
- self.get_offset()
- self.direction = direction
- # Note: appends must be one at a time (this way, repeats can draw from themselves if required)
- for i in xrange(self.length):
- byte = self.output[ self.offset + i * direction ]
- self.output.append( table[byte] if table else byte )
- def connect(tiles):
- """
- Combine 8x8 tiles into a 2bpp image.
- """
- return [byte for tile in tiles for byte in tile]
- def transpose(tiles, width=None):
- """
- Transpose a tile arrangement along line y=-x.
- 00 01 02 03 04 05 00 06 0c 12 18 1e
- 06 07 08 09 0a 0b 01 07 0d 13 19 1f
- 0c 0d 0e 0f 10 11 <-> 02 08 0e 14 1a 20
- 12 13 14 15 16 17 03 09 0f 15 1b 21
- 18 19 1a 1b 1c 1d 04 0a 10 16 1c 22
- 1e 1f 20 21 22 23 05 0b 11 17 1d 23
- 00 01 02 03 00 04 08
- 04 05 06 07 <-> 01 05 09
- 08 09 0a 0b 02 06 0a
- 03 07 0b
- """
- if width == None:
- width = int(sqrt(len(tiles))) # assume square image
- tiles = sorted(enumerate(tiles), key= lambda i_tile: i_tile[0] % width)
- return [tile for i, tile in tiles]
- def transpose_tiles(image, width=None):
- return connect(transpose(get_tiles(image), width))
- def bitflip(x, n):
- r = 0
- while n:
- r = (r << 1) | (x & 1)
- x >>= 1
- n -= 1
- return r
- class Decompressor:
- """
- pokered pic decompression.
- Ported to python 2.7 from the python 3 code at https://github.com/magical/pokemon-sprites-rby.
- """
- table1 = [(2 << i) - 1 for i in range(16)]
- table2 = [
- [0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5, 0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa],
- [0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa, 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5], # prev ^ 0xf
- [0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa, 0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5],
- [0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5, 0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa], # prev ^ 0xf
- ]
- table3 = [bitflip(i, 4) for i in range(16)]
- tilesize = 8
- def __init__(self, f=None, d=None, mirror=False, planar=True):
- if f is not None:
- self.bs = fbitstream( f )
- elif d is not None:
- self.bs = bitstream( d )
- else:
- print("No decompressed data specified")
- raise
- self.mirror = mirror
- self.planar = planar
- self.data = None
- def decompress(self):
- rams = [[], []]
- self.sizex = self._readint(4) * self.tilesize
- self.sizey = self._readint(4)
- self.size = self.sizex * self.sizey
- self.ramorder = self._readbit()
- r1 = self.ramorder
- r2 = self.ramorder ^ 1
- self._fillram(rams[r1])
- mode = self._readbit()
- if mode:
- mode += self._readbit()
- self._fillram(rams[r2])
- rams[0] = bytearray(bitgroups_to_bytes(rams[0]))
- rams[1] = bytearray(bitgroups_to_bytes(rams[1]))
- if mode == 0:
- self._decode(rams[0])
- self._decode(rams[1])
- elif mode == 1:
- self._decode(rams[r1])
- self._xor(rams[r1], rams[r2])
- elif mode == 2:
- self._decode(rams[r2], mirror=False)
- self._decode(rams[r1])
- self._xor(rams[r1], rams[r2])
- else:
- raise Exception("Invalid deinterlace mode!")
- data = []
- if self.planar:
- for a, b in zip(rams[0], rams[1]):
- data += [a, b]
- self.data = bytearray(data)
- else:
- for a, b in zip(bitstream(rams[0]), bitstream(rams[1])):
- data.append(a | (b << 1))
- self.data = bitgroups_to_bytes(data)
- def _fillram(self, ram):
- mode = ['rle', 'data'][self._readbit()]
- size = self.size * 4
- while len(ram) < size:
- if mode == 'rle':
- self._read_rle_chunk(ram)
- mode = 'data'
- elif mode == 'data':
- self._read_data_chunk(ram, size)
- mode = 'rle'
- '''if len(ram) > size:
- #ram = ram[:size]
- raise ValueError(size, len(ram))
- '''
- ram[:] = self._deinterlace_bitgroups(ram)
- def _read_rle_chunk(self, ram):
- i = 0
- while self._readbit():
- i += 1
- n = self.table1[i]
- a = self._readint(i + 1)
- n += a
- for i in range(n):
- ram.append(0)
- def _read_data_chunk(self, ram, size):
- while 1:
- bitgroup = self._readint(2)
- if bitgroup == 0:
- break
- ram.append(bitgroup)
- if size <= len(ram):
- break
- def _decode(self, ram, mirror=None):
- if mirror is None:
- mirror = self.mirror
- for x in range(self.sizex):
- bit = 0
- for y in range(self.sizey):
- i = y * self.sizex + x
- a = (ram[i] >> 4) & 0xf
- b = ram[i] & 0xf
- a = self.table2[bit][a]
- bit = a & 1
- if mirror:
- a = self.table3[a]
- b = self.table2[bit][b]
- bit = b & 1
- if mirror:
- b = self.table3[b]
- ram[i] = (a << 4) | b
- def _xor(self, ram1, ram2, mirror=None):
- if mirror is None:
- mirror = self.mirror
- for i in range(len(ram2)):
- if mirror:
- a = (ram2[i] >> 4) & 0xf
- b = ram2[i] & 0xf
- a = self.table3[a]
- b = self.table3[b]
- ram2[i] = (a << 4) | b
- ram2[i] ^= ram1[i]
- def _deinterlace_bitgroups(self, bits):
- l = []
- for y in range(self.sizey):
- for x in range(self.sizex):
- i = 4 * y * self.sizex + x
- for j in range(4):
- l.append(bits[i])
- i += self.sizex
- return l
- def _readbit(self):
- return next(self.bs)
- def _readint(self, count):
- return readint(self.bs, count)
- def decompress(f, offset=None, mirror=False):
- """
- Decompress a pic given a file object. Return a planar 2bpp image.
- Optional: offset (for roms).
- """
- if offset is not None:
- f.seek(offset)
- dcmp = Decompressor(f, mirror=mirror)
- dcmp.decompress()
- return dcmp.data
- def decomp_main( in_path, out_path ):
- if os.path.exists( out_path ):
- print( out_path + " already exists, use a different name" )
- else:
- if not os.path.exists( in_path ):
- print( in_path + " does not exist" )
- else:
- with open( in_path, 'rb' ) as in_file:
- sprite = decompress( in_file )
- with open( out_path, 'wb' ) as out_file:
- out_file.write( sprite )
- # hex converter
- def str2byt(x):
- done = False
- out = b''
- byte = 0
- for nybble in x:
- if not done:
- byte = int( nybble, 16 )
- byte <<= 4
- else:
- byte |= ( int( nybble, 16 ) & 0xF )
- out = out + bytes([byte])
- done = not done
- return out
- hx1 = lambda n: hex(n)[2:].zfill(2)
- REGEX_DEC = re.compile(r'^ d[bw] ((?:[0-9]{3},?)+)\s*(?:;.*)?$')
- REGEX_HEX = re.compile(r'^ d[bw] ((?:0(?:[A-Fa-f0-9]{2})+h,?)+)\s*(?:;.*)?$')
- def divhx(x):
- pcs = []
- for i in range(len(x) // 2):
- pcs.append(x[i*2:i*2+2])
- return ''.join(pcs[::-1])
- def divhxl(x):
- out = ''
- for k in x:
- out += divhx(k)
- return out
- def hex_convert_main( input_string, data_type=None, v=None ):
- lins = input_string.splitlines()
- if not data_type: # we have to guess the type
- r1 = REGEX_DEC
- r2 = REGEX_HEX
- for i in range( len(lins) ):
- if r1.match(lins[i]):
- data_type = 0
- break
- elif r2.match(lins[i]):
- data_type = 1
- break
- else:
- raise Exception
- if data_type == 0: # decimal
- reg = REGEX_DEC
- else:
- reg = REGEX_HEX
- datout = ''
- uuct = 0
- for i,l in enumerate(lins):
- mm = reg.match(l)
- if mm:
- cur_dats = mm.groups()[0].split(',')
- if data_type == 0:
- cur_dats_2 = [hx1(int(x[1:-1])) for x in cur_dats]
- else:
- cur_dats_2 = [x[1:-1] for x in cur_dats]
- datout += ''.join(cur_dats_2)
- else:
- if v:
- print('Line does not match pattern:')
- print(i+1, l)
- uuct += 1
- if uuct > 10:
- input()
- uuct = 0
- try:
- return bytes.fromhex( datout )
- except AttributeError:
- return bytearray.fromhex( datout )
- # makeimg.py
- GBGRY = ((232, 232, 232), (160, 160, 160), (88, 88, 88), (16, 16, 16))
- GBGRH = ((255, 255, 255), (176, 176, 176), (104, 104, 104), (0, 0, 0))
- GBGRN = ((224, 248, 208), (136, 192, 112), (52, 104, 86), (8, 24, 32))
- # GBGRHR = ((255, 255, 255), (104, 104, 104), (176, 176, 176), (0, 0, 0))
- GBGRHR = ((255, 255, 255), (85, 85, 85), (170, 170, 170), (0, 0, 0))
- # 255 * 1 , 255 * (1/3), 255 * (2/3) , 255 * 0
- def makeimg_main(args):
- dat = None
- cbb = False
- _2bpp = False
- nothing = False
- if not args.out and not args.width and not args.depth and not args.scale and not args.reverse and not args.sprite and not args.subblk \
- and not args.view and not args.palette and not args.horizontal and not args.square and not args.extradat and not args.flag_iscbb \
- and not args.flag_is2bpp and not args.flag_ishex and not args.flag_decimal and not args.flag_guess and not args.flag_compressed:
- nothing = True
- output_name = ''
- # is args.data a data string or a file name?
- if re.match(r'^(?:[A-Fa-f0-9]{2})+$', args.data): # is args.data a data string?
- if not os.path.exists( args.data ): # is it *REALLY* a data string and not a file path?
- dat = bytearray(args.data, hex_char_encoding) # if yes then use it directly
- if not args.out:
- print('-out is required if the input is a bytestring')
- exit()
- else: # if not, then open the filename and convert it to a datastring
- with open( args.data, 'rb' ) as f:
- raw = f.read()
- try:
- dat = bytearray( raw, hex_char_encoding )
- except TypeError:
- dat = bytearray( raw )
- if ".cbb" in args.data.lower():
- cbb = True
- if '.2bpp' in args.data.lower():
- _2bpp = True
- output_name = args.data.lower()
- if args.flag_ishex or nothing: # Decode a hex-coded file'
- if not args.flag_is2bpp and not cbb and not _2bpp:
- string = dat.decode( hex_char_encoding )
- if args.flag_guess:
- dat = hex_convert_main( string )
- else:
- if args.flag_decimal:
- dat = hex_convert_main( string, data_type = 0 )
- else:
- dat = hex_convert_main( string, data_type = 1 )
- if args.flag_iscbb or cbb:
- if not _2bpp:
- i = 16
- while i < len(dat):
- del dat[i:i+16]
- i += 16
- if args.flag_compressed or ( nothing and (not cbb) ): # decompress a file
- if ( not _2bpp ) or args.flag_compressed:
- decomp = Decompressor( d = dat, mirror = False )
- decomp.decompress()
- dat = decomp.data
- # the PNG converter expects its input to be in a hex string format
- try:
- dat = dat.hex()
- except AttributeError:
- dat = "".join("%02x" % b for b in dat)
- # Convert data to a PNG
- # get various settings
- imglen = len(dat) // 2
- if (not args.width) or nothing: # autodetect width
- imgw = int((imglen // 16) ** 0.5)
- print( "Assuming image is square...\nwidth/height in tiles: " + str(imgw) )
- else:
- imgw = args.width
- output_name += '_' + str(imgw)+'x'+str(imgw)+'_'
- if args.depth:
- assert args.depth in (1, 2, 4, 8)
- bitd = args.depth
- else:
- bitd = 2
- if not args.palette:
- cols = GBGRHR
- if args.palette == 'green':
- cols = GBGRN
- elif args.palette == 'gray':
- cols = GBGRY
- elif args.palette == 'grayhi':
- cols = GBGRH
- elif args.palette == 'grayhir':
- cols = GBGRHR
- elif type(args.palette) == type(''): # guess shades
- # automatic gain control
- cols = []
- if args.sprite:
- for i in range(2 ** bitd - 1):
- pcnt = i / (2 ** bitd - 2)
- # ~ print(pcnt)
- pcnt = math.sqrt(pcnt)
- # ~ print(pcnt)
- rgb = int(255 * pcnt)
- cols.append( [rgb, rgb, rgb] )
- if args.reverse:
- cols = cols[::-1]
- cols.insert(0, [255, 255, 255])
- else:
- for i in range(2 ** bitd):
- pcnt = i / (2 ** bitd - 1)
- rgb = int(255 * pcnt)
- cols.append([rgb, rgb, rgb])
- if args.reverse:
- cols = cols[::-1]
- imgh = imglen // bitd // imgw // 8
- if imgh * imgw * bitd * 8 != imglen:
- imgh += 1
- out_tmp = [[0 for x in range(imgw*8*3)] for y in range(imgh*8)]
- #out = Image.new('RGB', (imgw*8, imgh*8))
- #pxa = out.load()
- i = 0
- binimg = ''
- for nib in dat:
- binimg += bin(int(nib, 16))[2:].zfill(4)
- if bitd != 2:
- if args.subblk:
- for blky2 in range(imgh // 2):
- for blkx2 in range(imgw // 2):
- for blkyy in range(2):
- for blkxx in range(2):
- for yy in range(8):
- for xx in range(8):
- blkx = blkx2 * 2 + blkxx
- blky = blky2 * 2 + blkyy
- if nothing or not args.horizontal:
- x = blky * 8 + xx
- y = blkx * 8 + yy
- else:
- x = blkx * 8 + xx
- y = blky * 8 + yy
- if i >= len(binimg):
- continue
- colid = binimg[i*bitd:(i+1)*bitd]
- out_tmp[y][(x*3)+0] = cols[int(colid, 2)][0]
- out_tmp[y][(x*3)+1] = cols[int(colid, 2)][1]
- out_tmp[y][(x*3)+2] = cols[int(colid, 2)][2]
- i += 1
- else:
- for blky in range(imgh):
- for blkx in range(imgw):
- for yy in range(8):
- for xx in range(8):
- if nothing or not args.horizontal:
- x = blky * 8 + xx
- y = blkx * 8 + yy
- else:
- x = blkx * 8 + xx
- y = blky * 8 + yy
- if i >= len(binimg):
- continue
- colid = binimg[i*bitd:(i+1)*bitd]
- out_tmp[y][(x*3)+0] = cols[int(colid, 2)][0]
- out_tmp[y][(x*3)+1] = cols[int(colid, 2)][1]
- out_tmp[y][(x*3)+2] = cols[int(colid, 2)][2]
- i += 1
- else:
- if args.subblk:
- for blky2 in range(imgh // 2):
- for blkx2 in range(imgw // 2):
- for blkyy in range(2):
- for blkxx in range(2):
- for yy in range(8):
- for xx in range(8):
- blkx = blkx2 * 2 + blkxx
- blky = blky2 * 2 + blkyy
- if nothing or not args.horizontal:
- x = blky * 8 + xx
- y = blkx * 8 + yy
- else:
- x = blkx * 8 + xx
- y = blky * 8 + yy
- if i >= len(binimg):
- continue
- colidhi = binimg[i]
- colidlo = binimg[i + 8]
- colid = colidhi + colidlo
- out_tmp[y][(x*3)+0] = cols[int(colid, 2)][0]
- out_tmp[y][(x*3)+1] = cols[int(colid, 2)][1]
- out_tmp[y][(x*3)+2] = cols[int(colid, 2)][2]
- i += 1
- i += 8
- else:
- for blky in range(imgh):
- for blkx in range(imgw):
- for yy in range(8):
- for xx in range(8):
- if nothing or not args.horizontal:
- x = blky * 8 + xx
- y = blkx * 8 + yy
- else:
- x = blkx * 8 + xx
- y = blky * 8 + yy
- if i >= len(binimg):
- continue
- try:
- colidhi = binimg[i]
- colidlo = binimg[i + 8]
- colid = colidhi + colidlo
- out_tmp[y][(x*3)+0] = cols[int(colid, 2)][0]
- out_tmp[y][(x*3)+1] = cols[int(colid, 2)][1]
- out_tmp[y][(x*3)+2] = cols[int(colid, 2)][2]
- except IndexError:
- pass
- i += 1
- i += 8
- if args.scale:
- print("Sprite scaling is not implemented")
- # oldw, oldh = out.size
- # neww = oldw * args.scale
- # newh = oldh * args.scale
- # out = out.resize((neww, newh))
- # # ~ out = transform.scale(out, (neww, newh))
- out = from_array( out_tmp, 'RGB' )
- if not args.out:
- output_name += '.png'
- out.save(output_name)
- else:
- out.save(args.out)
- #
- if __name__ == '__main__':
- parser = argparse.ArgumentParser(
- formatter_class=argparse.RawDescriptionHelpFormatter,
- description=textwrap.dedent("""Make a PNG from a given GB graphic file
- Examples:
- makeimg_standalone.py SPRITE.DAT
- makeimg_standalone.py SPRITE.CBB
- makeimg_standalone.py -cbb -C SPRITE.CBB
- makeimg_standalone.py -H SPRITE.DAT
- makeimg_standalone.py -H -C SPRITE.DAT
- makeimg_standalone.py -H -C -d 2 -l grayhir -tq SPRITE.DAT
- makeimg_standalone.py -H -C -s 6 -d 2 -l grayhir -q SPRITE.DAT
- makeimg_standalone.py -H -C -s 6 -d 2 -l grayhir -q -out sprite_out.png SPRITE.DAT
- """)
- )
- parser.add_argument('data', type=str, help='byte string OR file to read')
- parser.add_argument('-out', type=str, help='filename to write (required if using a byte string)')
- parser.add_argument('-w', dest='width', type=int, help='width (in 8x8 tiles) of the image')
- parser.add_argument('-d', dest='depth', type=int, help='bit depth (1, 2, 4, 8)')
- parser.add_argument('-s', dest='scale', type=int, help='<- make the sprite this much bigger (not implemented)')
- parser.add_argument('-r', dest='reverse', action='store_true', help='reverse colors')
- parser.add_argument('-p', dest='sprite', action='store_true', help='use sprite colors')
- parser.add_argument('-k', dest='subblk', action='store_true', help='subblk')
- parser.add_argument('-v', dest='view', action='store_true', help='View the output file automatically')
- parser.add_argument('-l', dest='palette', type=str, help='The palette to use ("green"/"gray"/"grayhi"/"grayhir")')
- parser.add_argument('-t', dest='horizontal', action='store_true', help='Horizontal tile order?')
- parser.add_argument('-q', dest='square', action='store_true', help= 'Assume square image, autodetect width')
- parser.add_argument('-x', dest='extradat', action='store_true', help='extradat (?)')
- parser.add_argument('-cbb', dest='flag_iscbb', action='store_true', help='The input file is a CBB graphic')
- parser.add_argument('-2bpp', dest='flag_is2bpp', action='store_true', help='The input file is a raw 2bpp graphic')
- parser.add_argument('-H', dest='flag_ishex', action='store_true', help='The input file is a text-encoded binary')
- parser.add_argument('-D', dest='flag_decimal', action='store_true', help='(if -H) Input data type is decimal')
- parser.add_argument('-G', dest='flag_guess', action='store_true', help='(if -H) Try to guess input data type')
- parser.add_argument('-C', dest='flag_compressed', action='store_true', help='The input file is compressed')
- #parser.add_argument('-simple', dest="flag_simple", action='store_true', help='The same as -H -C -s 1')
- args = parser.parse_args()
- makeimg_main(args)
- #
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement