0
votes

my PNG 10x10 picture for the test

Hi everybody, I try to make a Python script that reads/writes a PNG file. I don't need a full option script. In this test, no compression, no filter, no interlace, I use a RGB palette and an Alpha palette (Color Type 3) with a 8 Bit Depth.

I simply don't understand the IDAT chunk... I was expecting a list of index colors like: 10px Width x 10px Height x 8 Bit Depth -> 100 Bytes of data in the IDAT but instead I have 206 Bytes. (Please correct me if I'm wrong) And a range of 0 to 66 for the index colors but it's completely out of range.

If someone can explain me how I'm supposed to read this data or what i did wrong, I'll appreciate it.

this is my code (line 50 for the IDAT):

#!/usr/bin/env python3
with open("smile.png", 'rb') as f:
    hexData = f.read().hex()

#Init cursor_0
cursor_0 = 0

#check signature (8 bytes)
start = cursor_0
stop = cursor_0+(8*2)
cursor_0 = stop
if hexData[start:stop] != "89504e470d0a1a0a":
    print("signature fail")

#Read each Chunk
read = True
while read:
    #NEW CHUNK

    #read length of the chunk (4 bytes)
    start = cursor_0
    stop = cursor_0+(4*2)
    cursor_0 = stop
    chunkDataLength = int(hexData[start:stop],16)

    #read type of the chunk (4 bytes)
    start = cursor_0
    stop = cursor_0+(4*2)
    cursor_0 = stop
    chunkTypeHex = hexData[start:stop]
    chunkType = bytes.fromhex(hexData[start:stop]).decode()

    #read the data of the chunk (variable)
    start = cursor_0
    stop = cursor_0+(chunkDataLength*2)
    cursor_0 = stop
    chunkDataHex = hexData[start:stop]

    #read the CRC of the chunk (4 bytes)
    start = cursor_0
    stop = cursor_0+(4*2)
    cursor_0 = stop
    chunkCrcHex = hexData[start:stop]

    #Decode

    #Init cursor_1
    cursor_1 = 0

    if chunkType == "IHDR":
        print(chunkType)

        #check the pDataLength
        if chunkDataLength != 13:
            print("unexpected pDataLength: "+ chunkDataLength)

        #Width (4 bytes)
        start = cursor_1
        stop = cursor_1+(4*2)
        cursor_1 = stop
        width = int(chunkDataHex[start:stop])
        print("Width: "+str(width))

        #Height (4 bytes)
        start = cursor_1
        stop = cursor_1+(4*2)
        cursor_1 = stop
        height = int(chunkDataHex[start:stop])
        print("Height: "+str(height))

        #Bit Depth (1 byte)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        bitDepth = int(chunkDataHex[start:stop])
        print("Bit Depth: "+str(bitDepth))

        #Color Type (1 byte)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        colorType = int(chunkDataHex[start:stop])
        print("ColorType: "+str(colorType))

        #Compression Method (1 byte)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        compressionMethod = int(chunkDataHex[start:stop])
        print("Compression Method: "+str(compressionMethod))

        #Filter Method (1 byte)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        filterMethod = int(chunkDataHex[start:stop])
        print("Filter Method: "+str(filterMethod))

        #Interlace Method (1 byte)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        interlaceMethod = int(chunkDataHex[start:stop])
        print("Interlace Method: "+str(interlaceMethod))

    elif chunkType == "PLTE":
        print(chunkType)

        print(str(int(chunkDataLength/3)) + " Colors")

        while cursor_1 < chunkDataLength*2:
            #RED (1 byte)
            start = cursor_1
            stop = cursor_1+(1*2)
            cursor_1 = stop
            red = chunkDataHex[start:stop]

            #GREEN (1 byte)
            start = cursor_1
            stop = cursor_1+(1*2)
            cursor_1 = stop
            green = chunkDataHex[start:stop]

            #BLUE (1 byte)
            start = cursor_1
            stop = cursor_1+(1*2)
            cursor_1 = stop
            blue = chunkDataHex[start:stop]

            color = red+green+blue
            #print("Color: "+ color)

    elif chunkType == "tRNS":
        print(chunkType)

        print(str(int(chunkDataLength)) + " Transparent Colors")

        while cursor_1 < chunkDataLength*2:
            #Transparent Color (1 byte)
            start = cursor_1
            stop = cursor_1+(1*2)
            cursor_1 = stop
            transparent = chunkDataHex[start:stop]

            #print("Transparent Color: "+ transparent)

    elif chunkType == "IDAT":
        print(chunkType)

        #>>>1ST TRY
        while cursor_1 < chunkDataLength*bitDepth/8*2:
            start = int(cursor_1)
            stop = int(cursor_1 + bitDepth/8*2)
            cursor_1 = stop
            colorIndex = int(chunkDataHex[start:stop],16)
            print("ColorIndex: "+str(colorIndex))


        #>>>2ND TRY
        #translate Hexadecimal to Binary
        chunkDataBin = bin(int(chunkDataHex,16))
        #print("len(chunkDataBin)/8="+str(len(chunkDataBin)/8))
        #print("chunkDataLength="+str(chunkDataLength))

        #start at 2 for jumping the 0b prefixe
        cursor_1 = 2

        while cursor_1 < chunkDataLength*bitDepth:
            start = cursor_1
            stop = cursor_1 + bitDepth
            cursor_1 = stop
            colorIndex = int(chunkDataBin[start:stop],2)
            #print("ColorIndex: "+str(colorIndex))

    elif chunkType == "IEND":
        print(chunkType)
        #If END OF FILE detected, break the loop
        read = False

    else:
        print("PyPng script can't handle " + chunkType + " chunk type")
