3
votes

I'm working on a solution to my other question which is reading the data in the 'zTXt' chunks of a PNG. I am as far as locating the chunks in the file, and reading the zTXt's keyword. I'm having trouble reading the compressed portion of zTXt. I've never worked with the DeflateStream object before, and am having some trouble with it. When reading, it appears to expect the length parameter to be in 'uncompressed' bytes. In my case however, I only know the length of the data in 'compressed' bytes. To hopefully get around this, I put all the data that needed to be decompressed into a MemoryStream, and then 'read to end' with a DeflateStream. Now that's just peachy, except it throws an InvalidDataException with the message "Block length does not match with its complement." Now I have no idea what this means. What could be going wrong?

The format of a chunk is 4 bytes for the ID ("zTXt"), a big-endian 32-bit int for the data length, the data, and finally a CRC32 checksum which I am ignoring for now.

The format of the zTXt chunk is first a null-terminated (string as a keyword), then one byte for the compression method (always 0, the DEFLATE method), with the rest of the data being compressed text.

My method takes in a fresh FileStream, and returns a dictionary with the zTXt keywords and data.

Here is the monster now:

public static List<KeyValuePair<string, string>> GetZtxt(FileStream stream)
{
    var ret = new List<KeyValuePair<string, string>>();
    try {
        stream.Position = 0;
        var br = new BinaryReader(stream, Encoding.ASCII);
        var head = br.ReadBytes(8); // The header is the same for all PNGs.
        if (!head.SequenceEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A })) return null; // Not a PNG.
        while (stream.Position < stream.Length) {
            int len; // Length of chunk data.
            if (BitConverter.IsLittleEndian)
                len = BitConverter.ToInt32(br.ReadBytes(4).Reverse().ToArray(), 0);
            else
                len = br.ReadInt32();

            char[] cName = br.ReadChars(4); // The chunk type.
            if (cName.SequenceEqual(new[] { 'z', 'T', 'X', 't' })) {
                var sb = new StringBuilder(); // Builds the null-terminated keyword associated with the chunk.
                char c = br.ReadChar();
                do {
                    sb.Append(c);
                    c = br.ReadChar();
                }
                while (c != '\0');
                byte method = br.ReadByte(); // The compression method.  Should always be 0. (DEFLATE method.)
                if (method != 0) {
                    stream.Seek(len - sb.Length + 3, SeekOrigin.Current); // If not 0, skip the rest of the chunk.
                    continue;
                }
                var data = br.ReadBytes(len - sb.Length - 1); // Rest of the chunk data...
                var ms = new MemoryStream(data, 0, data.Length); // ...in a MemoryStream...
                var ds = new DeflateStream(ms, CompressionMode.Decompress); // ...read by a DeflateStream...
                var sr = new StreamReader(ds); // ... and a StreamReader.  Yeesh.
                var str = sr.ReadToEnd(); // !!! InvalidDataException !!!
                ret.Add(new KeyValuePair<string, string>(sb.ToString(), str));
                stream.Seek(4, SeekOrigin.Current); // Skip the CRC check.
            }
            else {
                stream.Seek(len + 4, SeekOrigin.Current); // Skip the rest of the chunk.
            }
        }
    }
    catch (IOException) { }
    catch (InvalidDataException) { }
    catch (ArgumentOutOfRangeException) { }
    return ret;
}

Once this is tackled, I'll need to write a function that ADDS these zTXt chunks to the file. So hopefully I'll understand how the DeflateStream works once this is solved.

Thanks, much!!

1

1 Answers

7
votes

After all this time, I've finally found the problem. The data is in zlib format, which has a bit more data stored than just using DEFLATE alone. The file is read properly if I just read the 2 extra bytes in right before I get the compressed data.

See this feedback page. (I did not submit that one.)

I'm wondering now. The value of those two bytes are 0x78 and 0x9C respectively. If I find values other than those, should I assume the DEFLATE is going to fail?