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

希望能帮助其他人欣赏一些老照片!