如何在C++中手动读取PNG文件?

use*_*189 11 c++ file-io png fstream ifstream

便携式网络图形概述

任何给定PNG文件的总体布局如下所示:

文件头:8字节签名.

Chunks:数据块,从图像属性到实际图像本身.


问题

我想用C++读取PNG文件而不使用任何外部库.我想这样做是为了更深入地理解PNG格式和C++编程语言.

我开始使用fstream逐字节读取图像,但我无法通过任何PNG文件的标头.我尝试使用read( char*, int )将字节放入char数组,但read在标头之后的每个字节都失败.

如上所示,我认为我的程序总是被文件结束1A字节所占用.我正在使用Windows 7开发Windows 7和Linux机器.


一些我的(旧)代码

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出总是类似于:

Attempting to open image.png
File size: 1768222
Data size: 0
Run Code Online (Sandbox Code Playgroud)

文件大小正确,但数据大小明显不正确.请注意,我尝试跳过标题(避免文件结尾字符),并在声明大小时也考虑到这一点char* data.

以下是相应修改file.seekg( ... );行时的一些数据大小值:

file.seekg( n );             data size
----------------             ---------
0                            8
1                            7
2                            6
...                          ...
8                            0
9                            0
10                           0
Run Code Online (Sandbox Code Playgroud)

我的一些新规范

#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;
}
Run Code Online (Sandbox Code Playgroud)

我基本上只修改了这Data size:条线.需要注意的是,该Data size:行的输出总是非常接近type我所投射的最大值file.tellg().

usr*_*301 8

您的(新)代码包含两个基本错误:

data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size );  // <-- here
data[ size ] = '\0';      // <-- and here
Run Code Online (Sandbox Code Playgroud)

首先,您想读取没有 8 字节前缀的数据,并分配适当的空间量(不是真的,请参阅进一步)。但此时,size仍然保存了文件的字节数,包括 8 字节前缀。由于您要求读取size字节并且只剩下size-8字节,因此file.read操作失败。您不会检查错误,因此您不会注意到此时file已失效。通过错误检查,您应该已经看到了:

if (file)
  std::cout << "all characters read successfully.";
else
  std::cout << "error: only " << file.gcount() << " could be read";
Run Code Online (Sandbox Code Playgroud)

因为file从那时起就无效了,所有诸如您以后的操作都file.tellg()返回-1

第二个错误是data[size] = '\0'。你的缓冲区没有那么大;应该是data[size-8] = 0;。目前,您正在写入超出您之前分配的内存的内存,这会导致未定义行为并可能导致以后出现问题。

但是最后一个操作清楚地表明您正在考虑字符串。PNG 文件不是字符串,而是二进制数据流。分配+1其大小并将此值设置为0(使用不必要的“字符”思维方式,使用'\0')仅当输入文件是字符串类型时才有用 - 例如,纯文本文件。

对当前问题的简单修复是这样的(好吧,并为所有文件操作添加错误检查):

file.read( data, size-8 );
Run Code Online (Sandbox Code Playgroud)

但是,我强烈建议您首先查看更简单的文件格式。PNG 文件格式紧凑且文档齐全;但它也是通用的、复杂的,并且包含高度压缩的数据。对于初学者来说太难了

从更简单的图像格式开始。ppm是一种刻意简单的格式,很好的开始。tga,古老而简单,向您介绍了更多的概念,例如位深度和颜色映射。微软bmp有一些很好的小警告,但仍然可以被认为是“初学者友好的”。如果您对简单压缩感兴趣,a 的基本运行长度编码pcx是一个很好的起点。掌握之后,您可以查看gif使用更难的 LZW 压缩的格式。

只有当您成功地为这些实现解析器时,您才可能想再次查看 PNG。


Nat*_*ica 1

如果您想知道从文件中读取了多少数据,则只需tellg()再次使用即可。

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;
Run Code Online (Sandbox Code Playgroud)

您读取数据的代码也存在错误。您正在读取的size文件size大小比您需要的多 8 个字节,因为您跳过了标头。正确的代码是

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;
}
Run Code Online (Sandbox Code Playgroud)