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