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

16

可移植网络图形总览

任何给定的PNG文件的常规布局如下:

文件头: 8字节的签名。

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


问题

我想在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;
}

输出结果始终类似于这样:

Attempting to open image.png
File size: 1768222
Data size: 0

文件大小正确,但数据大小明显不正确。请注意,在声明 char* data 的大小时,我试图跳过标题(避免文件结尾字符),并在此考虑。

根据我相应修改 file.seekg(...); 代码行后,以下是一些数据大小值:

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;
}

我基本上只修改了Data size:这一行。需要注意的是,Data size:行的输出始终非常接近于我将file.tellg()转换为的任何type的最大值。


6
strlen 函数在遇到第一个空字符(null terminator)时停止计算,你假设添加到缓冲区结尾的空字符是唯一的。通常不应将二进制数据视为文本字符串。 - Captain Obvlious
我绝不是在暗示任何东西。PNG中存储的数据应被视为二进制数据,这意味着您永远不应该假定空终止符,并且使用 strlen 是正确的方法。您需要检查PNG文件格式并开始解释实际数据而不是假设它只是一堆字符串。 - Captain Obvlious
2
@user3745189 我想在C++中读取PNG文件,而不使用除STL之外的任何东西。你的代码没有使用STL中的任何内容。如果使用了STL,至少你需要将new[]替换为std::vector - PaulMcKenzie
@CaptainObvlious 噢,所以一个字节可以表示一些像素,但同时也可以是空终止符?不过,我应该能够将每个字节塞入一个字符中,对吧?我的意思是,并不是每个字节都是空终止符。 - user3745189
1
@user3745189 图像数据本身可能包含空字符。图像数据中的这些空字符与字符串终止无关,它们只是存在的数据。因此,您不应使用停止在空字符上的字符串函数。 - PaulMcKenzie
显示剩余6条评论
3个回答

9

你的(新)代码包含两个 必要 的错误:

data = new char[ size - 8 + 1 ];
file.seekg( 8 ); // skip the header
file.read( data, size );  // <-- here
data[ size ] = '\0';      // <-- and here
首先,您需要读取不带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";

因为从那时起,file是无效的,所有操作,如您后面的file.tellg()都返回-1
第二个错误是data[size] = '\0'。您的缓冲区不够大;它应该是data[size-8]=0;。目前,您正在写入比之前分配的内存更多的内存,这会导致未定义的行为并可能在以后导致问题。
但是最后一个操作显然表明您正在考虑“字符字符串”。PNG文件不是字符串,而是二进制数据流。为其大小分配+1并将此值设置为0(使用不必要的“逐个字符”思维方式,即'\0')仅对字符串类型的输入文件 - 如纯文本文件 - 有用。
解决当前问题的简单方法是这样的(好吧,并为所有文件操作添加错误检查):
file.read( data, size-8 );

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

从一个简单的图像格式开始。ppm 是一个故意简单的格式,适合初学者。tga,虽然有点老旧但很容易上手,可以让你了解更多的概念,例如位深度和颜色映射。微软的 bmp 有一些不错的小细节但仍然可以被认为是“初学者友好”的。如果您对简单压缩感兴趣,基本的 pcx 的运行长度编码是一个很好的起点。掌握了这个之后,您可以看看 gif 格式,它使用更难的 LZW 压缩。

只有在成功实现这些解析器之后,您才可能想再次查看 PNG。


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;

您的代码在读取数据时存在错误。您正在读取到 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;
}

我使用了稍微修改过的代码(((size_t)file.tellg() - 8)),这是输出结果:文件大小:1768222数据大小:4294967287。这让我觉得有些不寻常,因为数据大小比文件大小要大得多。看起来像是溢出。 - user3745189
@user3745189:这么大的数字最好以十六进制解释。你会看到它非常巨大,或者可能是(有符号的)一个小负数… -8 - Jongware
@user3745189,你的代码也有错误。我编辑了我的答案为可运行的代码。 - NathanOliver

0

解决方案 1:

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

更简单的方法:解决方案2:

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

file.readsome() 返回读取的字节数。


1
我不建议使用readsome(),因为http://en.cppreference.com/w/cpp/io/basic_istream/readsome说它可能不能总是做你想要的事情。 - NathanOliver
@NathanOliver。我从未遇到过这个问题,所以我不知道它。感谢您指出。您可以使用file.rdbuf()->sgetn(data, size)代替:http://en.cppreference.com/w/cpp/io/basic_streambuf/sgetn - cdonat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接