Search code examples
pythonpython-3.xpng

Migrate script in python 2 to python 3.x


Following this past question: How Can I Make This Python Script Work With Python 3?

I would like to make this script work in python 3. I've managed to fix some details but it still doesn't work.. now it seems throwing a error when decompressing the chunk data.

Here is the current error I have: Error -5 while decompressing data: incomplete or truncated stream

I'm using Python 3.6.2

And here is the script with things already migrated to Python 3. The script basically normalizes a PNG with custom iphone format.

import pdb
from struct import *
from zlib import *
import stat
import sys
import os
import zlib

def getNormalizedPNG(filename):
    pngheader = b"\x89PNG\r\n\x1a\n"

    pdb.set_trace()

    file = open(filename, "rb")
    oldPNG = file.read()
    file.close()

    if oldPNG[:8] != pngheader:
        return None

    newPNG = oldPNG[:8]

    chunkPos = len(newPNG)

    # For each chunk in the PNG file
    while chunkPos < len(oldPNG):

        # Reading chunk
        chunkLength = oldPNG[chunkPos:chunkPos+4]
        chunkLength = unpack(">L", chunkLength)[0]
        chunkType = oldPNG[chunkPos+4 : chunkPos+8]
        chunkData = oldPNG[chunkPos+8:chunkPos+8+chunkLength]
        chunkCRC = oldPNG[chunkPos+chunkLength+8:chunkPos+chunkLength+12]
        chunkCRC = unpack(">L", chunkCRC)[0]
        chunkPos += chunkLength + 12

        # Parsing the header chunk
        if chunkType == b"IHDR":
            width = unpack(">L", chunkData[0:4])[0]
            height = unpack(">L", chunkData[4:8])[0]

        # Parsing the image chunk
        if chunkType == b"IDAT":
            try:
                pdb.set_trace()
                # Uncompressing the image chunk
                bufSize = width * height * 4 + height

                chunkData = decompress(chunkData, -8, bufSize)

            except Exception as e:
                print("Already normalized")
                print(e)
                # The PNG image is normalized
                return None

            # Swapping red & blue bytes for each pixel
            newdata = b""
            for y in range(height):
                i = len(newdata)
                newdata += chunkData[i]
                for x in range(width):
                    i = len(newdata)
                    newdata += chunkData[i+2]
                    newdata += chunkData[i+1]
                    newdata += chunkData[i+0]
                    newdata += chunkData[i+3]

            # Compressing the image chunk
            chunkData = newdata
            chunkData = compress( chunkData )
            chunkLength = len( chunkData )
            chunkCRC = crc32(chunkType)
            chunkCRC = crc32(chunkData, chunkCRC)
            chunkCRC = (chunkCRC + 0x100000000) % 0x100000000

        # Removing CgBI chunk
        if chunkType != b"CgBI":
            newPNG += pack(">L", chunkLength)
            newPNG += chunkType
            if chunkLength > 0:
                newPNG += chunkData
            newPNG += pack(">L", chunkCRC)

        # Stopping the PNG file parsing
        if chunkType == b"IEND":
            break

    return newPNG


def updatePNG(filename):
    data = getNormalizedPNG(filename)

    if data != None:
        file = open(filename, "wb")
        file.write(data)
        file.close()
        return True
    return data

Any clue will be appreciated. Thanks! :)


Solution

  • The original code does not process multiple IDAT chunks right away; it does the right thing™ and only concatenates them into a single large object before decompressing it as a whole. IDAT chunks are not separately compressed, but your code assumes they do – and so it fails when there is more than one.

    There may be multiple IDAT chunks; if so, they shall appear consecutively with no other intervening chunks. The compressed datastream is then the concatenation of the contents of the data fields of all the IDAT chunks.
    11.2.4 IDAT Image data

    Re-wiring your loop to first gather all IDATs fixes things. Only when an IEND chunk is found, this data is decompressed, bytes are swapped, and a new IDAT chunk gets created. The final step, appending an IEND, closes the file.

    from struct import *
    from zlib import *
    import stat
    import sys
    import os
    import zlib
    
    def getNormalizedPNG(filename):
        pngheader = b"\x89PNG\r\n\x1a\n"
    
        file = open(filename, "rb")
        oldPNG = file.read()
        file.close()
    
        if oldPNG[:8] != pngheader:
            return None
    
        newPNG = oldPNG[:8]
    
        chunkPos = len(newPNG)
        chunkD = bytearray()
    
        foundCGBi = False
    
        # For each chunk in the PNG file
        while chunkPos < len(oldPNG):
    
            # Reading chunk
            chunkLength = oldPNG[chunkPos:chunkPos+4]
            chunkLength = unpack(">L", chunkLength)[0]
            chunkType = oldPNG[chunkPos+4 : chunkPos+8]
            chunkData = oldPNG[chunkPos+8:chunkPos+8+chunkLength]
            chunkCRC = oldPNG[chunkPos+chunkLength+8:chunkPos+chunkLength+12]
            chunkCRC = unpack(">L", chunkCRC)[0]
            chunkPos += chunkLength + 12
    
            # Parsing the header chunk
            if chunkType == b"IHDR":
                width = unpack(">L", chunkData[0:4])[0]
                height = unpack(">L", chunkData[4:8])[0]
    
            # Parsing the image chunk
            if chunkType == b"IDAT":
                # Concatename all image data chunks
                chunkD += chunkData
                continue
    
            # Stopping the PNG file parsing
            if chunkType == b"IEND":
                if not foundCGBi:
                    print ('Already normalized')
                    return None
    
                bufSize = width * height * 4 + height
                chunkData = decompress(chunkD, -8, bufSize)
    
                # Swapping red & blue bytes for each pixel
                chunkData = bytearray(chunkData)
                offset = 1
                for y in range(height):
                    for x in range(width):
                        chunkData[offset+4*x],chunkData[offset+4*x+2] = chunkData[offset+4*x+2],chunkData[offset+4*x]
                    offset += 1+4*width
    
                # Compressing the image chunk
                #chunkData = newdata
                chunkData = compress( chunkData )
                chunkLength = len( chunkData )
                chunkCRC = crc32(b'IDAT')
                chunkCRC = crc32(chunkData, chunkCRC)
                chunkCRC = (chunkCRC + 0x100000000) % 0x100000000
    
                newPNG += pack(">L", chunkLength)
                newPNG += b'IDAT'
                newPNG += chunkData
                newPNG += pack(">L", chunkCRC)
    
                chunkCRC = crc32(chunkType)
                newPNG += pack(">L", 0)
                newPNG += b'IEND'
                newPNG += pack(">L", chunkCRC)
                break
    
            # Removing CgBI chunk
            if chunkType == b"CgBI":
                foundCGBi = True
            else:
                newPNG += pack(">L", chunkLength)
                newPNG += chunkType
                if chunkLength > 0:
                    newPNG += chunkData
                newPNG += pack(">L", chunkCRC)
    
        return newPNG
    
    
    def updatePNG(filename):
        data = getNormalizedPNG(filename)
    
        if data != None:
            file = open(filename+'_fixed.png', "wb")
            file.write(data)
            file.close()
            return True
        return data
    
    updatePNG("broken_image.png")
    

    which results in a valid fixed file.

    This code does not restore the broken CgBI alpha channel! If you need proper alpha transparency, you need to apply the row filters to get straight-up RGB values, invert the alpha, and then apply the inverse of the row filters before compressing again.

    You could use the Python wrapper for PNGDefry, which is a C program that indeed performs these missing steps.

    Disclaimer: I am the writer of PNGdefry.