Search code examples
image-formats

How to decode QQD image format?


The QQD image format was used by some camera phones (likely Japanese domestic models) around 2006. I can't find any online information about the file format. How to convert them to something common?

First couple hundred bytes (in hex)

> 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...|

Solution

  • The QQD image format consists of an 0x8A-byte header, then multiple uncompressed images of varying sizes. Each image is stored from top-left to top-right, then working downwards. Each row of 16-bit red values (with high order bits after low), is followed by a row of green values then blue.

    For example, given the post-0x8A-byte-header image data in the question...

    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...|
    

    The red pixel data for the top row, starting from the left, is 0x0793, 0a22, 0ada, 0944, 0982 etc.. The first embedded image is always 240 pixels wide, so after 240 of these 2-byte red intensity values, there'll be the green and then the blue values for the same 240 pixels, then the second row's data will start.

    I haven't found anything in the header to indicate the embedded image sizes, but have - by trial and error - worked out the embedded image sizes for the qqd file sizes I've encountered.

    C++ decoder program

    The following code uses the first suitable bitmap library I googled online, which happens to be "bitmap_image.hpp" by Arash Partow, see here. If that link dies, grab yourself another similar library - I only used a set_pixel(x,y,Rgb888) function so porting will be trivial. Arash's library is header only, so just put the bitmap_image.hpp in the directory with the qqd2bmp.cc file below when compiling, e.g.:

    clang++ -O3 -std=c++11 -o qqd2bmp qqd2bmp.cc
    

    When you run, pass one or more .qqd filenames on the command line and it'll generate .bmp versions of the largest embedded image. I then used ImageMagick's mogrify -format png *.bmp to convert all the bitmaps.

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

    Hope that helps someone else enjoy some old photos!