The general layout of any given PNG file looks like this:
File Header: An 8-byte signature.
Chunks: Chunks of data ranging from image properties to the actual image itself.
I want to read PNG files in C++ without using any external libraries. I want to do this to gain a deeper understanding of both PNG format and the C++ programming language.
I started off using fstream
to read images byte-by-byte, but I can't get past the header of any PNG file. I try using read( char*, int )
to put the bytes into char
arrays, but read
fails on every byte after the header.
As seen above, I think my program always gets caught up on the end-of-file 1A
byte. I'm developing on Windows 7 for Windows 7 and Linux machines.
#include <iostream>
#include <fstream>
#include <cstring>
#include <cstddef>
const char* INPUT_FILENAME = "image.png";
int main()
{
std::ifstream file;
size_t size = 0;
std::cout << "Attempting to open " << INPUT_FILENAME << std::endl;
file.open( INPUT_FILENAME, std::ios::in | std::ios::binary | std::ios::ate );
char* data = 0;
file.seekg( 0, std::ios::end );
size = file.tellg();
std::cout << "File size: " << size << std::endl;
file.seekg( 0, std::ios::beg );
data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size );
data[ size ] = '\0';
std::cout << "Data size: " << std::strlen( data ) << std::endl;
}
The output is always similar to this:
Attempting to open image.png
File size: 1768222
Data size: 0
The file size is correct, but data size is clearly incorrect. Note that I try to skip the header (avoid the end-of-file character) and also account for this when declaring the size of char* data
.
Here are some data size values when I modify the file.seekg( ... );
line accordingly:
file.seekg( n ); data size
---------------- ---------
0 8
1 7
2 6
... ...
8 0
9 0
10 0
#include <iostream>
#include <fstream>
#include <cstring>
#include <cstddef>
const char* INPUT_FILENAME = "image.png";
int main()
{
std::ifstream file;
size_t size = 0;
std::cout << "Attempting to open " << INPUT_FILENAME << std::endl;
file.open( INPUT_FILENAME, std::ios::in | std::ios::binary | std::ios::ate );
char* data = 0;
file.seekg( 0, std::ios::end );
size = file.tellg();
std::cout << "File size: " << size << std::endl;
file.seekg( 0, std::ios::beg );
data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size );
data[ size ] = '\0';
std::cout << "Data size: " << ((unsigned long long)file.tellg() - 8) << std::endl;
}
I essentially just modified the Data size:
line. A thing to note is the output of the Data size:
line is always really close to the maximum value of whatever type
I cast file.tellg()
to.
Your (new) code contains two essential errors:
data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size ); // <-- here
data[ size ] = '\0'; // <-- and here
First off, you want to read the data without the 8 byte prefix, and you allocate the right amount of space (not really, see further). But at that point, size
still holds the total amount of bytes of the file, including the 8 byte prefix. Since you ask to read size
bytes and there are only size-8
bytes remaining, the file.read
operation fails. You don't check for errors and so you do not notice file
is invalidated at that point. With an error check you should have seen this:
if (file)
std::cout << "all characters read successfully.";
else
std::cout << "error: only " << file.gcount() << " could be read";
Because file
is invalid from that point on, all operations such as your later file.tellg()
return -1
.
The second error is data[size] = '\0'
. Your buffer is not that large; it should be data[size-8] = 0;
. Currently, you are writing into memory beyond what you allocated earlier, which causes Undefined Behavior and may lead to problems later on.
But that last operation clearly shows you are thinking in terms of character strings. A PNG file is not a string, it is a binary stream of data. Allocating +1
for its size and setting this value to 0
(with the unnecessary "character-wise" way of thinking, with '\0'
) is only useful if the input file is of a string type – say, a plain text file.
A simple fix for your current issues is this (well, and add error checking for all your file operations):
file.read( data, size-8 );
However, I would strongly advise you to look at a much simpler file format first. The PNG file format is compact and well documented; but it is also versatile, complicated, and contains highly compressed data. For a beginner it is way too hard.
Start with an easier image format. ppm
is a deliberately simple format, good to start with. tga
, old but easy, introduces you to several more concepts such as bit depths and color mapping. Microsoft's bmp
has some nice little caveats but can still be considered 'beginner friendly'. If you are interested in simple compression, the basic Run Length Encoding of a pcx
is a good starting point. After mastering that you could look in to the gif
format, which uses the much harder LZW compression.
Only if you succeed in implementing parsers for these, you may want to look at PNG again.