Reputation: 11
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")
Upvotes: 0
Views: 3432
Reputation: 22457
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.
Upvotes: 0
Reputation: 11
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))
Upvotes: 0