简体   繁体   中英

How can I manually read PNG files in C++?

Portable Network Graphics Overview

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.


The Problem

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.


Some of My (Old) Code

#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

Some of My New Code

#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 );  // <-- 
data[ size ] = '\0';      // <-- 

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.

If you want to know how much data you read from the file then just use tellg() again.

data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size );
data[ size ] = '\0';
if(file.good()) // make sure we had a good read.
    std::cout << "Data size: " << file.tellg() - 8 << std::endl;

There is an error in you code with reading the data as well. You are reading to size where size is the size of the file which is 8 bytes more than you need since you are skipping the header. The correct code is

const char* INPUT_FILENAME = "ban hammer.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);
    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 - 8);
    data[size] = '\0';
    std::cout << "Data size: " << file.tellg() << std::endl;
    cin.get();
    return 0;
}

Solution 1:

file.read( data, size );
Size_t data_size = file.tellg() - 8;
std::cout << "Data size: " << data_size << std::endl;

Even easier: Solution 2:

Size_t data_size = file.readsome( data, size );
std::cout << "Data size: " << data_size << std::endl;

file.readsome() returns the number of bytes read.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM