【问题标题】:How can I manually read PNG files in C++?如何在 C++ 中手动读取 PNG 文件?
【发布时间】:2015-09-13 19:22:18
【问题描述】:

便携式网络图形概述

任何给定 PNG 文件的一般布局如下所示:

文件头:一个 8 字节的签名。

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


问题

我想在不使用任何外部库的情况下读取 C++ 中的 PNG 文件。我想这样做是为了更深入地了解 PNG 格式和 C++ 编程语言。

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

如上所示,我认为我的程序总是被文件结尾1A 字节捕获。我正在为 Windows 7 和 Linux 机器在 Windows 7 上进行开发。


我的一些(旧)代码

#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 的最大值。

【问题讨论】:

  • strlen 在第一个空终止符处停止,您假设添加到缓冲区末尾的空终止符是唯一的。将二进制数据视为文本字符串通常不是一个好主意。
  • @CaptainObvlious 但是我怎样才能获得这些数据大小值呢?你说的有点道理,但似乎也暗示根据这些值,标题后面的每个值都是空终止符:file.seekg(0), data size: 8; file.seekg(1),数据大小:7; file.seekg(2),数据大小:6; ... file.seekg(8),数据大小:0; file.seekg(9),数据大小:0; file.seekg(10),数据大小:0; ...
  • 我并不是在暗示 。存储在 PNG 中的数据应被视为二进制数据,这意味着您永远不应假设空终止符,strlen 将是正确的方法。您需要检查 PNG 的文件格式并开始解释数据的实际情况,而不是假设它只是一堆字符串。
  • @user3745189 I want to read PNG files in C++ without using anything other than STL 您的代码没有使用 STL 中的任何内容。如果是这样,至少你会用std::vector替换new[]
  • @user3745189 图像数据本身可以有空字符。图像数据中的那些空字符与字符串终止无关——它只是那里的数据。因此,您不要使用在 null 处停止的字符串函数。

标签: c++ file-io png fstream ifstream


【解决方案1】:

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

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

首先,您要读取不带 8 字节前缀的数据,并分配适量的空间(不是真的,请参阅更多内容)。但此时,size 仍然保存文件的 total 字节数,包括 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。

【讨论】:

    【解决方案2】:

    如果您想知道从文件中读取了多少数据,只需再次使用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)),这是输出:File size: 1768222Data size: 4294967287 这让我觉得发生了一些令人毛骨悚然的事情,因为数据大小比文件大得多尺寸。它看起来像溢出。
    • @user3745189:这么大的数字解释为十六进制更有意义。你会看到这是一个巨大的数字,或者可能(签名)一个小的负数...-8
    • @user3745189 您的代码也有错误。我编辑了我对工作代码的回答。
    【解决方案3】:

    解决方案 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() 返回读取的字节数。

    【讨论】:

    猜你喜欢
    • 2016-06-18
    • 2022-01-02
    • 1970-01-01
    • 2023-03-19
    • 2011-09-20
    • 1970-01-01
    • 1970-01-01
    • 2011-02-03
    • 1970-01-01
    相关资源
    最近更新 更多