| # | 
| # The Python Imaging Library. | 
| # $Id$ | 
| # | 
| # PNG support code | 
| # | 
| # See "PNG (Portable Network Graphics) Specification, version 1.0; | 
| # W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). | 
| # | 
| # history: | 
| # 1996-05-06 fl   Created (couldn't resist it) | 
| # 1996-12-14 fl   Upgraded, added read and verify support (0.2) | 
| # 1996-12-15 fl   Separate PNG stream parser | 
| # 1996-12-29 fl   Added write support, added getchunks | 
| # 1996-12-30 fl   Eliminated circular references in decoder (0.3) | 
| # 1998-07-12 fl   Read/write 16-bit images as mode I (0.4) | 
| # 2001-02-08 fl   Added transparency support (from Zircon) (0.5) | 
| # 2001-04-16 fl   Don't close data source in "open" method (0.6) | 
| # 2004-02-24 fl   Don't even pretend to support interlaced files (0.7) | 
| # 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8) | 
| # 2004-09-20 fl   Added PngInfo chunk container | 
| # 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev) | 
| # 2008-08-13 fl   Added tRNS support for RGB images | 
| # 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech) | 
| # 2009-03-08 fl   Added zTXT support (from Lowell Alleman) | 
| # 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua) | 
| # | 
| # Copyright (c) 1997-2009 by Secret Labs AB | 
| # Copyright (c) 1996 by Fredrik Lundh | 
| # | 
| # See the README file for information on usage and redistribution. | 
| # | 
|   | 
| __version__ = "0.9" | 
|   | 
| import re, string | 
|   | 
| import Image, ImageFile, ImagePalette, zlib | 
|   | 
|   | 
| def i16(c): | 
|     return ord(c[1]) + (ord(c[0])<<8) | 
| def i32(c): | 
|     return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) | 
|   | 
| is_cid = re.compile("\w\w\w\w").match | 
|   | 
|   | 
| _MAGIC = "\211PNG\r\n\032\n" | 
|   | 
|   | 
| _MODES = { | 
|     # supported bits/color combinations, and corresponding modes/rawmodes | 
|     (1, 0): ("1", "1"), | 
|     (2, 0): ("L", "L;2"), | 
|     (4, 0): ("L", "L;4"), | 
|     (8, 0): ("L", "L"), | 
|     (16,0): ("I", "I;16B"), | 
|     (8, 2): ("RGB", "RGB"), | 
|     (16,2): ("RGB", "RGB;16B"), | 
|     (1, 3): ("P", "P;1"), | 
|     (2, 3): ("P", "P;2"), | 
|     (4, 3): ("P", "P;4"), | 
|     (8, 3): ("P", "P"), | 
|     (8, 4): ("LA", "LA"), | 
|     (16,4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available | 
|     (8, 6): ("RGBA", "RGBA"), | 
|     (16,6): ("RGBA", "RGBA;16B"), | 
| } | 
|   | 
|   | 
| # -------------------------------------------------------------------- | 
| # Support classes.  Suitable for PNG and related formats like MNG etc. | 
|   | 
| class ChunkStream: | 
|   | 
|     def __init__(self, fp): | 
|   | 
|         self.fp = fp | 
|         self.queue = [] | 
|   | 
|         if not hasattr(Image.core, "crc32"): | 
|             self.crc = self.crc_skip | 
|   | 
|     def read(self): | 
|         "Fetch a new chunk. Returns header information." | 
|   | 
|         if self.queue: | 
|             cid, pos, len = self.queue[-1] | 
|             del self.queue[-1] | 
|             self.fp.seek(pos) | 
|         else: | 
|             s = self.fp.read(8) | 
|             cid = s[4:] | 
|             pos = self.fp.tell() | 
|             len = i32(s) | 
|   | 
|         if not is_cid(cid): | 
|             raise SyntaxError, "broken PNG file (chunk %s)" % repr(cid) | 
|   | 
|         return cid, pos, len | 
|   | 
|     def close(self): | 
|         self.queue = self.crc = self.fp = None | 
|   | 
|     def push(self, cid, pos, len): | 
|   | 
|         self.queue.append((cid, pos, len)) | 
|   | 
|     def call(self, cid, pos, len): | 
|         "Call the appropriate chunk handler" | 
|   | 
|         if Image.DEBUG: | 
|             print "STREAM", cid, pos, len | 
|         return getattr(self, "chunk_" + cid)(pos, len) | 
|   | 
|     def crc(self, cid, data): | 
|         "Read and verify checksum" | 
|   | 
|         crc1 = Image.core.crc32(data, Image.core.crc32(cid)) | 
|         crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) | 
|         if crc1 != crc2: | 
|             raise SyntaxError, "broken PNG file"\ | 
|                 "(bad header checksum in %s)" % cid | 
|   | 
|     def crc_skip(self, cid, data): | 
|         "Read checksum.  Used if the C module is not present" | 
|   | 
|         self.fp.read(4) | 
|   | 
|     def verify(self, endchunk = "IEND"): | 
|   | 
|         # Simple approach; just calculate checksum for all remaining | 
|         # blocks.  Must be called directly after open. | 
|   | 
|         cids = [] | 
|   | 
|         while 1: | 
|             cid, pos, len = self.read() | 
|             if cid == endchunk: | 
|                 break | 
|             self.crc(cid, ImageFile._safe_read(self.fp, len)) | 
|             cids.append(cid) | 
|   | 
|         return cids | 
|   | 
|   | 
| # -------------------------------------------------------------------- | 
| # PNG chunk container (for use with save(pnginfo=)) | 
|   | 
| class PngInfo: | 
|   | 
|     def __init__(self): | 
|         self.chunks = [] | 
|   | 
|     def add(self, cid, data): | 
|         self.chunks.append((cid, data)) | 
|   | 
|     def add_text(self, key, value, zip=0): | 
|         if zip: | 
|             import zlib | 
|             self.add("zTXt", key + "\0\0" + zlib.compress(value)) | 
|         else: | 
|             self.add("tEXt", key + "\0" + value) | 
|   | 
| # -------------------------------------------------------------------- | 
| # PNG image stream (IHDR/IEND) | 
|   | 
| class PngStream(ChunkStream): | 
|   | 
|     def __init__(self, fp): | 
|   | 
|         ChunkStream.__init__(self, fp) | 
|   | 
|         # local copies of Image attributes | 
|         self.im_info = {} | 
|         self.im_text = {} | 
|         self.im_size = (0,0) | 
|         self.im_mode = None | 
|         self.im_tile = None | 
|         self.im_palette = None | 
|   | 
|     def chunk_iCCP(self, pos, len): | 
|   | 
|         # ICC profile | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         # according to PNG spec, the iCCP chunk contains: | 
|         # Profile name  1-79 bytes (character string) | 
|         # Null separator        1 byte (null character) | 
|         # Compression method    1 byte (0) | 
|         # Compressed profile    n bytes (zlib with deflate compression) | 
|         i = string.find(s, chr(0)) | 
|         if Image.DEBUG: | 
|             print "iCCP profile name", s[:i] | 
|             print "Compression method", ord(s[i]) | 
|         comp_method = ord(s[i]) | 
|         if comp_method != 0: | 
|             raise SyntaxError("Unknown compression method %s in iCCP chunk" % comp_method) | 
|         try: | 
|             icc_profile = zlib.decompress(s[i+2:]) | 
|         except zlib.error: | 
|             icc_profile = None # FIXME | 
|         self.im_info["icc_profile"] = icc_profile | 
|         return s | 
|   | 
|     def chunk_IHDR(self, pos, len): | 
|   | 
|         # image header | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         self.im_size = i32(s), i32(s[4:]) | 
|         try: | 
|             self.im_mode, self.im_rawmode = _MODES[(ord(s[8]), ord(s[9]))] | 
|         except: | 
|             pass | 
|         if ord(s[12]): | 
|             self.im_info["interlace"] = 1 | 
|         if ord(s[11]): | 
|             raise SyntaxError, "unknown filter category" | 
|         return s | 
|   | 
|     def chunk_IDAT(self, pos, len): | 
|   | 
|         # image data | 
|         self.im_tile = [("zip", (0,0)+self.im_size, pos, self.im_rawmode)] | 
|         self.im_idat = len | 
|         raise EOFError | 
|   | 
|     def chunk_IEND(self, pos, len): | 
|   | 
|         # end of PNG image | 
|         raise EOFError | 
|   | 
|     def chunk_PLTE(self, pos, len): | 
|   | 
|         # palette | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         if self.im_mode == "P": | 
|             self.im_palette = "RGB", s | 
|         return s | 
|   | 
|     def chunk_tRNS(self, pos, len): | 
|   | 
|         # transparency | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         if self.im_mode == "P": | 
|             i = string.find(s, chr(0)) | 
|             if i >= 0: | 
|                 self.im_info["transparency"] = i | 
|         elif self.im_mode == "L": | 
|             self.im_info["transparency"] = i16(s) | 
|         elif self.im_mode == "RGB": | 
|             self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) | 
|         return s | 
|   | 
|     def chunk_gAMA(self, pos, len): | 
|   | 
|         # gamma setting | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         self.im_info["gamma"] = i32(s) / 100000.0 | 
|         return s | 
|   | 
|     def chunk_pHYs(self, pos, len): | 
|   | 
|         # pixels per unit | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         px, py = i32(s), i32(s[4:]) | 
|         unit = ord(s[8]) | 
|         if unit == 1: # meter | 
|             dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) | 
|             self.im_info["dpi"] = dpi | 
|         elif unit == 0: | 
|             self.im_info["aspect"] = px, py | 
|         return s | 
|   | 
|     def chunk_tEXt(self, pos, len): | 
|   | 
|         # text | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         try: | 
|             k, v = string.split(s, "\0", 1) | 
|         except ValueError: | 
|             k = s; v = "" # fallback for broken tEXt tags | 
|         if k: | 
|             self.im_info[k] = self.im_text[k] = v | 
|         return s | 
|   | 
|     def chunk_zTXt(self, pos, len): | 
|   | 
|         # compressed text | 
|         s = ImageFile._safe_read(self.fp, len) | 
|         k, v = string.split(s, "\0", 1) | 
|         comp_method = ord(v[0]) | 
|         if comp_method != 0: | 
|             raise SyntaxError("Unknown compression method %s in zTXt chunk" % comp_method) | 
|         import zlib | 
|         self.im_info[k] = self.im_text[k] = zlib.decompress(v[1:]) | 
|         return s | 
|   | 
| # -------------------------------------------------------------------- | 
| # PNG reader | 
|   | 
| def _accept(prefix): | 
|     return prefix[:8] == _MAGIC | 
|   | 
| ## | 
| # Image plugin for PNG images. | 
|   | 
| class PngImageFile(ImageFile.ImageFile): | 
|   | 
|     format = "PNG" | 
|     format_description = "Portable network graphics" | 
|   | 
|     def _open(self): | 
|   | 
|         if self.fp.read(8) != _MAGIC: | 
|             raise SyntaxError, "not a PNG file" | 
|   | 
|         # | 
|         # Parse headers up to the first IDAT chunk | 
|   | 
|         self.png = PngStream(self.fp) | 
|   | 
|         while 1: | 
|   | 
|             # | 
|             # get next chunk | 
|   | 
|             cid, pos, len = self.png.read() | 
|   | 
|             try: | 
|                 s = self.png.call(cid, pos, len) | 
|             except EOFError: | 
|                 break | 
|             except AttributeError: | 
|                 if Image.DEBUG: | 
|                     print cid, pos, len, "(unknown)" | 
|                 s = ImageFile._safe_read(self.fp, len) | 
|   | 
|             self.png.crc(cid, s) | 
|   | 
|         # | 
|         # Copy relevant attributes from the PngStream.  An alternative | 
|         # would be to let the PngStream class modify these attributes | 
|         # directly, but that introduces circular references which are | 
|         # difficult to break if things go wrong in the decoder... | 
|         # (believe me, I've tried ;-) | 
|   | 
|         self.mode = self.png.im_mode | 
|         self.size = self.png.im_size | 
|         self.info = self.png.im_info | 
|         self.text = self.png.im_text # experimental | 
|         self.tile = self.png.im_tile | 
|   | 
|         if self.png.im_palette: | 
|             rawmode, data = self.png.im_palette | 
|             self.palette = ImagePalette.raw(rawmode, data) | 
|   | 
|         self.__idat = len # used by load_read() | 
|   | 
|   | 
|     def verify(self): | 
|         "Verify PNG file" | 
|   | 
|         if self.fp is None: | 
|             raise RuntimeError("verify must be called directly after open") | 
|   | 
|         # back up to beginning of IDAT block | 
|         self.fp.seek(self.tile[0][2] - 8) | 
|   | 
|         self.png.verify() | 
|         self.png.close() | 
|   | 
|         self.fp = None | 
|   | 
|     def load_prepare(self): | 
|         "internal: prepare to read PNG file" | 
|   | 
|         if self.info.get("interlace"): | 
|             self.decoderconfig = self.decoderconfig + (1,) | 
|   | 
|         ImageFile.ImageFile.load_prepare(self) | 
|   | 
|     def load_read(self, bytes): | 
|         "internal: read more image data" | 
|   | 
|         while self.__idat == 0: | 
|             # end of chunk, skip forward to next one | 
|   | 
|             self.fp.read(4) # CRC | 
|   | 
|             cid, pos, len = self.png.read() | 
|   | 
|             if cid not in ["IDAT", "DDAT"]: | 
|                 self.png.push(cid, pos, len) | 
|                 return "" | 
|   | 
|             self.__idat = len # empty chunks are allowed | 
|   | 
|         # read more data from this chunk | 
|         if bytes <= 0: | 
|             bytes = self.__idat | 
|         else: | 
|             bytes = min(bytes, self.__idat) | 
|   | 
|         self.__idat = self.__idat - bytes | 
|   | 
|         return self.fp.read(bytes) | 
|   | 
|   | 
|     def load_end(self): | 
|         "internal: finished reading image data" | 
|   | 
|         self.png.close() | 
|         self.png = None | 
|   | 
|   | 
| # -------------------------------------------------------------------- | 
| # PNG writer | 
|   | 
| def o16(i): | 
|     return chr(i>>8&255) + chr(i&255) | 
|   | 
| def o32(i): | 
|     return chr(i>>24&255) + chr(i>>16&255) + chr(i>>8&255) + chr(i&255) | 
|   | 
| _OUTMODES = { | 
|     # supported PIL modes, and corresponding rawmodes/bits/color combinations | 
|     "1":   ("1", chr(1)+chr(0)), | 
|     "L;1": ("L;1", chr(1)+chr(0)), | 
|     "L;2": ("L;2", chr(2)+chr(0)), | 
|     "L;4": ("L;4", chr(4)+chr(0)), | 
|     "L":   ("L", chr(8)+chr(0)), | 
|     "LA":  ("LA", chr(8)+chr(4)), | 
|     "I":   ("I;16B", chr(16)+chr(0)), | 
|     "P;1": ("P;1", chr(1)+chr(3)), | 
|     "P;2": ("P;2", chr(2)+chr(3)), | 
|     "P;4": ("P;4", chr(4)+chr(3)), | 
|     "P":   ("P", chr(8)+chr(3)), | 
|     "RGB": ("RGB", chr(8)+chr(2)), | 
|     "RGBA":("RGBA", chr(8)+chr(6)), | 
| } | 
|   | 
| def putchunk(fp, cid, *data): | 
|     "Write a PNG chunk (including CRC field)" | 
|   | 
|     data = string.join(data, "") | 
|   | 
|     fp.write(o32(len(data)) + cid) | 
|     fp.write(data) | 
|     hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) | 
|     fp.write(o16(hi) + o16(lo)) | 
|   | 
| class _idat: | 
|     # wrap output from the encoder in IDAT chunks | 
|   | 
|     def __init__(self, fp, chunk): | 
|         self.fp = fp | 
|         self.chunk = chunk | 
|     def write(self, data): | 
|         self.chunk(self.fp, "IDAT", data) | 
|   | 
| def _save(im, fp, filename, chunk=putchunk, check=0): | 
|     # save an image to disk (called by the save method) | 
|   | 
|     mode = im.mode | 
|   | 
|     if mode == "P": | 
|   | 
|         # | 
|         # attempt to minimize storage requirements for palette images | 
|   | 
|         if im.encoderinfo.has_key("bits"): | 
|   | 
|             # number of bits specified by user | 
|             n = 1 << im.encoderinfo["bits"] | 
|   | 
|         else: | 
|   | 
|             # check palette contents | 
|             n = 256 # FIXME | 
|   | 
|         if n <= 2: | 
|             bits = 1 | 
|         elif n <= 4: | 
|             bits = 2 | 
|         elif n <= 16: | 
|             bits = 4 | 
|         else: | 
|             bits = 8 | 
|   | 
|         if bits != 8: | 
|             mode = "%s;%d" % (mode, bits) | 
|   | 
|     # encoder options | 
|     if im.encoderinfo.has_key("dictionary"): | 
|         dictionary = im.encoderinfo["dictionary"] | 
|     else: | 
|         dictionary = "" | 
|   | 
|     im.encoderconfig = (im.encoderinfo.has_key("optimize"), dictionary) | 
|   | 
|     # get the corresponding PNG mode | 
|     try: | 
|         rawmode, mode = _OUTMODES[mode] | 
|     except KeyError: | 
|         raise IOError, "cannot write mode %s as PNG" % mode | 
|   | 
|     if check: | 
|         return check | 
|   | 
|     # | 
|     # write minimal PNG file | 
|   | 
|     fp.write(_MAGIC) | 
|   | 
|     chunk(fp, "IHDR", | 
|           o32(im.size[0]), o32(im.size[1]),     #  0: size | 
|           mode,                                 #  8: depth/type | 
|           chr(0),                               # 10: compression | 
|           chr(0),                               # 11: filter category | 
|           chr(0))                               # 12: interlace flag | 
|   | 
|     if im.mode == "P": | 
|         chunk(fp, "PLTE", im.im.getpalette("RGB")) | 
|   | 
|     if im.encoderinfo.has_key("transparency"): | 
|         if im.mode == "P": | 
|             transparency = max(0, min(255, im.encoderinfo["transparency"])) | 
|             chunk(fp, "tRNS", chr(255) * transparency + chr(0)) | 
|         elif im.mode == "L": | 
|             transparency = max(0, min(65535, im.encoderinfo["transparency"])) | 
|             chunk(fp, "tRNS", o16(transparency)) | 
|         elif im.mode == "RGB": | 
|             red, green, blue = im.encoderinfo["transparency"] | 
|             chunk(fp, "tRNS", o16(red) + o16(green) + o16(blue)) | 
|         else: | 
|             raise IOError("cannot use transparency for this mode") | 
|   | 
|     if 0: | 
|         # FIXME: to be supported some day | 
|         chunk(fp, "gAMA", o32(int(gamma * 100000.0))) | 
|   | 
|     dpi = im.encoderinfo.get("dpi") | 
|     if dpi: | 
|         chunk(fp, "pHYs", | 
|               o32(int(dpi[0] / 0.0254 + 0.5)), | 
|               o32(int(dpi[1] / 0.0254 + 0.5)), | 
|               chr(1)) | 
|   | 
|     info = im.encoderinfo.get("pnginfo") | 
|     if info: | 
|         for cid, data in info.chunks: | 
|             chunk(fp, cid, data) | 
|   | 
|     # ICC profile writing support -- 2008-06-06 Florian Hoech | 
|     if im.info.has_key("icc_profile"): | 
|         # ICC profile | 
|         # according to PNG spec, the iCCP chunk contains: | 
|         # Profile name  1-79 bytes (character string) | 
|         # Null separator        1 byte (null character) | 
|         # Compression method    1 byte (0) | 
|         # Compressed profile    n bytes (zlib with deflate compression) | 
|         try: | 
|             import ICCProfile | 
|             p = ICCProfile.ICCProfile(im.info["icc_profile"]) | 
|             name = p.tags.desc.get("ASCII", p.tags.desc.get("Unicode", p.tags.desc.get("Macintosh", p.tags.desc.get("en", {}).get("US", "ICC Profile")))).encode("latin1", "replace")[:79] | 
|         except ImportError: | 
|             name = "ICC Profile" | 
|         data = name + "\0\0" + zlib.compress(im.info["icc_profile"]) | 
|         chunk(fp, "iCCP", data) | 
|   | 
|     ImageFile._save(im, _idat(fp, chunk), [("zip", (0,0)+im.size, 0, rawmode)]) | 
|   | 
|     chunk(fp, "IEND", "") | 
|   | 
|     try: | 
|         fp.flush() | 
|     except: | 
|         pass | 
|   | 
|   | 
| # -------------------------------------------------------------------- | 
| # PNG chunk converter | 
|   | 
| def getchunks(im, **params): | 
|     """Return a list of PNG chunks representing this image.""" | 
|   | 
|     class collector: | 
|         data = [] | 
|         def write(self, data): | 
|             pass | 
|         def append(self, chunk): | 
|             self.data.append(chunk) | 
|   | 
|     def append(fp, cid, *data): | 
|         data = string.join(data, "") | 
|         hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) | 
|         crc = o16(hi) + o16(lo) | 
|         fp.append((cid, data, crc)) | 
|   | 
|     fp = collector() | 
|   | 
|     try: | 
|         im.encoderinfo = params | 
|         _save(im, fp, None, append) | 
|     finally: | 
|         del im.encoderinfo | 
|   | 
|     return fp.data | 
|   | 
|   | 
| # -------------------------------------------------------------------- | 
| # Registry | 
|   | 
| Image.register_open("PNG", PngImageFile, _accept) | 
| Image.register_save("PNG", _save) | 
|   | 
| Image.register_extension("PNG", ".png") | 
|   | 
| Image.register_mime("PNG", "image/png") |