Files

138 lines
5.1 KiB
C++

#pragma once
#include <QByteArray>
#include <QSize>
#include <QFile>
#include <QString>
#include <QtEndian>
#include <array>
#include <cmath>
#include <functional>
#include <optional>
#include <stdexcept>
#include <vector>
// Tiny DNG files: an RGGB mosaic, a linear sRGB camera.
namespace DNGFixture {
struct Options {
int width = 64;
int height = 48;
// A photosite's value: 0 red, 1 green, 2 blue.
std::function<quint16(int channel, int x, int y)> value = [](int, int, int) { return quint16(20000); };
std::array<double, 3> asShotNeutral{1, 1, 1};
quint16 orientation = 1;
// A header's size past the pixels written, for budgets.
std::optional<QSize> claimed;
// Row-major: the camera's channels from sRGB's.
std::array<double, 9> mix{1, 0, 0, 0, 1, 0, 0, 0, 1};
// A fourth plane, the second green, as CMYG sensors have.
bool fourColours = false;
};
// XYZ (D65) to linear sRGB: the camera sees sRGB.
inline constexpr std::array<double, 9> colorMatrix{3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108, 0.0415560, 0.0556434, -0.2040259, 1.0572252};
struct Entry {
quint16 tag;
quint16 type;
quint32 count;
QByteArray data;
};
inline QByteArray le16(quint16 value)
{
QByteArray bytes(2, '\0');
qToLittleEndian(value, bytes.data());
return bytes;
}
inline QByteArray le32(quint32 value)
{
QByteArray bytes(4, '\0');
qToLittleEndian(value, bytes.data());
return bytes;
}
// Ten-thousandths; a signed numerator keeps its bits.
inline QByteArray rational(double value)
{
return le32(quint32(qint32(std::lround(value * 10000)))) + le32(10000);
}
inline QByteArray data(const Options &options)
{
QByteArray pixels;
for (int y = 0; y < options.height; ++y)
for (int x = 0; x < options.width; ++x) {
// RGGB: red on even rows and columns.
const int channel = y % 2 == 0 ? (x % 2 == 0 ? 0 : 1) : (x % 2 == 0 ? 1 : 2);
pixels += le16(options.value(channel, x, y));
}
QByteArray matrix, neutral;
const int planes = options.fourColours ? 4 : 3;
for (int row = 0; row < planes; ++row)
for (size_t column = 0; column < 3; ++column) {
const size_t from = size_t(row == 3 ? 1 : row) * 3;
matrix += rational(options.mix[from] * colorMatrix[column] + options.mix[from + 1] * colorMatrix[3 + column] + options.mix[from + 2] * colorMatrix[6 + column]);
}
for (int plane = 0; plane < planes; ++plane)
neutral += rational(options.asShotNeutral[size_t(plane == 3 ? 1 : plane)]);
const QByteArray model("OmaPhoto Fixture", 17);
std::vector<Entry> entries{
{254, 4, 1, le32(0)},
{256, 4, 1, le32(quint32(options.claimed.value_or(QSize(options.width, options.height)).width()))},
{257, 4, 1, le32(quint32(options.claimed.value_or(QSize(options.width, options.height)).height()))},
{258, 3, 1, le16(16) + le16(0)},
{259, 3, 1, le16(1) + le16(0)},
{262, 3, 1, le16(32803) + le16(0)},
{271, 2, 9, QByteArray("OmaPhoto", 9)},
{272, 2, 8, QByteArray("Fixture", 8)},
{273, 4, 1, QByteArray()},
{274, 3, 1, le16(options.orientation) + le16(0)},
{277, 3, 1, le16(1) + le16(0)},
{278, 4, 1, le32(quint32(options.height))},
{279, 4, 1, le32(quint32(pixels.size()))},
{284, 3, 1, le16(1) + le16(0)},
{33421, 3, 2, le16(2) + le16(2)},
{33422, 1, 4, options.fourColours ? QByteArray("\0\1\3\2", 4) : QByteArray("\0\1\1\2", 4)},
{50706, 1, 4, QByteArray("\1\4\0\0", 4)},
{50707, 1, 4, QByteArray("\1\1\0\0", 4)},
{50708, 2, quint32(model.size()), model},
{50710, 1, quint32(planes), QByteArray("\0\1\2\3", planes)},
{50714, 4, 1, le32(0)},
{50717, 4, 1, le32(65535)},
{50721, 10, quint32(planes * 3), matrix},
{50728, 5, quint32(planes), neutral},
{50778, 3, 1, le16(21) + le16(0)},
};
// Header, then the directory, its outside values, then the pixels.
const quint32 directory = 8, outside = directory + 2 + quint32(entries.size()) * 12 + 4;
QByteArray values;
for (Entry &entry : entries)
if (entry.data.size() > 4) {
if (values.size() % 2)
values += '\0';
const QByteArray stored = entry.data;
entry.data = le32(outside + quint32(values.size()));
values += stored;
}
if (values.size() % 2)
values += '\0';
const quint32 strip = outside + quint32(values.size());
QByteArray file = QByteArray("II*\0", 4) + le32(directory) + le16(quint16(entries.size()));
for (const Entry &entry : entries) {
const QByteArray inline_ = entry.tag == 273 ? le32(strip) : entry.data.leftJustified(4, '\0');
file += le16(entry.tag) + le16(entry.type) + le32(entry.count) + inline_;
}
file += le32(0) + values + pixels;
return file;
}
inline QString write(const QString &path, const Options &options = {})
{
QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data(options)) <= 0)
throw std::runtime_error("could not write a DNG");
return path;
}
}