aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRangi42 <remy.oukaour+rangi42@gmail.com>2024-10-07 16:48:18 -0400
committerRangi42 <remy.oukaour+rangi42@gmail.com>2024-10-07 16:48:18 -0400
commitb52b44f8834fd64d7f279804585e0a554cf61d5b (patch)
tree7081a0f36d4bcb82786cb97361da03f48bb1e0e6
parentComment wave5 garbage data (#474) (diff)
downloadpokeyellow-b52b44f8834fd64d7f279804585e0a554cf61d5b.tar.gz
pokeyellow-b52b44f8834fd64d7f279804585e0a554cf61d5b.tar.xz
pokeyellow-b52b44f8834fd64d7f279804585e0a554cf61d5b.zip
Remove independent tools and link to pokemon-asm-tools
-rw-r--r--README.md2
-rwxr-xr-xtools/consts.py56
-rwxr-xr-xtools/free_space.awk78
-rwxr-xr-xtools/palfix.py63
-rw-r--r--tools/png.py2357
-rwxr-xr-xtools/rgb555.py38
-rwxr-xr-xtools/sym_comments.py52
-rwxr-xr-xtools/toc.py99
-rwxr-xr-xtools/unique.py106
9 files changed, 2 insertions, 2849 deletions
diff --git a/README.md b/README.md
index d522f2db..fa804e06 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ To set up the repository, see [**INSTALL.md**](INSTALL.md).
- [**Wiki**][wiki] (includes [tutorials][tutorials])
- [**Symbols**][symbols]
+- [**Tools**][tools]
You can find us on [Discord (pret, #pokered)](https://discord.gg/d5dubZ3).
@@ -25,5 +26,6 @@ For other pret projects, see [pret.github.io](https://pret.github.io/).
[wiki]: https://github.com/pret/pokered/wiki
[tutorials]: https://github.com/pret/pokered/wiki/Tutorials
[symbols]: https://github.com/pret/pokered/tree/symbols
+[tools]: https://github.com/pret/pokemon-asm-tools
[ci]: https://github.com/pret/pokered/actions
[ci-badge]: https://github.com/pret/pokered/actions/workflows/main.yml/badge.svg
diff --git a/tools/consts.py b/tools/consts.py
deleted file mode 100755
index 37c85951..00000000
--- a/tools/consts.py
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python consts.py constants/some_constants.asm
-
-View numeric values of `const`ants.
-"""
-
-import sys
-import re
-
-const_value = 0
-const_inc = 1
-
-def asm_int(s):
- base = {'$': 16, '&': 8, '%': 2}.get(s[0], 10)
- return int(s if base == 10 else s[1:], base)
-
-def print_const(s, v):
- print(f'{s} == {v} == ${v:x}')
-
-def parse_for_constants(line):
- global const_value, const_inc
- if not (m := re.match(r'^\s+([A-Za-z_][A-Za-z0-9_@#]*)(?:\s+([^;\\n]+))?', line)):
- return
- macro, rest = m.groups()
- args = [arg.strip() for arg in rest.split(',')] if rest else []
- if args and not args[-1]:
- args = args[:-1]
- if macro == 'const_def':
- const_value = asm_int(args[0]) if len(args) >= 1 else 0
- const_inc = asm_int(args[1]) if len(args) >= 2 else 1
- elif macro == 'const':
- print_const(args[0], const_value)
- const_value += const_inc
- elif macro == 'shift_const':
- print_const(args[0], 1 << const_value)
- print_const(args[0] + '_F', const_value)
- const_value += const_inc
- elif macro == 'const_skip':
- const_value += const_inc * (asm_int(args[0]) if len(args) >= 1 else 1)
- elif macro == 'const_next':
- const_value = asm_int(args[0])
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} constants/some_constants.asm', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- with open(filename, 'r', encoding='utf-8') as file:
- for line in file:
- parse_for_constants(line)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/free_space.awk b/tools/free_space.awk
deleted file mode 100755
index 28785377..00000000
--- a/tools/free_space.awk
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/gawk -f
-
-# Usage: tools/free_space.awk [BANK=<bank_spec>] pokered.map
-
-# The BANK argument allows printing free space in one, all, or none of the ROM's banks.
-# Valid arguments are numbers (in decimal "42" or hexadecimal "0x2a"), "all" or "none".
-# If not specified, defaults to "none".
-# The `BANK` argument MUST be before the map file name, otherwise it has no effect!
-# Yes: tools/free_space.awk BANK=all pokered.map
-# No: tools/free_space.awk pokered.map BANK=42
-
-# Copyright (c) 2020, Eldred Habert.
-# SPDX-License-Identifier: MIT
-
-BEGIN {
- nb_banks = 0
- free = 0
- rom_bank = 0 # Safety net for malformed files
-
- # Default settings
- # Variables assigned via the command-line (except through `-v`) are *after* `BEGIN`
- BANK="none"
-}
-
-# Only accept ROM banks, ignore everything else
-toupper($0) ~ /^[ \t]*ROM[0X][ \t]+BANK[ \t]+#/ {
- nb_banks++
- rom_bank = 1
- split($0, fields, /[ \t]/)
- bank_num = strtonum(substr(fields[3], 2))
-}
-
-function register_bank(amount) {
- free += amount
- rom_bank = 0 # Reject upcoming banks by default
-
- if (BANK ~ /all/ || BANK == bank_num) {
- printf "Bank %3d: %5d/16384 (%.2f%%)\n", bank_num, amount, amount * 100 / 16384
- }
-}
-function register_bank_str(str) {
- if (str ~ /\$[0-9A-F]+/) {
- register_bank(strtonum("0x" substr(str, 2)))
- } else {
- printf "Malformed number? \"%s\" does not start with '$'\n", str
- }
-}
-
-rom_bank && toupper($0) ~ /^[ \t]*EMPTY$/ {
- # Empty bank
- register_bank(16384)
-}
-rom_bank && toupper($0) ~ /^[ \t]*SLACK:[ \t]/ {
- # Old (rgbds <=0.6.0) end-of-bank free space
- register_bank_str($2)
-}
-rom_bank && toupper($0) ~ /^[ \t]*TOTAL EMPTY:[ \t]/ {
- # New (rgbds >=0.6.1) total free space
- register_bank_str($3)
-}
-
-END {
- # Determine number of banks, by rounding to upper power of 2
- total_banks = 2 # Smallest size is 2 banks
- while (total_banks < nb_banks) {
- total_banks *= 2
- }
-
- # RGBLINK omits "trailing" ROM banks, so fake them
- bank_num = nb_banks
- while (bank_num < total_banks) {
- register_bank(16384)
- bank_num++
- }
-
- total = total_banks * 16384
- printf "Free space: %5d/%5d (%.2f%%)\n", free, total, free * 100 / total
-}
diff --git a/tools/palfix.py b/tools/palfix.py
deleted file mode 100755
index 3a997d54..00000000
--- a/tools/palfix.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python palfix.py image.png
-
-Fix the palette format of the input image to two-bit grayscale.
-Colored images will have their palette sorted {white, light
-color, dark color, black}.
-"""
-
-import sys
-
-import png
-
-def rgb8_to_rgb5(c):
- r, g, b = c
- return (r // 8, g // 8, b // 8)
-
-def invert(c):
- r, g, b = c
- return (31 - r, 31 - g, 31 - b)
-
-def luminance(c):
- r, g, b = c
- return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2
-
-def rgb5_pixels(row):
- yield from (rgb8_to_rgb5(row[x:x+3]) for x in range(0, len(row), 4))
-
-def fix_pal(filename):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = list(rows)
- b_and_w = {(0, 0, 0), (31, 31, 31)}
- colors = {c for row in rows for c in rgb5_pixels(row)} - b_and_w
- if not colors:
- colors = {(21, 21, 21), (10, 10, 10)}
- elif len(colors) == 1:
- c = colors.pop()
- colors = {c, invert(c)}
- elif len(colors) != 2:
- return False
- palette = tuple(sorted(colors | b_and_w, key=luminance, reverse=True))
- assert len(palette) == 4
- rows = [[3 - palette.index(c) for c in rgb5_pixels(row)] for row in rows]
- writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
- return True
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- elif not fix_pal(filename):
- print(f'{filename} has too many colors!', file=sys.stderr)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/png.py b/tools/png.py
deleted file mode 100644
index dd9746bf..00000000
--- a/tools/png.py
+++ /dev/null
@@ -1,2357 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# png.py - PNG encoder/decoder in pure Python
-#
-# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
-# Portions Copyright (C) 2009 David Jones <drj@pobox.com>
-# And probably portions Copyright (C) 2006 Nicko van Someren <nicko@nicko.org>
-#
-# Original concept by Johann C. Rocholl.
-#
-# LICENCE (MIT)
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-"""
-The ``png`` module can read and write PNG files.
-
-Installation and Overview
--------------------------
-
-``pip install pypng``
-
-For help, type ``import png; help(png)`` in your python interpreter.
-
-A good place to start is the :class:`Reader` and :class:`Writer` classes.
-
-Coverage of PNG formats is fairly complete;
-all allowable bit depths (1/2/4/8/16/24/32/48/64 bits per pixel) and
-colour combinations are supported:
-
-- greyscale (1/2/4/8/16 bit);
-- RGB, RGBA, LA (greyscale with alpha) with 8/16 bits per channel;
-- colour mapped images (1/2/4/8 bit).
-
-Interlaced images,
-which support a progressive display when downloading,
-are supported for both reading and writing.
-
-A number of optional chunks can be specified (when writing)
-and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``.
-
-The ``sBIT`` chunk can be used to specify precision for
-non-native bit depths.
-
-Requires Python 3.5 or higher.
-Installation is trivial,
-but see the ``README.txt`` file (with the source distribution) for details.
-
-Full use of all features will need some reading of the PNG specification
-http://www.w3.org/TR/2003/REC-PNG-20031110/.
-
-The package also comes with command line utilities.
-
-- ``pripamtopng`` converts
- `Netpbm <http://netpbm.sourceforge.net/>`_ PAM/PNM files to PNG;
-- ``pripngtopam`` converts PNG to file PAM/PNM.
-
-There are a few more for simple PNG manipulations.
-
-Spelling and Terminology
-------------------------
-
-Generally British English spelling is used in the documentation.
-So that's "greyscale" and "colour".
-This not only matches the author's native language,
-it's also used by the PNG specification.
-
-Colour Models
--------------
-
-The major colour models supported by PNG (and hence by PyPNG) are:
-
-- greyscale;
-- greyscale--alpha;
-- RGB;
-- RGB--alpha.
-
-Also referred to using the abbreviations: L, LA, RGB, RGBA.
-Each letter codes a single channel:
-*L* is for Luminance or Luma or Lightness (greyscale images);
-*A* stands for Alpha, the opacity channel
-(used for transparency effects, but higher values are more opaque,
-so it makes sense to call it opacity);
-*R*, *G*, *B* stand for Red, Green, Blue (colour image).
-
-Lists, arrays, sequences, and so on
------------------------------------
-
-When getting pixel data out of this module (reading) and
-presenting data to this module (writing) there are
-a number of ways the data could be represented as a Python value.
-
-The preferred format is a sequence of *rows*,
-which each row being a sequence of *values*.
-In this format, the values are in pixel order,
-with all the values from all the pixels in a row
-being concatenated into a single sequence for that row.
-
-Consider an image that is 3 pixels wide by 2 pixels high, and each pixel
-has RGB components:
-
-Sequence of rows::
-
- list([R,G,B, R,G,B, R,G,B],
- [R,G,B, R,G,B, R,G,B])
-
-Each row appears as its own list,
-but the pixels are flattened so that three values for one pixel
-simply follow the three values for the previous pixel.
-
-This is the preferred because
-it provides a good compromise between space and convenience.
-PyPNG regards itself as at liberty to replace any sequence type with
-any sufficiently compatible other sequence type;
-in practice each row is an array (``bytearray`` or ``array.array``).
-
-To allow streaming the outer list is sometimes
-an iterator rather than an explicit list.
-
-An alternative format is a single array holding all the values.
-
-Array of values::
-
- [R,G,B, R,G,B, R,G,B,
- R,G,B, R,G,B, R,G,B]
-
-The entire image is one single giant sequence of colour values.
-Generally an array will be used (to save space), not a list.
-
-The top row comes first,
-and within each row the pixels are ordered from left-to-right.
-Within a pixel the values appear in the order R-G-B-A
-(or L-A for greyscale--alpha).
-
-There is another format, which should only be used with caution.
-It is mentioned because it is used internally,
-is close to what lies inside a PNG file itself,
-and has some support from the public API.
-This format is called *packed*.
-When packed, each row is a sequence of bytes (integers from 0 to 255),
-just as it is before PNG scanline filtering is applied.
-When the bit depth is 8 this is the same as a sequence of rows;
-when the bit depth is less than 8 (1, 2 and 4),
-several pixels are packed into each byte;
-when the bit depth is 16 each pixel value is decomposed into 2 bytes
-(and `packed` is a misnomer).
-This format is used by the :meth:`Writer.write_packed` method.
-It isn't usually a convenient format,
-but may be just right if the source data for
-the PNG image comes from something that uses a similar format
-(for example, 1-bit BMPs, or another PNG file).
-"""
-
-__version__ = "0.0.21"
-
-import collections
-import io # For io.BytesIO
-import itertools
-import math
-# http://www.python.org/doc/2.4.4/lib/module-operator.html
-import operator
-import re
-import struct
-import sys
-# http://www.python.org/doc/2.4.4/lib/module-warnings.html
-import warnings
-import zlib
-
-from array import array
-
-
-__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array']
-
-
-# The PNG signature.
-# http://www.w3.org/TR/PNG/#5PNG-file-signature
-signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
-
-# The xstart, ystart, xstep, ystep for the Adam7 interlace passes.
-adam7 = ((0, 0, 8, 8),
- (4, 0, 8, 8),
- (0, 4, 4, 8),
- (2, 0, 4, 4),
- (0, 2, 2, 4),
- (1, 0, 2, 2),
- (0, 1, 1, 2))
-
-
-def adam7_generate(width, height):
- """
- Generate the coordinates for the reduced scanlines
- of an Adam7 interlaced image
- of size `width` by `height` pixels.
-
- Yields a generator for each pass,
- and each pass generator yields a series of (x, y, xstep) triples,
- each one identifying a reduced scanline consisting of
- pixels starting at (x, y) and taking every xstep pixel to the right.
- """
-
- for xstart, ystart, xstep, ystep in adam7:
- if xstart >= width:
- continue
- yield ((xstart, y, xstep) for y in range(ystart, height, ystep))
-
-
-# Models the 'pHYs' chunk (used by the Reader)
-Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter')
-
-
-def group(s, n):
- return list(zip(* [iter(s)] * n))
-
-
-def isarray(x):
- return isinstance(x, array)
-
-
-def check_palette(palette):
- """
- Check a palette argument (to the :class:`Writer` class) for validity.
- Returns the palette as a list if okay;
- raises an exception otherwise.
- """
-
- # None is the default and is allowed.
- if palette is None:
- return None
-
- p = list(palette)
- if not (0 < len(p) <= 256):
- raise ProtocolError(
- "a palette must have between 1 and 256 entries,"
- " see https://www.w3.org/TR/PNG/#11PLTE")
- seen_triple = False
- for i, t in enumerate(p):
- if len(t) not in (3, 4):
- raise ProtocolError(
- "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 parameter."""
-
-
-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 compression (which is generally acceptable).
-
- If `interlace` is true then an interlaced image is created
- (using PNG's so far only interlace 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 values, 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))
- return nrows
-
- def write_passes(self, outfile, rows):
- """
- Write a PNG image to the output file.
-
- Most users are expected to find the :meth:`write` or
- :meth:`write_array` method more convenient.
-
- The rows should be given to this method in the order that
- they appear in the output file.
- For straightlaced images, this is the usual top to bottom ordering.
- For interlaced images the rows should have been interlaced before
- passing them to this function.
-
- `rows` should be an iterable that yields each row
- (each row being a sequence of values).
- """
-
- # Ensure rows are scaled (to 4-/8-/16-bit),
- # and packed into bytes.
-
- if self.rescale:
- rows = rescale_rows(rows, self.rescale)
-
- if self.bitdepth < 8:
- rows = pack_rows(rows, self.bitdepth)
- elif self.bitdepth == 16:
- rows = unpack_rows(rows)
-
- return self.write_packed(outfile, rows)
-
- def write_packed(self, outfile, rows):
- """
- Write PNG file to `outfile`.
- `rows` should be an iterator that yields each packed row;
- a packed row being a sequence of packed bytes.
-
- The rows have a filter byte prefixed and
- are then compressed into one or more IDAT chunks.
- They are not processed any further,
- so if bitdepth is other than 1, 2, 4, 8, 16,
- the pixel values should have been scaled
- before passing them to this method.
-
- This method does work for interlaced images but it is best avoided.
- For interlaced images, the rows should be
- presented in the order that they appear in the file.
- """
-
- self.write_preamble(outfile)
-
- # http://www.w3.org/TR/PNG/#11IDAT
- if self.compression is not None:
- compressor = zlib.compressobj(self.compression)
- else:
- compressor = zlib.compressobj()
-
- # data accumulates bytes to be compressed for the IDAT chunk;
- # it's compressed when sufficiently large.
- data = bytearray()
-
- # raise i scope out of the for loop. set to -1, because the for loop
- # sets i to 0 on the first pass
- i = -1
- for i, row in enumerate(rows):
- # Add "None" filter type.
- # Currently, it's essential that this filter type be used
- # for every scanline as
- # we do not mark the first row of a reduced pass image;
- # that means we could accidentally compute
- # the wrong filtered scanline if we used
- # "up", "average", or "paeth" on such a line.
- data.append(0)
- data.extend(row)
- if len(data) > self.chunk_limit:
- compressed = compressor.compress(data)
- if len(compressed):
- write_chunk(outfile, b'IDAT', compressed)
- data = bytearray()
-
- compressed = compressor.compress(bytes(data))
- flushed = compressor.flush()
- if len(compressed) or len(flushed):
- write_chunk(outfile, b'IDAT', compressed + flushed)
- # http://www.w3.org/TR/PNG/#11IEND
- write_chunk(outfile, b'IEND')
- return i + 1
-
- def write_preamble(self, outfile):
- # http://www.w3.org/TR/PNG/#5PNG-file-signature
- outfile.write(signature)
-
- # http://www.w3.org/TR/PNG/#11IHDR
- write_chunk(outfile, b'IHDR',
- struct.pack("!2I5B", self.width, self.height,
- self.bitdepth, self.color_type,
- 0, 0, self.interlace))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11gAMA
- if self.gamma is not None:
- write_chunk(outfile, b'gAMA',
- struct.pack("!L", int(round(self.gamma * 1e5))))
-
- # See :chunk:order
- # http://www.w3.org/TR/PNG/#11sBIT
- if self.rescale:
- write_chunk(
- outfile, b'sBIT',
- struct.pack('%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)
- return self.write_passes(
- outfile,
- self.array_scanlines_interlace(pixels)
- )
- else:
- return self.write_passes(
- outfile,
- self.array_scanlines(pixels)
- )
-
- def array_scanlines(self, pixels):
- """
- Generates rows (each a sequence of values) from
- a single array of values.
- """
-
- # Values per row
- vpr = self.width * self.planes
- stop = 0
- for y in range(self.height):
- start = stop
- stop = start + vpr
- yield pixels[start:stop]
-
- def array_scanlines_interlace(self, pixels):
- """
- Generator for interlaced scanlines from an array.
- `pixels` is the full source image as a single array of values.
- The generator yields each scanline of the reduced passes in turn,
- each scanline being a sequence of values.
- """
-
- # http://www.w3.org/TR/PNG/#8InterlaceMethods
- # Array type.
- fmt = 'BH'[self.bitdepth > 8]
- # Value per row
- vpr = self.width * self.planes
-
- # Each iteration generates a scanline starting at (x, y)
- # and consisting of every xstep pixels.
- for lines in adam7_generate(self.width, self.height):
- for x, y, xstep in lines:
- # Pixels per row (of reduced image)
- ppr = int(math.ceil((self.width - x) / float(xstep)))
- # Values per row (of reduced image)
- reduced_row_len = ppr * self.planes
- if xstep == 1:
- # Easy case: line is a simple slice.
- offset = y * vpr
- yield pixels[offset: offset + vpr]
- continue
- # We have to step by xstep,
- # which we can do one plane at a time
- # using the step in Python slices.
- row = array(fmt)
- # There's no easier way to set the length of an array
- row.extend(pixels[0:reduced_row_len])
- offset = y * vpr + x * self.planes
- end_offset = (y + 1) * vpr
- skip = self.planes * xstep
- for i in range(self.planes):
- row[i::self.planes] = \
- pixels[offset + i: end_offset: skip]
- yield row
-
-
-def write_chunk(outfile, tag, data=b''):
- """
- Write a PNG chunk to the output file, including length and
- checksum.
- """
-
- data = bytes(data)
- # http://www.w3.org/TR/PNG/#5Chunk-layout
- outfile.write(struct.pack("!I", len(data)))
- outfile.write(tag)
- outfile.write(data)
- checksum = zlib.crc32(tag)
- checksum = zlib.crc32(data, checksum)
- checksum &= 2 ** 32 - 1
- outfile.write(struct.pack("!I", checksum))
-
-
-def write_chunks(out, chunks):
- """Create a PNG file by writing out the chunks."""
-
- out.write(signature)
- for chunk in chunks:
- write_chunk(out, *chunk)
-
-
-def rescale_rows(rows, rescale):
- """
- Take each row in rows (an iterator) and yield
- a fresh row with the pixels scaled according to
- the rescale parameters in the list `rescale`.
- Each element of `rescale` is a tuple of
- (source_bitdepth, target_bitdepth),
- with one element per channel.
- """
-
- # One factor for each channel
- fs = [float(2 ** s[1] - 1)/float(2 ** s[0] - 1)
- for s in rescale]
-
- # Assume all target_bitdepths are the same
- target_bitdepths = set(s[1] for s in rescale)
- assert len(target_bitdepths) == 1
- (target_bitdepth, ) = target_bitdepths
- typecode = 'BH'[target_bitdepth > 8]
-
- # Number of channels
- n_chans = len(rescale)
-
- for row in rows:
- rescaled_row = array(typecode, iter(row))
- for i in range(n_chans):
- channel = array(
- typecode,
- (int(round(fs[i] * x)) for x in row[i::n_chans]))
- rescaled_row[i::n_chans] = channel
- yield rescaled_row
-
-
-def pack_rows(rows, bitdepth):
- """Yield packed rows that are a byte array.
- Each byte is packed with the values from several pixels.
- """
-
- assert bitdepth < 8
- assert 8 % bitdepth == 0
-
- # samples per byte
- spb = int(8 / bitdepth)
-
- def make_byte(block):
- """Take a block of (2, 4, or 8) values,
- and pack them into a single byte.
- """
-
- res = 0
- for v in block:
- res = (res << bitdepth) + v
- return res
-
- for row in rows:
- a = bytearray(row)
- # Adding padding bytes so we can group into a whole
- # number of spb-tuples.
- n = float(len(a))
- extra = math.ceil(n / spb) * spb - n
- a.extend([0] * int(extra))
- # Pack into bytes.
- # Each block is the samples for one byte.
- blocks = group(a, spb)
- yield bytearray(make_byte(block) for block in blocks)
-
-
-def unpack_rows(rows):
- """Unpack each row from being 16-bits per value,
- to being a sequence of bytes.
- """
- for row in rows:
- fmt = '!%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)
- 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.
- No signature (empty read()) will raise EOFError;
- An invalid signature will raise FormatError.
- EOFError is raised to make possible the case where
- a program can read multiple PNG files from the same stream.
- The end of the stream can be distinguished from non-PNG files
- or corrupted PNG files.
- """
-
- if self.signature:
- return
- self.signature = self.file.read(8)
- if len(self.signature) == 0:
- raise EOFError("End of PNG stream.")
- 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]
-
-
-# Only reason to include this in this module is that
-# several utilities need it, and it is small.
-def binary_stdin():
- """
- A sys.stdin that returns bytes.
- """
-
- return sys.stdin.buffer
-
-
-def binary_stdout():
- """
- A sys.stdout that accepts bytes.
- """
-
- stdout = sys.stdout.buffer
-
- # On Windows the C runtime file orientation needs changing.
- if sys.platform == "win32":
- import msvcrt
- import os
- msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
-
- return stdout
-
-
-def cli_open(path):
- if path == "-":
- return binary_stdin()
- return open(path, "rb")
-
-
-def main(argv):
- """
- Run command line PNG.
- Which reports version.
- """
- print(__version__, __file__)
-
-
-if __name__ == '__main__':
- try:
- main(sys.argv)
- except Error as e:
- print(e, file=sys.stderr)
diff --git a/tools/rgb555.py b/tools/rgb555.py
deleted file mode 100755
index bf650313..00000000
--- a/tools/rgb555.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python rgb555.py image.png
-
-Round all colors of the input image to RGB555.
-"""
-
-import sys
-
-import png
-
-def rgb8_to_rgb5(c):
- return (c & 0b11111000) | (c >> 5)
-
-def round_pal(filename):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = list(rows)
- for row in rows:
- del row[3::4]
- rows = [[rgb8_to_rgb5(c) for c in row] for row in rows]
- writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- round_pal(filename)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/sym_comments.py b/tools/sym_comments.py
deleted file mode 100755
index 0f060e5c..00000000
--- a/tools/sym_comments.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python sym_comments.py file.asm [pokered.sym] > file_commented.asm
-
-Comments each label in file.asm with its bank:address from the sym file.
-"""
-
-import sys
-import re
-
-def main():
- if len(sys.argv) not in {2, 3}:
- print(f'Usage: {sys.argv[0]} file.asm [pokered.sym] > file_commented.asm', file=sys.stderr)
- sys.exit(1)
-
- wram_name = sys.argv[1]
- sym_name = sys.argv[2] if len(sys.argv) == 3 else 'pokered.sym'
-
- sym_def_rx = re.compile(r'(^{sym})(:.*)|(^\.{sym})(.*)'.format(sym=r'[A-Za-z_][A-Za-z0-9_#@]*'))
-
- sym_addrs = {}
- with open(sym_name, 'r', encoding='utf-8') as file:
- for line in file:
- line = line.split(';', 1)[0].rstrip()
- parts = line.split(' ', 1)
- if len(parts) != 2:
- continue
- addr, sym = parts
- sym_addrs[sym] = addr
-
- with open(wram_name, 'r', encoding='utf-8') as file:
- cur_label = None
- for line in file:
- line = line.rstrip()
- if (m = re.match(sym_def_rx, line)):
- sym, rest = m.group(1), m.group(2)
- if sym is None and rest is None:
- sym, rest = m.group(3), m.group(4)
- key = sym
- if not sym.startswith('.'):
- cur_label = sym
- elif cur_label:
- key = cur_label + sym
- if key in sym_addrs:
- addr = sym_addrs[key]
- line = sym + rest + ' ; ' + addr
- print(line)
-
-if __name__ == '__main__':
- main()
diff --git a/tools/toc.py b/tools/toc.py
deleted file mode 100755
index d9a3d963..00000000
--- a/tools/toc.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python toc.py file.md
-
-Replace a "## TOC" heading in a Markdown file with a table of contents,
-generated from the other headings in the file. Supports multiple files.
-Headings must start with "##" signs to be detected.
-"""
-
-import sys
-import re
-from collections import namedtuple
-from urllib.parse import quote
-
-toc_name = 'Contents'
-valid_toc_headings = {'## TOC', '##TOC'}
-
-TocItem = namedtuple('TocItem', ['name', 'anchor', 'level'])
-punctuation_rx = re.compile(r'[^\w\- ]+')
-numbered_heading_rx = re.compile(r'^[0-9]+\. ')
-specialchar_rx = re.compile(r'[⅔]+')
-
-def name_to_anchor(name):
- # GitHub's algorithm for generating anchors from headings
- # https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
- anchor = name.strip().lower() # lowercase
- anchor = re.sub(punctuation_rx, '', anchor) # remove punctuation
- anchor = anchor.replace(' ', '-') # replace spaces with dash
- anchor = re.sub(specialchar_rx, '', anchor) # remove misc special chars
- anchor = quote(anchor) # url encode
- return anchor
-
-def get_toc_index(lines):
- toc_index = None
- for i, line in enumerate(lines):
- if line.rstrip() in valid_toc_headings:
- toc_index = i
- break
- return toc_index
-
-def get_toc_items(lines, toc_index):
- for i, line in enumerate(lines):
- if i <= toc_index:
- continue
- if line.startswith('##'):
- name = line.lstrip('#')
- level = len(line) - len(name) - len('##')
- name = name.strip()
- anchor = name_to_anchor(name)
- yield TocItem(name, anchor, level)
-
-def toc_string(toc_items):
- lines = [f'## {toc_name}', '']
- for name, anchor, level in toc_items:
- padding = ' ' * level
- if re.match(numbered_heading_rx, name):
- bullet, name = name.split('.', 1)
- bullet += '.'
- name = name.lstrip()
- else:
- bullet = '-'
- lines.append(f'{padding}{bullet} [{name}](#{anchor})')
- return '\n'.join(lines) + '\n'
-
-def add_toc(filename):
- with open(filename, 'r', encoding='utf-8') as file:
- lines = file.readlines()
- toc_index = get_toc_index(lines)
- if toc_index is None:
- return None # no TOC heading
- toc_items = list(get_toc_items(lines, toc_index))
- if not toc_items:
- return False # no content headings
- with open(filename, 'w', encoding='utf-8') as file:
- for i, line in enumerate(lines):
- if i == toc_index:
- file.write(toc_string(toc_items))
- else:
- file.write(line)
- return True # OK
-
-def main():
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} file.md', file=sys.stderr)
- sys.exit(1)
- for filename in sys.argv[1:]:
- print(filename)
- result = add_toc(filename)
- if result is None:
- print('Warning: No "## TOC" heading found', file=sys.stderr)
- elif result is False:
- print('Warning: No content headings found', file=sys.stderr)
- else:
- print('OK')
-
-if __name__ == '__main__':
- main()
diff --git a/tools/unique.py b/tools/unique.py
deleted file mode 100755
index 34530a3e..00000000
--- a/tools/unique.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-Usage: python unique.py [-f|--flip] [-x|--cross] image.png
-
-Erase duplicate tiles from an input image.
--f or --flip counts X/Y mirrored tiles as duplicates.
--x or --cross erases with a cross instead of a blank tile.
-"""
-
-import sys
-
-import png
-
-def rgb5_pixels(row):
- yield from (tuple(c // 8 for c in row[x:x+3]) for x in range(0, len(row), 4))
-
-def rgb8_pixels(row):
- yield from (c * 8 + c // 4 for pixel in row for c in pixel)
-
-def gray_pixels(row):
- yield from (pixel[0] // 10 for pixel in row)
-
-def rows_to_tiles(rows, width, height):
- assert len(rows) == height and len(rows[0]) == width
- yield from (tuple(tuple(row[x:x+8]) for row in rows[y:y+8])
- for y in range(0, height, 8) for x in range(0, width, 8))
-
-def tiles_to_rows(tiles, width, height):
- assert width % 8 == 0 and height % 8 == 0
- width, height = width // 8, height // 8
- tiles = list(tiles)
- assert len(tiles) == width * height
- tile_rows = (tiles[y:y+width] for y in range(0, width * height, width))
- yield from ([tile[y][x] for tile in tile_row for x in range(8)]
- for tile_row in tile_rows for y in range(8))
-
-def tile_variants(tile, flip):
- yield tile
- if flip:
- yield tile[::-1]
- yield tuple(row[::-1] for row in tile)
- yield tuple(row[::-1] for row in tile[::-1])
-
-def unique_tiles(tiles, flip, cross):
- if cross:
- blank = [[(0, 0, 0)] * 8 for _ in range(8)]
- for y in range(8):
- blank[y][y] = blank[y][7 - y] = (31, 31, 31)
- blank = tuple(tuple(row) for row in blank)
- else:
- blank = tuple(tuple([(31, 31, 31)] * 8) for _ in range(8))
- seen = set()
- for tile in tiles:
- if any(variant in seen for variant in tile_variants(tile, flip)):
- yield blank
- else:
- yield tile
- seen.add(tile)
-
-def is_grayscale(colors):
- return (colors.issubset({(31, 31, 31), (21, 21, 21), (10, 10, 10), (0, 0, 0)}) or
- colors.issubset({(31, 31, 31), (20, 20, 20), (10, 10, 10), (0, 0, 0)}))
-
-def erase_duplicates(filename, flip, cross):
- with open(filename, 'rb') as file:
- width, height, rows = png.Reader(file).asRGBA8()[:3]
- rows = [list(rgb5_pixels(row)) for row in rows]
- if width % 8 or height % 8:
- return False
- tiles = unique_tiles(rows_to_tiles(rows, width, height), flip, cross)
- rows = list(tiles_to_rows(tiles, width, height))
- if is_grayscale({c for row in rows for c in row}):
- rows = [list(gray_pixels(row)) for row in rows]
- writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
- else:
- rows = [list(rgb8_pixels(row)) for row in rows]
- writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
- with open(filename, 'wb') as file:
- writer.write(file, rows)
- return True
-
-def main():
- flip = cross = False
- while True:
- if len(sys.argv) < 2:
- print(f'Usage: {sys.argv[0]} [-f|--flip] [-x|--cross] tileset.png', file=sys.stderr)
- sys.exit(1)
- elif sys.argv[1] in {'-f', '--flip'}:
- flip = True
- elif sys.argv[1] in {'-x', '--cross'}:
- cross = True
- elif sys.argv[1] in {'-fx', '-xf'}:
- flip = cross = True
- else:
- break
- sys.argv.pop(1)
- for filename in sys.argv[1:]:
- if not filename.lower().endswith('.png'):
- print(f'{filename} is not a .png file!', file=sys.stderr)
- elif not erase_duplicates(filename, flip, cross):
- print(f'{filename} is not divisible into 8x8 tiles!', file=sys.stderr)
-
-if __name__ == '__main__':
- main()