2

2 Answers

0
votes

Ok, my bad... i think i forgot the zlib compression... This block look like better, but i sill have 273 pixels insted of 100...

   elif chunkType == "IDAT":
        print(chunkType)

        chunkDataBin=zlib.decompress(bytes.fromhex(chunkDataHex))

        while cursor_1 < len(chunkDataBin):         
            colorIndex= chunkDataBin[cursor_1]
            cursor_1 += 1
            print("colorIndex: "+str(colorIndex))

So, the data I have are not out of range anymore. Each line look like begin with a 0 index.

This picture have 16px Width on 16px Height (not 10px Width on 10px Height like I said).

I was still working with a hexadecimal number instead of a decimal one:

        #Width (4 octets)
        start = cursor_1
        stop = cursor_1+(4*2)
        cursor_1 = stop
        width = int(chunkDataHex[start:stop],16)
        print("Width: "+str(width))

        #Height (4 octets)
        start = cursor_1
        stop = cursor_1+(4*2)
        cursor_1 = stop
        height = int(chunkDataHex[start:stop],16)
        print("Height: "+str(height))

        #Bit Depth (1 octets)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        bitDepth = int(chunkDataHex[start:stop],16)
        print("Bit Depth: "+str(bitDepth))

        #Color Type (1 octets)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        colorType = int(chunkDataHex[start:stop],16)
        print("ColorType: "+str(colorType))

        #Compression Method (1 octets)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        compressionMethod = int(chunkDataHex[start:stop],16)
        print("Compression Method: "+str(compressionMethod))

        #Filter Method (1 octets)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        filterMethod = int(chunkDataHex[start:stop],16)
        print("Filter Method: "+str(filterMethod))

        #Interlace Method (1 octets)
        start = cursor_1
        stop = cursor_1+(1*2)
        cursor_1 = stop
        interlaceMethod = int(chunkDataHex[start:stop],16)
        print("Interlace Method: "+str(interlaceMethod))
0
votes

If you plan to 'manually' extract PNG data, it's a good idea to keep your browser open at the official specifications. The pixel data of PNG images is compressed, as explained in 10. Compression. After decompressing, you get a stream of binary data consisting of height runs, each one with filter_type + width × pixel_data (where width and height are straightforward defined in the IHDR chunk, and the exact format of pixel_data – bit depth and alpha – needs to be derived from the Colour Type flag in IHDR.

So step 1 is to use zlib to decompress the binary chunk. After decompressing, you end up with 16 × (1 + 16) = 272 bytes.

The next step is to iterate over the rows and apply the filter method of each row, to (finally!) end up with a list of actual values in the format defined by the Colour Type.

Each of the filter types uses the previously decoded row as input (row number -1 is considered to be all zeroes), on which a function is applied with the new bytes. Fortunately, your sample image only uses Row Filter #0: None, which is the easiest to implement as it only replaces the old value with the new one. For a more complete PNG decoder, you will need to implement all 4 filters.

This leads to the following:

elif chunkType == "IDAT":
    print(chunkType)

    chunkDataBin=zlib.decompress(bytes.fromhex(chunkDataHex))
    print (len(chunkDataBin))

    # create the initial (empty) row
    processRow = [0] * width
    for y in range(height):
        rowFilter = chunkDataBin[y*(width+1)]
        print ("Row Filter: %d; pixel data " % rowFilter, end='')
        for x in range(width):
            colorIndex = chunkDataBin[y*(width+1)+1+x]
            # process filters

            # Filter: None
            if rowFilter == 0:
                processRow[x] = colorIndex
            else:
                # raise an error for not implemented filters
                raise ValueError("Filter type %d is not implemented" % rowFilter)
        for x in range(width):
            print("%02X " % processRow[x], end='')
        print ()

Processing the IDAT chunk as soon as you come across it works for your test file, but according to the specifications, there can be multiple IDAT chunks. The proper way to handle this is to keep a global object to which you concatenate all IDAT chunks you encounter (they should be consecutive as well). Only after you concatenated all of them into a single stream, you can use zlib to decompress this.