845 lines
32 KiB
C++
845 lines
32 KiB
C++
// Copyright (C) 2020 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
|
|
#include "qicc_p.h"
|
|
|
|
#include <qbuffer.h>
|
|
#include <qbytearray.h>
|
|
#include <qvarlengtharray.h>
|
|
#include <qhash.h>
|
|
#include <qdatastream.h>
|
|
#include <qendian.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qstring.h>
|
|
|
|
#include "qcolorspace_p.h"
|
|
#include "qcolortrc_p.h"
|
|
|
|
#include <array>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
Q_LOGGING_CATEGORY(lcIcc, "qt.gui.icc", QtWarningMsg)
|
|
|
|
namespace QIcc {
|
|
|
|
struct ICCProfileHeader
|
|
{
|
|
quint32_be profileSize;
|
|
|
|
quint32_be preferredCmmType;
|
|
|
|
quint32_be profileVersion;
|
|
quint32_be profileClass;
|
|
quint32_be inputColorSpace;
|
|
quint32_be pcs;
|
|
quint32_be datetime[3];
|
|
quint32_be signature;
|
|
quint32_be platformSignature;
|
|
quint32_be flags;
|
|
quint32_be deviceManufacturer;
|
|
quint32_be deviceModel;
|
|
quint32_be deviceAttributes[2];
|
|
|
|
quint32_be renderingIntent;
|
|
qint32_be illuminantXyz[3];
|
|
|
|
quint32_be creatorSignature;
|
|
quint32_be profileId[4];
|
|
|
|
quint32_be reserved[7];
|
|
|
|
// Technically after the header, but easier to include here:
|
|
quint32_be tagCount;
|
|
};
|
|
|
|
constexpr quint32 IccTag(uchar a, uchar b, uchar c, uchar d)
|
|
{
|
|
return (a << 24) | (b << 16) | (c << 8) | d;
|
|
}
|
|
|
|
enum class ColorSpaceType : quint32 {
|
|
Rgb = IccTag('R', 'G', 'B', ' '),
|
|
Gray = IccTag('G', 'R', 'A', 'Y'),
|
|
};
|
|
|
|
enum class ProfileClass : quint32 {
|
|
Input = IccTag('s', 'c', 'n', 'r'),
|
|
Display = IccTag('m', 'n', 't', 'r'),
|
|
// Not supported:
|
|
Output = IccTag('p', 'r', 't', 'r'),
|
|
ColorSpace = IccTag('s', 'p', 'a', 'c'),
|
|
};
|
|
|
|
enum class Tag : quint32 {
|
|
acsp = IccTag('a', 'c', 's', 'p'),
|
|
RGB_ = IccTag('R', 'G', 'B', ' '),
|
|
XYZ_ = IccTag('X', 'Y', 'Z', ' '),
|
|
rXYZ = IccTag('r', 'X', 'Y', 'Z'),
|
|
gXYZ = IccTag('g', 'X', 'Y', 'Z'),
|
|
bXYZ = IccTag('b', 'X', 'Y', 'Z'),
|
|
rTRC = IccTag('r', 'T', 'R', 'C'),
|
|
gTRC = IccTag('g', 'T', 'R', 'C'),
|
|
bTRC = IccTag('b', 'T', 'R', 'C'),
|
|
kTRC = IccTag('k', 'T', 'R', 'C'),
|
|
A2B0 = IccTag('A', '2', 'B', '0'),
|
|
A2B1 = IccTag('A', '2', 'B', '1'),
|
|
B2A0 = IccTag('B', '2', 'A', '0'),
|
|
B2A1 = IccTag('B', '2', 'A', '1'),
|
|
desc = IccTag('d', 'e', 's', 'c'),
|
|
text = IccTag('t', 'e', 'x', 't'),
|
|
cprt = IccTag('c', 'p', 'r', 't'),
|
|
curv = IccTag('c', 'u', 'r', 'v'),
|
|
para = IccTag('p', 'a', 'r', 'a'),
|
|
wtpt = IccTag('w', 't', 'p', 't'),
|
|
bkpt = IccTag('b', 'k', 'p', 't'),
|
|
mft1 = IccTag('m', 'f', 't', '1'),
|
|
mft2 = IccTag('m', 'f', 't', '2'),
|
|
mluc = IccTag('m', 'l', 'u', 'c'),
|
|
mAB_ = IccTag('m', 'A', 'B', ' '),
|
|
mBA_ = IccTag('m', 'B', 'A', ' '),
|
|
chad = IccTag('c', 'h', 'a', 'd'),
|
|
sf32 = IccTag('s', 'f', '3', '2'),
|
|
|
|
// Apple extensions for ICCv2:
|
|
aarg = IccTag('a', 'a', 'r', 'g'),
|
|
aagg = IccTag('a', 'a', 'g', 'g'),
|
|
aabg = IccTag('a', 'a', 'b', 'g'),
|
|
};
|
|
|
|
} // namespace QIcc
|
|
|
|
inline size_t qHash(const QIcc::Tag &key, size_t seed = 0)
|
|
{
|
|
return qHash(quint32(key), seed);
|
|
}
|
|
|
|
namespace QIcc {
|
|
|
|
struct TagTableEntry
|
|
{
|
|
quint32_be signature;
|
|
quint32_be offset;
|
|
quint32_be size;
|
|
};
|
|
|
|
struct GenericTagData {
|
|
quint32_be type;
|
|
quint32_be null;
|
|
};
|
|
|
|
struct XYZTagData : GenericTagData {
|
|
qint32_be fixedX;
|
|
qint32_be fixedY;
|
|
qint32_be fixedZ;
|
|
};
|
|
|
|
struct CurvTagData : GenericTagData {
|
|
quint32_be valueCount;
|
|
// followed by curv values: quint16_be[]
|
|
};
|
|
|
|
struct ParaTagData : GenericTagData {
|
|
quint16_be curveType;
|
|
quint16_be null2;
|
|
// followed by parameter values: quint32_be[1-7];
|
|
};
|
|
|
|
struct DescTagData : GenericTagData {
|
|
quint32_be asciiDescriptionLength;
|
|
// followed by ascii description: char[]
|
|
// .. we ignore the rest
|
|
};
|
|
|
|
struct MlucTagRecord {
|
|
quint16_be languageCode;
|
|
quint16_be countryCode;
|
|
quint32_be size;
|
|
quint32_be offset;
|
|
};
|
|
|
|
struct MlucTagData : GenericTagData {
|
|
quint32_be recordCount;
|
|
quint32_be recordSize; // = sizeof(MlucTagRecord)
|
|
MlucTagRecord records[1];
|
|
};
|
|
|
|
// For both mAB and mBA
|
|
struct mABTagData : GenericTagData {
|
|
quint8 inputChannels;
|
|
quint8 outputChannels;
|
|
quint8 padding[2];
|
|
quint32_be bCurvesOffset;
|
|
quint32_be matrixOffset;
|
|
quint32_be mCurvesOffset;
|
|
quint32_be clutOffset;
|
|
quint32_be aCurvesOffset;
|
|
};
|
|
|
|
struct Sf32TagData : GenericTagData {
|
|
quint32_be value[1];
|
|
};
|
|
|
|
static int toFixedS1516(float x)
|
|
{
|
|
return int(x * 65536.0f + 0.5f);
|
|
}
|
|
|
|
static float fromFixedS1516(int x)
|
|
{
|
|
return x * (1.0f / 65536.0f);
|
|
}
|
|
|
|
static bool isValidIccProfile(const ICCProfileHeader &header)
|
|
{
|
|
if (header.signature != uint(Tag::acsp)) {
|
|
qCWarning(lcIcc, "Failed ICC signature test");
|
|
return false;
|
|
}
|
|
|
|
// Don't overflow 32bit integers:
|
|
if (header.tagCount >= (INT32_MAX - sizeof(ICCProfileHeader)) / sizeof(TagTableEntry)) {
|
|
qCWarning(lcIcc, "Failed tag count sanity");
|
|
return false;
|
|
}
|
|
if (header.profileSize - sizeof(ICCProfileHeader) < header.tagCount * sizeof(TagTableEntry)) {
|
|
qCWarning(lcIcc, "Failed basic size sanity");
|
|
return false;
|
|
}
|
|
|
|
if (header.profileClass != uint(ProfileClass::Input)
|
|
&& header.profileClass != uint(ProfileClass::Display)
|
|
&& (header.profileClass != uint(ProfileClass::Output)
|
|
|| header.inputColorSpace != uint(ColorSpaceType::Gray))) {
|
|
qCInfo(lcIcc, "Unsupported ICC profile class 0x%x", quint32(header.profileClass));
|
|
return false;
|
|
}
|
|
if (header.inputColorSpace != uint(ColorSpaceType::Rgb)
|
|
&& header.inputColorSpace != uint(ColorSpaceType::Gray)) {
|
|
qCInfo(lcIcc, "Unsupported ICC input color space 0x%x", quint32(header.inputColorSpace));
|
|
return false;
|
|
}
|
|
if (header.pcs != 0x58595a20 /* 'XYZ '*/) {
|
|
// ### support PCSLAB
|
|
qCInfo(lcIcc, "Unsupported ICC profile connection space 0x%x", quint32(header.pcs));
|
|
return false;
|
|
}
|
|
|
|
QColorVector illuminant;
|
|
illuminant.x = fromFixedS1516(header.illuminantXyz[0]);
|
|
illuminant.y = fromFixedS1516(header.illuminantXyz[1]);
|
|
illuminant.z = fromFixedS1516(header.illuminantXyz[2]);
|
|
if (illuminant != QColorVector::D50()) {
|
|
qCWarning(lcIcc, "Invalid ICC illuminant");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int writeColorTrc(QDataStream &stream, const QColorTrc &trc)
|
|
{
|
|
if (trc.isIdentity()) {
|
|
stream << uint(Tag::curv) << uint(0);
|
|
stream << uint(0);
|
|
return 12;
|
|
}
|
|
|
|
if (trc.m_type == QColorTrc::Type::Function) {
|
|
const QColorTransferFunction &fun = trc.m_fun;
|
|
stream << uint(Tag::para) << uint(0);
|
|
if (fun.isGamma()) {
|
|
stream << ushort(0) << ushort(0);
|
|
stream << toFixedS1516(fun.m_g);
|
|
return 12 + 4;
|
|
}
|
|
bool type3 = qFuzzyIsNull(fun.m_e) && qFuzzyIsNull(fun.m_f);
|
|
stream << ushort(type3 ? 3 : 4) << ushort(0);
|
|
stream << toFixedS1516(fun.m_g);
|
|
stream << toFixedS1516(fun.m_a);
|
|
stream << toFixedS1516(fun.m_b);
|
|
stream << toFixedS1516(fun.m_c);
|
|
stream << toFixedS1516(fun.m_d);
|
|
if (type3)
|
|
return 12 + 5 * 4;
|
|
stream << toFixedS1516(fun.m_e);
|
|
stream << toFixedS1516(fun.m_f);
|
|
return 12 + 7 * 4;
|
|
}
|
|
|
|
Q_ASSERT(trc.m_type == QColorTrc::Type::Table);
|
|
stream << uint(Tag::curv) << uint(0);
|
|
stream << uint(trc.m_table.m_tableSize);
|
|
if (!trc.m_table.m_table16.isEmpty()) {
|
|
for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
|
|
stream << ushort(trc.m_table.m_table16[i]);
|
|
}
|
|
} else {
|
|
for (uint i = 0; i < trc.m_table.m_tableSize; ++i) {
|
|
stream << ushort(trc.m_table.m_table8[i] * 257U);
|
|
}
|
|
}
|
|
return 12 + 2 * trc.m_table.m_tableSize;
|
|
}
|
|
|
|
QByteArray toIccProfile(const QColorSpace &space)
|
|
{
|
|
if (!space.isValid())
|
|
return QByteArray();
|
|
|
|
const QColorSpacePrivate *spaceDPtr = QColorSpacePrivate::get(space);
|
|
|
|
constexpr int tagCount = 9;
|
|
constexpr uint profileDataOffset = 128 + 4 + 12 * tagCount;
|
|
constexpr uint variableTagTableOffsets = 128 + 4 + 12 * 5;
|
|
uint currentOffset = 0;
|
|
uint rTrcOffset, gTrcOffset, bTrcOffset;
|
|
uint rTrcSize, gTrcSize, bTrcSize;
|
|
uint descOffset, descSize;
|
|
|
|
QBuffer buffer;
|
|
buffer.open(QIODevice::WriteOnly);
|
|
QDataStream stream(&buffer);
|
|
|
|
// Profile header:
|
|
stream << uint(0); // Size, we will update this later
|
|
stream << uint(0);
|
|
stream << uint(0x02400000); // Version 2.4 (note we use 'para' from version 4)
|
|
stream << uint(ProfileClass::Display);
|
|
stream << uint(Tag::RGB_);
|
|
stream << uint(Tag::XYZ_);
|
|
stream << uint(0) << uint(0) << uint(0);
|
|
stream << uint(Tag::acsp);
|
|
stream << uint(0) << uint(0) << uint(0);
|
|
stream << uint(0) << uint(0) << uint(0);
|
|
stream << uint(1); // Rendering intent
|
|
stream << uint(0x0000f6d6); // D50 X
|
|
stream << uint(0x00010000); // D50 Y
|
|
stream << uint(0x0000d32d); // D50 Z
|
|
stream << IccTag('Q','t', QT_VERSION_MAJOR, QT_VERSION_MINOR);
|
|
stream << uint(0) << uint(0) << uint(0) << uint(0);
|
|
stream << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0) << uint(0);
|
|
|
|
// Tag table:
|
|
stream << uint(tagCount);
|
|
stream << uint(Tag::rXYZ) << uint(profileDataOffset + 00) << uint(20);
|
|
stream << uint(Tag::gXYZ) << uint(profileDataOffset + 20) << uint(20);
|
|
stream << uint(Tag::bXYZ) << uint(profileDataOffset + 40) << uint(20);
|
|
stream << uint(Tag::wtpt) << uint(profileDataOffset + 60) << uint(20);
|
|
stream << uint(Tag::cprt) << uint(profileDataOffset + 80) << uint(12);
|
|
// From here the offset and size will be updated later:
|
|
stream << uint(Tag::rTRC) << uint(0) << uint(0);
|
|
stream << uint(Tag::gTRC) << uint(0) << uint(0);
|
|
stream << uint(Tag::bTRC) << uint(0) << uint(0);
|
|
stream << uint(Tag::desc) << uint(0) << uint(0);
|
|
// TODO: consider adding 'chad' tag (required in ICC >=4 when we have non-D50 whitepoint)
|
|
currentOffset = profileDataOffset;
|
|
|
|
// Tag data:
|
|
stream << uint(Tag::XYZ_) << uint(0);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.r.x);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.r.y);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.r.z);
|
|
stream << uint(Tag::XYZ_) << uint(0);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.g.x);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.g.y);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.g.z);
|
|
stream << uint(Tag::XYZ_) << uint(0);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.b.x);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.b.y);
|
|
stream << toFixedS1516(spaceDPtr->toXyz.b.z);
|
|
stream << uint(Tag::XYZ_) << uint(0);
|
|
stream << toFixedS1516(spaceDPtr->whitePoint.x);
|
|
stream << toFixedS1516(spaceDPtr->whitePoint.y);
|
|
stream << toFixedS1516(spaceDPtr->whitePoint.z);
|
|
stream << uint(Tag::text) << uint(0);
|
|
stream << uint(IccTag('N', '/', 'A', '\0'));
|
|
currentOffset += 92;
|
|
|
|
// From now on the data is variable sized:
|
|
rTrcOffset = currentOffset;
|
|
rTrcSize = writeColorTrc(stream, spaceDPtr->trc[0]);
|
|
currentOffset += rTrcSize;
|
|
if (spaceDPtr->trc[0] == spaceDPtr->trc[1]) {
|
|
gTrcOffset = rTrcOffset;
|
|
gTrcSize = rTrcSize;
|
|
} else {
|
|
gTrcOffset = currentOffset;
|
|
gTrcSize = writeColorTrc(stream, spaceDPtr->trc[1]);
|
|
currentOffset += gTrcSize;
|
|
}
|
|
if (spaceDPtr->trc[0] == spaceDPtr->trc[2]) {
|
|
bTrcOffset = rTrcOffset;
|
|
bTrcSize = rTrcSize;
|
|
} else {
|
|
bTrcOffset = currentOffset;
|
|
bTrcSize = writeColorTrc(stream, spaceDPtr->trc[2]);
|
|
currentOffset += bTrcSize;
|
|
}
|
|
|
|
descOffset = currentOffset;
|
|
QByteArray description = space.description().toUtf8();
|
|
stream << uint(Tag::desc) << uint(0);
|
|
stream << uint(description.size() + 1);
|
|
stream.writeRawData(description.constData(), description.size() + 1);
|
|
stream << uint(0) << uint(0);
|
|
stream << ushort(0) << uchar(0);
|
|
QByteArray macdesc(67, '\0');
|
|
stream.writeRawData(macdesc.constData(), 67);
|
|
descSize = 90 + description.size() + 1;
|
|
currentOffset += descSize;
|
|
|
|
buffer.close();
|
|
QByteArray iccProfile = buffer.buffer();
|
|
// Now write final size
|
|
*(quint32_be *)iccProfile.data() = iccProfile.size();
|
|
// And the final indices and sizes of variable size tags:
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = rTrcOffset;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = rTrcSize;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = gTrcOffset;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = gTrcSize;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 4) = bTrcOffset;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 2 * 12 + 8) = bTrcSize;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 4) = descOffset;
|
|
*(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 3 * 12 + 8) = descSize;
|
|
|
|
#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS)
|
|
const ICCProfileHeader *iccHeader = (const ICCProfileHeader *)iccProfile.constData();
|
|
Q_ASSERT(qsizetype(iccHeader->profileSize) == qsizetype(iccProfile.size()));
|
|
Q_ASSERT(isValidIccProfile(*iccHeader));
|
|
#endif
|
|
|
|
return iccProfile;
|
|
}
|
|
|
|
struct TagEntry {
|
|
quint32 offset;
|
|
quint32 size;
|
|
};
|
|
|
|
static bool parseXyzData(const QByteArray &data, const TagEntry &tagEntry, QColorVector &colorVector)
|
|
{
|
|
if (tagEntry.size < sizeof(XYZTagData)) {
|
|
qCWarning(lcIcc) << "Undersized XYZ tag";
|
|
return false;
|
|
}
|
|
const XYZTagData xyz = qFromUnaligned<XYZTagData>(data.constData() + tagEntry.offset);
|
|
if (xyz.type != quint32(Tag::XYZ_)) {
|
|
qCWarning(lcIcc) << "Bad XYZ content type";
|
|
return false;
|
|
}
|
|
const float x = fromFixedS1516(xyz.fixedX);
|
|
const float y = fromFixedS1516(xyz.fixedY);
|
|
const float z = fromFixedS1516(xyz.fixedZ);
|
|
|
|
colorVector = QColorVector(x, y, z);
|
|
return true;
|
|
}
|
|
|
|
static bool parseTRC(const QByteArray &data, const TagEntry &tagEntry, QColorTrc &gamma)
|
|
{
|
|
const GenericTagData trcData = qFromUnaligned<GenericTagData>(data.constData()
|
|
+ tagEntry.offset);
|
|
if (trcData.type == quint32(Tag::curv)) {
|
|
Q_STATIC_ASSERT(sizeof(CurvTagData) == 12);
|
|
const CurvTagData curv = qFromUnaligned<CurvTagData>(data.constData() + tagEntry.offset);
|
|
if (curv.valueCount > (1 << 16))
|
|
return false;
|
|
if (tagEntry.size - 12 < 2 * curv.valueCount)
|
|
return false;
|
|
const auto valueOffset = tagEntry.offset + sizeof(CurvTagData);
|
|
if (curv.valueCount == 0) {
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction(); // Linear
|
|
} else if (curv.valueCount == 1) {
|
|
const quint16 v = qFromBigEndian<quint16>(data.constData() + valueOffset);
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction::fromGamma(v * (1.0f / 256.0f));
|
|
} else {
|
|
QList<quint16> tabl;
|
|
tabl.resize(curv.valueCount);
|
|
static_assert(sizeof(GenericTagData) == 2 * sizeof(quint32_be),
|
|
"GenericTagData has padding. The following code is a subject to UB.");
|
|
qFromBigEndian<quint16>(data.constData() + valueOffset, curv.valueCount, tabl.data());
|
|
QColorTransferTable table = QColorTransferTable(curv.valueCount, std::move(tabl));
|
|
QColorTransferFunction curve;
|
|
if (!table.checkValidity()) {
|
|
qCWarning(lcIcc) << "Invalid curv table";
|
|
return false;
|
|
} else if (!table.asColorTransferFunction(&curve)) {
|
|
gamma.m_type = QColorTrc::Type::Table;
|
|
gamma.m_table = table;
|
|
} else {
|
|
qCDebug(lcIcc) << "Detected curv table as function";
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = curve;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (trcData.type == quint32(Tag::para)) {
|
|
Q_STATIC_ASSERT(sizeof(ParaTagData) == 12);
|
|
const ParaTagData para = qFromUnaligned<ParaTagData>(data.constData() + tagEntry.offset);
|
|
const auto parametersOffset = tagEntry.offset + sizeof(ParaTagData);
|
|
quint32 parameters[7];
|
|
switch (para.curveType) {
|
|
case 0: {
|
|
if (tagEntry.size < sizeof(ParaTagData) + 1 * 4)
|
|
return false;
|
|
qFromBigEndian<quint32>(data.constData() + parametersOffset, 1, parameters);
|
|
float g = fromFixedS1516(parameters[0]);
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction::fromGamma(g);
|
|
break;
|
|
}
|
|
case 1: {
|
|
if (tagEntry.size < sizeof(ParaTagData) + 3 * 4)
|
|
return false;
|
|
qFromBigEndian<quint32>(data.constData() + parametersOffset, 3, parameters);
|
|
if (parameters[1] == 0)
|
|
return false;
|
|
float g = fromFixedS1516(parameters[0]);
|
|
float a = fromFixedS1516(parameters[1]);
|
|
float b = fromFixedS1516(parameters[2]);
|
|
float d = -b / a;
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, 0.0f, 0.0f, g);
|
|
break;
|
|
}
|
|
case 2: {
|
|
if (tagEntry.size < sizeof(ParaTagData) + 4 * 4)
|
|
return false;
|
|
qFromBigEndian<quint32>(data.constData() + parametersOffset, 4, parameters);
|
|
if (parameters[1] == 0)
|
|
return false;
|
|
float g = fromFixedS1516(parameters[0]);
|
|
float a = fromFixedS1516(parameters[1]);
|
|
float b = fromFixedS1516(parameters[2]);
|
|
float c = fromFixedS1516(parameters[3]);
|
|
float d = -b / a;
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction(a, b, 0.0f, d, c, c, g);
|
|
break;
|
|
}
|
|
case 3: {
|
|
if (tagEntry.size < sizeof(ParaTagData) + 5 * 4)
|
|
return false;
|
|
qFromBigEndian<quint32>(data.constData() + parametersOffset, 5, parameters);
|
|
float g = fromFixedS1516(parameters[0]);
|
|
float a = fromFixedS1516(parameters[1]);
|
|
float b = fromFixedS1516(parameters[2]);
|
|
float c = fromFixedS1516(parameters[3]);
|
|
float d = fromFixedS1516(parameters[4]);
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction(a, b, c, d, 0.0f, 0.0f, g);
|
|
break;
|
|
}
|
|
case 4: {
|
|
if (tagEntry.size < sizeof(ParaTagData) + 7 * 4)
|
|
return false;
|
|
qFromBigEndian<quint32>(data.constData() + parametersOffset, 7, parameters);
|
|
float g = fromFixedS1516(parameters[0]);
|
|
float a = fromFixedS1516(parameters[1]);
|
|
float b = fromFixedS1516(parameters[2]);
|
|
float c = fromFixedS1516(parameters[3]);
|
|
float d = fromFixedS1516(parameters[4]);
|
|
float e = fromFixedS1516(parameters[5]);
|
|
float f = fromFixedS1516(parameters[6]);
|
|
gamma.m_type = QColorTrc::Type::Function;
|
|
gamma.m_fun = QColorTransferFunction(a, b, c, d, e, f, g);
|
|
break;
|
|
}
|
|
default:
|
|
qCWarning(lcIcc) << "Unknown para type" << uint(para.curveType);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
qCWarning(lcIcc) << "Invalid TRC data type";
|
|
return false;
|
|
}
|
|
|
|
static bool parseDesc(const QByteArray &data, const TagEntry &tagEntry, QString &descName)
|
|
{
|
|
const GenericTagData tag = qFromUnaligned<GenericTagData>(data.constData() + tagEntry.offset);
|
|
|
|
// Either 'desc' (ICCv2) or 'mluc' (ICCv4)
|
|
if (tag.type == quint32(Tag::desc)) {
|
|
Q_STATIC_ASSERT(sizeof(DescTagData) == 12);
|
|
const DescTagData desc = qFromUnaligned<DescTagData>(data.constData() + tagEntry.offset);
|
|
const quint32 len = desc.asciiDescriptionLength;
|
|
if (len < 1)
|
|
return false;
|
|
if (tagEntry.size - 12 < len)
|
|
return false;
|
|
const char *asciiDescription = data.constData() + tagEntry.offset + sizeof(DescTagData);
|
|
if (asciiDescription[len - 1] != '\0')
|
|
return false;
|
|
descName = QString::fromLatin1(asciiDescription, len - 1);
|
|
return true;
|
|
}
|
|
if (tag.type != quint32(Tag::mluc))
|
|
return false;
|
|
|
|
if (tagEntry.size < sizeof(MlucTagData))
|
|
return false;
|
|
const MlucTagData mluc = qFromUnaligned<MlucTagData>(data.constData() + tagEntry.offset);
|
|
if (mluc.recordCount < 1)
|
|
return false;
|
|
if (mluc.recordSize < 12)
|
|
return false;
|
|
// We just use the primary record regardless of language or country.
|
|
const quint32 stringOffset = mluc.records[0].offset;
|
|
const quint32 stringSize = mluc.records[0].size;
|
|
if (tagEntry.size < stringOffset || tagEntry.size - stringOffset < stringSize )
|
|
return false;
|
|
if ((stringSize | stringOffset) & 1)
|
|
return false;
|
|
quint32 stringLen = stringSize / 2;
|
|
QVarLengthArray<char16_t> utf16hostendian(stringLen);
|
|
qFromBigEndian<char16_t>(data.constData() + tagEntry.offset + stringOffset, stringLen,
|
|
utf16hostendian.data());
|
|
// The given length shouldn't include 0-termination, but might.
|
|
if (stringLen > 1 && utf16hostendian[stringLen - 1] == 0)
|
|
--stringLen;
|
|
descName = QString::fromUtf16(utf16hostendian.data(), stringLen);
|
|
return true;
|
|
}
|
|
|
|
static bool parseRgbMatrix(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr)
|
|
{
|
|
// Parse XYZ tags
|
|
if (!parseXyzData(data, tagIndex[Tag::rXYZ], colorspaceDPtr->toXyz.r))
|
|
return false;
|
|
if (!parseXyzData(data, tagIndex[Tag::gXYZ], colorspaceDPtr->toXyz.g))
|
|
return false;
|
|
if (!parseXyzData(data, tagIndex[Tag::bXYZ], colorspaceDPtr->toXyz.b))
|
|
return false;
|
|
if (!parseXyzData(data, tagIndex[Tag::wtpt], colorspaceDPtr->whitePoint))
|
|
return false;
|
|
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
|
|
if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromSRgb()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: sRGB primaries detected";
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
|
|
} else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromAdobeRgb()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: Adobe RGB primaries detected";
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::AdobeRgb;
|
|
} else if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromDciP3D65()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: DCI-P3 D65 primaries detected";
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::DciP3D65;
|
|
}
|
|
if (colorspaceDPtr->toXyz == QColorMatrix::toXyzFromProPhotoRgb()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: ProPhoto RGB primaries detected";
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::ProPhotoRgb;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool parseGrayMatrix(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr)
|
|
{
|
|
// We will use sRGB primaries and fit to match the given white-point if
|
|
// it doesn't match sRGB's.
|
|
QColorVector whitePoint;
|
|
if (!parseXyzData(data, tagIndex[Tag::wtpt], whitePoint))
|
|
return false;
|
|
if (!qFuzzyCompare(whitePoint.y, 1.0f) || (1.0f + whitePoint.z + whitePoint.x) == 0.0f) {
|
|
qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - gray white-point not normalized";
|
|
return false;
|
|
}
|
|
if (whitePoint == QColorVector::D65()) {
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::SRgb;
|
|
} else {
|
|
colorspaceDPtr->primaries = QColorSpace::Primaries::Custom;
|
|
// Calculate chromaticity from xyz (assuming y == 1.0f).
|
|
float y = 1.0f / (1.0f + whitePoint.z + whitePoint.x);
|
|
float x = whitePoint.x * y;
|
|
QColorSpacePrimaries primaries(QColorSpace::Primaries::SRgb);
|
|
primaries.whitePoint = QPointF(x,y);
|
|
if (!primaries.areValid()) {
|
|
qCWarning(lcIcc, "fromIccProfile: Invalid ICC profile - invalid white-point(%f, %f)", x, y);
|
|
return false;
|
|
}
|
|
colorspaceDPtr->toXyz = primaries.toXyzMatrix();
|
|
}
|
|
return true;
|
|
}
|
|
static bool parseTRCs(const QByteArray &data, const QHash<Tag, TagEntry> &tagIndex, QColorSpacePrivate *colorspaceDPtr, bool isColorSpaceTypeGray)
|
|
{
|
|
TagEntry rTrc;
|
|
TagEntry gTrc;
|
|
TagEntry bTrc;
|
|
if (isColorSpaceTypeGray) {
|
|
rTrc = tagIndex[Tag::kTRC];
|
|
gTrc = tagIndex[Tag::kTRC];
|
|
bTrc = tagIndex[Tag::kTRC];
|
|
} else if (tagIndex.contains(Tag::aarg) && tagIndex.contains(Tag::aagg) && tagIndex.contains(Tag::aabg)) {
|
|
// Apple extension for parametric version of TRCs in ICCv2:
|
|
rTrc = tagIndex[Tag::aarg];
|
|
gTrc = tagIndex[Tag::aagg];
|
|
bTrc = tagIndex[Tag::aabg];
|
|
} else {
|
|
rTrc = tagIndex[Tag::rTRC];
|
|
gTrc = tagIndex[Tag::gTRC];
|
|
bTrc = tagIndex[Tag::bTRC];
|
|
}
|
|
|
|
QColorTrc rCurve;
|
|
QColorTrc gCurve;
|
|
QColorTrc bCurve;
|
|
if (!parseTRC(data, rTrc, rCurve)) {
|
|
qCWarning(lcIcc) << "fromIccProfile: Invalid rTRC";
|
|
return false;
|
|
}
|
|
if (!parseTRC(data, gTrc, gCurve)) {
|
|
qCWarning(lcIcc) << "fromIccProfile: Invalid gTRC";
|
|
return false;
|
|
}
|
|
if (!parseTRC(data, bTrc, bCurve)) {
|
|
qCWarning(lcIcc) << "fromIccProfile: Invalid bTRC";
|
|
return false;
|
|
}
|
|
if (rCurve == gCurve && gCurve == bCurve) {
|
|
if (rCurve.isIdentity()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: Linear gamma detected";
|
|
colorspaceDPtr->trc[0] = QColorTransferFunction();
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Linear;
|
|
colorspaceDPtr->gamma = 1.0f;
|
|
} else if (rCurve.m_type == QColorTrc::Type::Function) {
|
|
if (rCurve.m_fun.isGamma()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: Simple gamma detected";
|
|
colorspaceDPtr->trc[0] = QColorTransferFunction::fromGamma(rCurve.m_fun.m_g);
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Gamma;
|
|
colorspaceDPtr->gamma = rCurve.m_fun.m_g;
|
|
} else if (rCurve.m_fun.isSRgb()) {
|
|
qCDebug(lcIcc) << "fromIccProfile: sRGB gamma detected";
|
|
colorspaceDPtr->trc[0] = QColorTransferFunction::fromSRgb();
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::SRgb;
|
|
} else {
|
|
colorspaceDPtr->trc[0] = rCurve;
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
|
|
}
|
|
} else {
|
|
colorspaceDPtr->trc[0] = rCurve;
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
|
|
}
|
|
colorspaceDPtr->trc[1] = colorspaceDPtr->trc[0];
|
|
colorspaceDPtr->trc[2] = colorspaceDPtr->trc[0];
|
|
} else {
|
|
colorspaceDPtr->trc[0] = rCurve;
|
|
colorspaceDPtr->trc[1] = gCurve;
|
|
colorspaceDPtr->trc[2] = bCurve;
|
|
colorspaceDPtr->transferFunction = QColorSpace::TransferFunction::Custom;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace)
|
|
{
|
|
if (data.size() < qsizetype(sizeof(ICCProfileHeader))) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed size sanity 1";
|
|
return false;
|
|
}
|
|
const ICCProfileHeader header = qFromUnaligned<ICCProfileHeader>(data.constData());
|
|
if (!isValidIccProfile(header))
|
|
return false; // if failed we already printing a warning
|
|
if (qsizetype(header.profileSize) > data.size() || qsizetype(header.profileSize) < qsizetype(sizeof(ICCProfileHeader))) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed size sanity 2";
|
|
return false;
|
|
}
|
|
|
|
const qsizetype offsetToData = sizeof(ICCProfileHeader) + header.tagCount * sizeof(TagTableEntry);
|
|
Q_ASSERT(offsetToData > 0);
|
|
if (offsetToData > data.size()) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed index size sanity";
|
|
return false;
|
|
}
|
|
|
|
QHash<Tag, TagEntry> tagIndex;
|
|
for (uint i = 0; i < header.tagCount; ++i) {
|
|
// Read tag index
|
|
const qsizetype tableOffset = sizeof(ICCProfileHeader) + i * sizeof(TagTableEntry);
|
|
const TagTableEntry tagTable = qFromUnaligned<TagTableEntry>(data.constData()
|
|
+ tableOffset);
|
|
|
|
// Sanity check tag sizes and offsets:
|
|
if (qsizetype(tagTable.offset) < offsetToData) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 1";
|
|
return false;
|
|
}
|
|
// Checked separately from (+ size) to handle overflow.
|
|
if (tagTable.offset > header.profileSize) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed tag offset sanity 2";
|
|
return false;
|
|
}
|
|
if (tagTable.size < 12) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed minimal tag size sanity";
|
|
return false;
|
|
}
|
|
if (tagTable.size > header.profileSize - tagTable.offset) {
|
|
qCWarning(lcIcc) << "fromIccProfile: failed tag offset + size sanity";
|
|
return false;
|
|
}
|
|
if (tagTable.offset & 0x03) {
|
|
qCWarning(lcIcc) << "fromIccProfile: invalid tag offset alignment";
|
|
return false;
|
|
}
|
|
// printf("'%4s' %d %d\n", (const char *)&tagTable.signature,
|
|
// quint32(tagTable.offset),
|
|
// quint32(tagTable.size));
|
|
tagIndex.insert(Tag(quint32(tagTable.signature)), { tagTable.offset, tagTable.size });
|
|
}
|
|
|
|
// Check the profile is three-component matrix based (what we currently support):
|
|
if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
|
|
if (!tagIndex.contains(Tag::rXYZ) || !tagIndex.contains(Tag::gXYZ) || !tagIndex.contains(Tag::bXYZ) ||
|
|
!tagIndex.contains(Tag::rTRC) || !tagIndex.contains(Tag::gTRC) || !tagIndex.contains(Tag::bTRC) ||
|
|
!tagIndex.contains(Tag::wtpt)) {
|
|
qCInfo(lcIcc) << "fromIccProfile: Unsupported ICC profile - not three component matrix based";
|
|
return false;
|
|
}
|
|
} else {
|
|
Q_ASSERT(header.inputColorSpace == uint(ColorSpaceType::Gray));
|
|
if (!tagIndex.contains(Tag::kTRC) || !tagIndex.contains(Tag::wtpt)) {
|
|
qCWarning(lcIcc) << "fromIccProfile: Invalid ICC profile - not valid gray scale based";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
colorSpace->detach();
|
|
QColorSpacePrivate *colorspaceDPtr = QColorSpacePrivate::get(*colorSpace);
|
|
|
|
if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) {
|
|
if (!parseRgbMatrix(data, tagIndex, colorspaceDPtr))
|
|
return false;
|
|
} else {
|
|
if (!parseGrayMatrix(data, tagIndex, colorspaceDPtr))
|
|
return false;
|
|
}
|
|
|
|
// Reset the matrix to our canonical values:
|
|
if (colorspaceDPtr->primaries != QColorSpace::Primaries::Custom)
|
|
colorspaceDPtr->setToXyzMatrix();
|
|
|
|
if (!parseTRCs(data, tagIndex, colorspaceDPtr, header.inputColorSpace == uint(ColorSpaceType::Gray)))
|
|
return false;
|
|
|
|
if (tagIndex.contains(Tag::desc)) {
|
|
if (!parseDesc(data, tagIndex[Tag::desc], colorspaceDPtr->description))
|
|
qCWarning(lcIcc) << "fromIccProfile: Failed to parse description";
|
|
else
|
|
qCDebug(lcIcc) << "fromIccProfile: Description" << colorspaceDPtr->description;
|
|
}
|
|
|
|
colorspaceDPtr->identifyColorSpace();
|
|
if (colorspaceDPtr->namedColorSpace)
|
|
qCDebug(lcIcc) << "fromIccProfile: Named colorspace detected: " << QColorSpace::NamedColorSpace(colorspaceDPtr->namedColorSpace);
|
|
|
|
colorspaceDPtr->iccProfile = data;
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace QIcc
|
|
|
|
QT_END_NAMESPACE
|