QQD图像格式如何解码?
How to decode QQD image format?
QQD 图像格式在 2006 年左右被一些照相手机(可能是日本国内型号)使用。我在网上找不到关于该文件格式的任何信息。如何将它们转换成通用的东西?
前几百个字节(十六进制)
> hexdump -C Bfb2d6ac070d9c77d0b9d911ef441f3c1.qqd | head -n 20
00000000 49 49 42 4d 49 50 0e 00 20 00 00 00 80 3f 00 00 |IIBMIP.. ....?..|
00000010 80 3f 00 00 00 00 00 00 00 00 01 00 00 00 00 00 |.?..............|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000040 80 3f 04 00 00 00 00 00 00 00 03 00 00 00 80 00 |.?..............|
00000050 00 00 f0 00 00 00 a0 00 00 00 01 00 00 00 8a 00 |................|
00000060 00 00 04 00 00 00 f3 00 00 00 a2 00 00 00 01 00 |................|
00000070 00 00 8a 84 03 00 02 00 00 00 cc 03 00 00 88 02 |................|
00000080 00 00 01 00 00 00 2e 1f 07 00 93 07 22 0a da 0a |............"...|
00000090 44 09 82 09 77 09 ce 09 4d 21 c8 1e 78 1d 12 0a |D...w...M!..x...|
000000a0 df 08 c1 09 50 0a de 09 74 0a c6 0a 79 09 81 08 |....P...t...y...|
QQD 图像格式包含一个 0x8A 字节 header,然后是多个不同大小的未压缩图像。每张图片从 top-left 到 top-right 存储,然后向下工作。每行 16 位红色值(高位在低位之后)后跟一行绿色值,然后是蓝色值。
例如,给定问题中的post-0x8A-byte-header图像数据...
00000080 .. .. .. .. .. .. .. .. .. .. 93 07 22 0a da 0a |............"...|
00000090 44 09 82 09 77 09 ce 09 4d 21 c8 1e 78 1d 12 0a |D...w...M!..x...|
000000a0 df 08 c1 09 50 0a de 09 74 0a c6 0a 79 09 81 08 |....P...t...y...|
第一行的红色像素数据,从左边开始,是0x0793、0a22、0ada、0944、0982等。第一个嵌入图像总是240像素宽,所以在这些2字节的240之后红色强度值,相同的 240 像素将是绿色,然后是蓝色值,然后第二行的数据将开始。
我没有在 header 中找到任何指示嵌入图像大小的内容,但通过反复试验 - 计算出我遇到的 qqd 文件大小的嵌入图像大小。
C++解码器程序
下面的代码使用了我在网上搜索到的第一个合适的位图库,恰好是Arash Partow "bitmap_image.hpp",见here。如果那个 link 死了,给自己找另一个类似的库——我只用了一个 set_pixel(x,y,Rgb888)
函数,所以移植很简单。 Arash的库只有header,所以编译时把bitmap_image.hpp
放在下面qqd2bmp.cc
文件所在的目录即可,例如:
clang++ -O3 -std=c++11 -o qqd2bmp qqd2bmp.cc
当您 运行 在命令行中传递一个或多个 .qqd
文件名时,它将生成最大嵌入图像的 .bmp
版本。然后我使用 ImageMagick 的 mogrify -format png *.bmp
来转换所有位图。
// qqd2bmp - QQD phone camera file format -> bitmap conversion
// Tony Delroy
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <string>
#include <iterator>
#include <stdint.h>
#include <cmath>
#include <cassert>
#include "bitmap_image.hpp"
#define OSS(MSG) \
static_cast<std::ostringstream&>(std::ostringstream{} << MSG).str()
using u8 = uint8_t;
using u16 = uint16_t;
namespace
{
std::string g_filename;
struct Rgb888
{
uint8_t red, green, blue;
bool operator==(const Rgb888& rhs) const
{ return red == rhs.red && green == rhs.green && blue == rhs.blue; }
};
struct RrowGrowBrow
{
RrowGrowBrow(std::string name = "BrowGrowBrow") : name_(name) { }
std::string name_;
const std::string& name() const { return name_; }
template <typename Bitmap>
const uint16_t*
operator()(const uint16_t* p, unsigned width, unsigned height,
Bitmap& output)
{
double max = 0;
for (unsigned y = 0; y < height; y += 3)
for (unsigned x = 0; x < width; ++x)
if (p[y * width + x] > max)
max = p[y * width + x];
for (unsigned y = 0; y < height; ++y)
for (unsigned x = 0; x < width; ++x)
output.set_pixel(x, y, Rgb888{
u8(p[y * 3 * width + x] / max * 255),
u8(p[(y * 3 + 1) * width + x] / max * 255),
u8(p[(y * 3 + 2) * width + x] / max * 255) });
return &p[height * 3 * width];
}
};
template <class F>
const uint16_t* extract(const uint16_t* p, unsigned width, unsigned height,
F fn = F{})
{
bitmap_image output{width, height};
p = fn(p, width, height, output);
std::string name = fn.name();
if (!name.empty())
output.save_image(OSS(g_filename << '.' << name << '.' << width
<< ".bmp").c_str());
return p;
}
struct NoopImage
{
template <typename T>
void set_pixel(unsigned a, unsigned b, const T&)
{ }
};
template <class F>
const uint16_t* skip(const uint16_t* p, unsigned width, unsigned height,
F fn = F{})
{
NoopImage noop_image;
return fn(p, width, height, noop_image);
}
}
std::vector<std::pair<int, int>> get_embedded_image_sizes(size_t qqd_file_size)
{
switch (qqd_file_size)
{
// -ve width = skip over image without saving to .bmp
case 4533870:
return { {-240,320}, {-240,20}, {-240,20}, {-162,243}, {648,972} };
case 2629002:
return { {-240,180}, {-176,132}, {704,500}, {-704,28} };
case 2830602:
return { {-240,320}, {-132,172}, {528,528*4/3} };
case 4245870:
return { {-240,160}, {-243,162}, {972,648} };
case 4261944:
return { {-240,159}, {-243,162}, {975,649} };
case 506346:
return { {-240,159}, {-64,42}, {256,170} };
case 1053354:
return { {-240,320}, {-66,88}, {264,352} };
case 1631370:
return { {-240,360}, {-85,128}, {341,512} };
case 636924: // known sizes, but so small might as well use JPEG
case 442794:
case 381738:
/* when decoding new size, first embedded image always 240 wide
* and matches JPEG thumbnail dimensions, then can use the
* loop below and browse the output bitmaps to find the
* remaining images
for (int i = 100; i > 1200; i += 1)
images.push_back({i, i}); // width=height doesn't advance
// pointer for next image parse
*/
default:
std::cerr << "you'll have to recompile with embedded image sizes for "
<< qqd_file_size << "-byte qqd file '" << g_filename << "'\n";
return { };
}
}
int main(int argc, const char* argv[])
{
for (int optind = 1; optind < argc; ++optind)
if (std::ifstream in{g_filename = argv[optind]})
{
std::string qqd{ std::istreambuf_iterator<char>{in},
std::istreambuf_iterator<char>{} };
std::cout << g_filename << ' ' << qqd.size() << " bytes\n";
auto images = get_embedded_image_sizes(qqd.size());
size_t header_length = 0x8a, offset = 0;
const uint16_t* p = (uint16_t*)&qqd[header_length];
int n = 0;
// for (auto [width, height] : images) // C++20
for (auto width_and_height : images) // so C++03 compilers work...
{
auto width = width_and_height.first;
auto height = width_and_height.second;
const uint16_t* q =
width < 0 ? skip<RrowGrowBrow>(p, -width, height, {OSS(n++)})
: extract<RrowGrowBrow>(p, width, height, {OSS(n++)});
if (height != abs(width)) // sentinel condition to reparse as
p = q; // different width
offset = (char*)p - qqd.data();
std::cout << "offset: " << offset << ' '
<< std::hex << offset << std::dec << '\n';
}
if (offset < qqd.size())
std::cerr << "WARNING: not all embedded images were fully parsed "
"from '" << g_filename << "'\n";
else if (offset > qqd.size())
std::cerr << "WARNING: compiled-in images sizes imply more data "
"than exists in qqd file '" << g_filename << "'\n";
}
else
std::cerr << "can't open qqd file '" << g_filename << "'\n";
}
希望能帮助其他人欣赏一些老照片!
QQD 图像格式在 2006 年左右被一些照相手机(可能是日本国内型号)使用。我在网上找不到关于该文件格式的任何信息。如何将它们转换成通用的东西?
前几百个字节(十六进制)
> hexdump -C Bfb2d6ac070d9c77d0b9d911ef441f3c1.qqd | head -n 20
00000000 49 49 42 4d 49 50 0e 00 20 00 00 00 80 3f 00 00 |IIBMIP.. ....?..|
00000010 80 3f 00 00 00 00 00 00 00 00 01 00 00 00 00 00 |.?..............|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000040 80 3f 04 00 00 00 00 00 00 00 03 00 00 00 80 00 |.?..............|
00000050 00 00 f0 00 00 00 a0 00 00 00 01 00 00 00 8a 00 |................|
00000060 00 00 04 00 00 00 f3 00 00 00 a2 00 00 00 01 00 |................|
00000070 00 00 8a 84 03 00 02 00 00 00 cc 03 00 00 88 02 |................|
00000080 00 00 01 00 00 00 2e 1f 07 00 93 07 22 0a da 0a |............"...|
00000090 44 09 82 09 77 09 ce 09 4d 21 c8 1e 78 1d 12 0a |D...w...M!..x...|
000000a0 df 08 c1 09 50 0a de 09 74 0a c6 0a 79 09 81 08 |....P...t...y...|
QQD 图像格式包含一个 0x8A 字节 header,然后是多个不同大小的未压缩图像。每张图片从 top-left 到 top-right 存储,然后向下工作。每行 16 位红色值(高位在低位之后)后跟一行绿色值,然后是蓝色值。
例如,给定问题中的post-0x8A-byte-header图像数据...
00000080 .. .. .. .. .. .. .. .. .. .. 93 07 22 0a da 0a |............"...|
00000090 44 09 82 09 77 09 ce 09 4d 21 c8 1e 78 1d 12 0a |D...w...M!..x...|
000000a0 df 08 c1 09 50 0a de 09 74 0a c6 0a 79 09 81 08 |....P...t...y...|
第一行的红色像素数据,从左边开始,是0x0793、0a22、0ada、0944、0982等。第一个嵌入图像总是240像素宽,所以在这些2字节的240之后红色强度值,相同的 240 像素将是绿色,然后是蓝色值,然后第二行的数据将开始。
我没有在 header 中找到任何指示嵌入图像大小的内容,但通过反复试验 - 计算出我遇到的 qqd 文件大小的嵌入图像大小。
C++解码器程序
下面的代码使用了我在网上搜索到的第一个合适的位图库,恰好是Arash Partow "bitmap_image.hpp",见here。如果那个 link 死了,给自己找另一个类似的库——我只用了一个 set_pixel(x,y,Rgb888)
函数,所以移植很简单。 Arash的库只有header,所以编译时把bitmap_image.hpp
放在下面qqd2bmp.cc
文件所在的目录即可,例如:
clang++ -O3 -std=c++11 -o qqd2bmp qqd2bmp.cc
当您 运行 在命令行中传递一个或多个 .qqd
文件名时,它将生成最大嵌入图像的 .bmp
版本。然后我使用 ImageMagick 的 mogrify -format png *.bmp
来转换所有位图。
// qqd2bmp - QQD phone camera file format -> bitmap conversion
// Tony Delroy
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <string>
#include <iterator>
#include <stdint.h>
#include <cmath>
#include <cassert>
#include "bitmap_image.hpp"
#define OSS(MSG) \
static_cast<std::ostringstream&>(std::ostringstream{} << MSG).str()
using u8 = uint8_t;
using u16 = uint16_t;
namespace
{
std::string g_filename;
struct Rgb888
{
uint8_t red, green, blue;
bool operator==(const Rgb888& rhs) const
{ return red == rhs.red && green == rhs.green && blue == rhs.blue; }
};
struct RrowGrowBrow
{
RrowGrowBrow(std::string name = "BrowGrowBrow") : name_(name) { }
std::string name_;
const std::string& name() const { return name_; }
template <typename Bitmap>
const uint16_t*
operator()(const uint16_t* p, unsigned width, unsigned height,
Bitmap& output)
{
double max = 0;
for (unsigned y = 0; y < height; y += 3)
for (unsigned x = 0; x < width; ++x)
if (p[y * width + x] > max)
max = p[y * width + x];
for (unsigned y = 0; y < height; ++y)
for (unsigned x = 0; x < width; ++x)
output.set_pixel(x, y, Rgb888{
u8(p[y * 3 * width + x] / max * 255),
u8(p[(y * 3 + 1) * width + x] / max * 255),
u8(p[(y * 3 + 2) * width + x] / max * 255) });
return &p[height * 3 * width];
}
};
template <class F>
const uint16_t* extract(const uint16_t* p, unsigned width, unsigned height,
F fn = F{})
{
bitmap_image output{width, height};
p = fn(p, width, height, output);
std::string name = fn.name();
if (!name.empty())
output.save_image(OSS(g_filename << '.' << name << '.' << width
<< ".bmp").c_str());
return p;
}
struct NoopImage
{
template <typename T>
void set_pixel(unsigned a, unsigned b, const T&)
{ }
};
template <class F>
const uint16_t* skip(const uint16_t* p, unsigned width, unsigned height,
F fn = F{})
{
NoopImage noop_image;
return fn(p, width, height, noop_image);
}
}
std::vector<std::pair<int, int>> get_embedded_image_sizes(size_t qqd_file_size)
{
switch (qqd_file_size)
{
// -ve width = skip over image without saving to .bmp
case 4533870:
return { {-240,320}, {-240,20}, {-240,20}, {-162,243}, {648,972} };
case 2629002:
return { {-240,180}, {-176,132}, {704,500}, {-704,28} };
case 2830602:
return { {-240,320}, {-132,172}, {528,528*4/3} };
case 4245870:
return { {-240,160}, {-243,162}, {972,648} };
case 4261944:
return { {-240,159}, {-243,162}, {975,649} };
case 506346:
return { {-240,159}, {-64,42}, {256,170} };
case 1053354:
return { {-240,320}, {-66,88}, {264,352} };
case 1631370:
return { {-240,360}, {-85,128}, {341,512} };
case 636924: // known sizes, but so small might as well use JPEG
case 442794:
case 381738:
/* when decoding new size, first embedded image always 240 wide
* and matches JPEG thumbnail dimensions, then can use the
* loop below and browse the output bitmaps to find the
* remaining images
for (int i = 100; i > 1200; i += 1)
images.push_back({i, i}); // width=height doesn't advance
// pointer for next image parse
*/
default:
std::cerr << "you'll have to recompile with embedded image sizes for "
<< qqd_file_size << "-byte qqd file '" << g_filename << "'\n";
return { };
}
}
int main(int argc, const char* argv[])
{
for (int optind = 1; optind < argc; ++optind)
if (std::ifstream in{g_filename = argv[optind]})
{
std::string qqd{ std::istreambuf_iterator<char>{in},
std::istreambuf_iterator<char>{} };
std::cout << g_filename << ' ' << qqd.size() << " bytes\n";
auto images = get_embedded_image_sizes(qqd.size());
size_t header_length = 0x8a, offset = 0;
const uint16_t* p = (uint16_t*)&qqd[header_length];
int n = 0;
// for (auto [width, height] : images) // C++20
for (auto width_and_height : images) // so C++03 compilers work...
{
auto width = width_and_height.first;
auto height = width_and_height.second;
const uint16_t* q =
width < 0 ? skip<RrowGrowBrow>(p, -width, height, {OSS(n++)})
: extract<RrowGrowBrow>(p, width, height, {OSS(n++)});
if (height != abs(width)) // sentinel condition to reparse as
p = q; // different width
offset = (char*)p - qqd.data();
std::cout << "offset: " << offset << ' '
<< std::hex << offset << std::dec << '\n';
}
if (offset < qqd.size())
std::cerr << "WARNING: not all embedded images were fully parsed "
"from '" << g_filename << "'\n";
else if (offset > qqd.size())
std::cerr << "WARNING: compiled-in images sizes imply more data "
"than exists in qqd file '" << g_filename << "'\n";
}
else
std::cerr << "can't open qqd file '" << g_filename << "'\n";
}
希望能帮助其他人欣赏一些老照片!