diff options
author | Allan Sandfeld Jensen <[email protected]> | 2024-02-28 16:31:39 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <[email protected]> | 2024-05-31 16:24:50 +0200 |
commit | 53ad7b2d9bf0a6b10656e8bed8eba518888e9495 (patch) | |
tree | bc19d81c55f680bfd8c461eafcc6804716adb3b0 | |
parent | 57595bd90a974f33ce4573afff0f145caabd4b32 (diff) |
Complete color space toICC write
Also write gray, CMYK and mAB RGB color space profiles.
Fixes: QTBUG-125302
Change-Id: Id3b3b64537b9c08f1d40b8243c228ad111d08289
Reviewed-by: Laszlo Agocs <[email protected]>
-rw-r--r-- | src/gui/painting/qcolorspace.cpp | 5 | ||||
-rw-r--r-- | src/gui/painting/qcolortrc_p.h | 2 | ||||
-rw-r--r-- | src/gui/painting/qicc.cpp | 737 | ||||
-rw-r--r-- | tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp | 9 | ||||
-rw-r--r-- | tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp | 6 |
5 files changed, 633 insertions, 126 deletions
diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp index f21e6ec7382..99c88ee0bea 100644 --- a/src/gui/painting/qcolorspace.cpp +++ b/src/gui/painting/qcolorspace.cpp @@ -1423,13 +1423,16 @@ QDebug operator<<(QDebug dbg, const QColorSpace &colorSpace) if (colorSpace.d_ptr) { if (colorSpace.d_ptr->namedColorSpace) dbg << colorSpace.d_ptr->namedColorSpace << ", "; + else + dbg << colorSpace.colorModel() << ", "; if (!colorSpace.isValid()) { dbg << "Invalid"; if (!colorSpace.d_ptr->iccProfile.isEmpty()) dbg << " with profile data"; } else if (colorSpace.d_ptr->isThreeComponentMatrix()) { dbg << colorSpace.primaries() << ", " << colorSpace.transferFunction(); - dbg << ", gamma=" << colorSpace.gamma(); + if (colorSpace.transferFunction() == QColorSpace::TransferFunction::Gamma) + dbg << "=" << colorSpace.gamma(); } else { if (colorSpace.d_ptr->isPcsLab) dbg << "PCSLab, "; diff --git a/src/gui/painting/qcolortrc_p.h b/src/gui/painting/qcolortrc_p.h index d1ad1987dfe..c7a1d1bc882 100644 --- a/src/gui/painting/qcolortrc_p.h +++ b/src/gui/painting/qcolortrc_p.h @@ -86,6 +86,8 @@ public: friend inline bool operator!=(const QColorTrc &o1, const QColorTrc &o2); friend inline bool operator==(const QColorTrc &o1, const QColorTrc &o2); + const QColorTransferTable &table() const { return m_table; } + Type m_type; QColorTransferFunction m_fun; QColorTransferTable m_table; diff --git a/src/gui/painting/qicc.cpp b/src/gui/painting/qicc.cpp index a2786fbb8b2..957fd833f2c 100644 --- a/src/gui/painting/qicc.cpp +++ b/src/gui/painting/qicc.cpp @@ -264,7 +264,11 @@ struct MatrixElement { static int toFixedS1516(float x) { - return int(x * 65536.0f + 0.5f); + if (x < float(SHRT_MIN)) + return INT_MIN; + if (x > float(SHRT_MAX)) + return INT_MAX; + return qRound(x * 65536.0f); } static float fromFixedS1516(int x) @@ -368,31 +372,431 @@ static int writeColorTrc(QDataStream &stream, const QColorTrc &trc) return 12 + 2 * trc.m_table.m_tableSize; } +// very simple version for small values (<=4) of exp. +static constexpr qsizetype intPow(qsizetype x, qsizetype exp) +{ + return (exp <= 1) ? x : x * intPow(x, exp - 1); +} + +struct ElementCombo +{ + const QColorMatrix *inMatrix = nullptr; + const QColorSpacePrivate::TransferElement *inTable = nullptr; + const QColorCLUT *clut = nullptr; + const QColorSpacePrivate::TransferElement *midTable = nullptr; + const QColorMatrix *midMatrix = nullptr; + const QColorVector *midOffset = nullptr; + const QColorSpacePrivate::TransferElement *outTable = nullptr; +}; + +static void visitElement(ElementCombo &combo, const QColorSpacePrivate::TransferElement &element, int number, + const QList<QColorSpacePrivate::Element> &list) +{ + if (number == 0) + combo.inTable = &element; + else if (number == list.size() - 1) + combo.outTable = &element; + else if (number == 1 && combo.inMatrix) + combo.inTable = &element; + else + combo.midTable = &element; +} + +static void visitElement(ElementCombo &combo, const QColorMatrix &element, int number, + const QList<QColorSpacePrivate::Element> &) +{ + if (number == 0) + combo.inMatrix = &element; + else + combo.midMatrix = &element; +} + +static void visitElement(ElementCombo &combo, const QColorVector &element, int, + const QList<QColorSpacePrivate::Element> &) +{ + combo.midOffset = &element; +} + +static void visitElement(ElementCombo &combo, const QColorCLUT &element, int, + const QList<QColorSpacePrivate::Element> &) +{ + combo.clut = &element; +} + +static bool isTableTrc(const QColorSpacePrivate::TransferElement *transfer) +{ + int i = 0; + while (i < 4 && transfer->trc[i].isValid()) { + if (transfer->trc[i].m_type != QColorTrc::Type::Table) + return false; + i++; + } + return i > 0; +} + +static bool isTableTrcSingleSize(const QColorSpacePrivate::TransferElement *transfer) +{ + Q_ASSERT(transfer->trc[0].m_type == QColorTrc::Type::Table); + int i = 1; + const uint32_t size = transfer->trc[0].table().m_tableSize; + while (i < 4 && transfer->trc[i].isValid()) { + if (transfer->trc[i].table().m_tableSize != size) + return false; + i++; + } + return true; +} + +static int writeMab(QDataStream &stream, const QList<QColorSpacePrivate::Element> &abList, bool isAb, bool pcsLab, bool isCmyk) +{ + int number = 0; + ElementCombo combo; + for (auto &&element : abList) + std::visit([&](auto &&elm) { visitElement(combo, elm, number++, abList); }, element); + + Q_ASSERT(!(combo.inMatrix && combo.midMatrix)); + + // qWarning() << Q_FUNC_INFO << bool(combo.inMatrix) << bool(combo.inTable) << bool(combo.clut) << bool(combo.midTable) << bool(combo.midMatrix) << bool(combo.midOffset) << bool(combo.outTable); + bool lut16 = true; + if (combo.midMatrix || combo.midTable || combo.midOffset) + lut16 = false; + if (combo.clut && (combo.clut->gridPointsX != combo.clut->gridPointsY || + combo.clut->gridPointsX != combo.clut->gridPointsZ || + (combo.clut->gridPointsW > 1 && combo.clut->gridPointsX != combo.clut->gridPointsW))) + lut16 = false; + if (lut16 && combo.inTable) + lut16 = isTableTrc(combo.inTable) && isTableTrcSingleSize(combo.inTable); + if (lut16 && combo.outTable) + lut16 = isTableTrc(combo.outTable) && isTableTrcSingleSize(combo.outTable); + + if (!lut16) { + if (combo.inMatrix) + qSwap(combo.inMatrix, combo.midMatrix); + if (isAb) + stream << uint(Tag::mAB_) << uint(0); + else + stream << uint(Tag::mBA_) << uint(0); + } else { + stream << uint(Tag::mft2) << uint(0); + } + + const int inChannels = (isCmyk && isAb) ? 4 : 3; + const int outChannels = (isCmyk && !isAb) ? 4 : 3; + stream << uchar(inChannels) << uchar(outChannels); + qsizetype gridPointsLut16 = 0; + if (lut16 && combo.clut) + gridPointsLut16 = combo.clut->gridPointsX; + if (lut16) + stream << uchar(gridPointsLut16) << uchar(0); + else + stream << quint16(0); + if (lut16) { + if (combo.inMatrix) { + stream << toFixedS1516(combo.inMatrix->r.x); + stream << toFixedS1516(combo.inMatrix->g.x); + stream << toFixedS1516(combo.inMatrix->b.x); + stream << toFixedS1516(combo.inMatrix->r.y); + stream << toFixedS1516(combo.inMatrix->g.y); + stream << toFixedS1516(combo.inMatrix->b.y); + stream << toFixedS1516(combo.inMatrix->r.z); + stream << toFixedS1516(combo.inMatrix->g.z); + stream << toFixedS1516(combo.inMatrix->b.z); + } else { + stream << toFixedS1516(1.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(1.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(0.0f); + stream << toFixedS1516(1.0f); + } + int inputEntries = 0, outputEntries = 0; + if (combo.inTable) + inputEntries = combo.inTable->trc[0].table().m_tableSize; + else + inputEntries = 2; + if (combo.outTable) + outputEntries = combo.outTable->trc[0].table().m_tableSize; + else + outputEntries = 2; + stream << quint16(inputEntries); + stream << quint16(outputEntries); + auto writeTable = [&](const QColorSpacePrivate::TransferElement *table, int entries, int channels) { + if (table) { + for (int j = 0; j < channels; ++j) { + if (!table->trc[j].table().m_table16.isEmpty()) { + for (int i = 0; i < entries; ++i) + stream << table->trc[j].table().m_table16[i]; + } else { + for (int i = 0; i < entries; ++i) + stream << quint16(table->trc[j].table().m_table8[i] * 257); + } + } + } else { + for (int j = 0; j < channels; ++j) + stream << quint16(0) << quint16(65535); + } + }; + + writeTable(combo.inTable, inputEntries, inChannels); + + if (combo.clut) { + if (isAb && pcsLab) { + for (const QColorVector &v : combo.clut->table) { + stream << quint16(v.x * 65280.0f + 0.5f); + stream << quint16(v.y * 65280.0f + 0.5f); + stream << quint16(v.z * 65280.0f + 0.5f); + } + } else { + if (outChannels == 4) { + for (const QColorVector &v : combo.clut->table) { + stream << quint16(v.x * 65535.0f + 0.5f); + stream << quint16(v.y * 65535.0f + 0.5f); + stream << quint16(v.z * 65535.0f + 0.5f); + stream << quint16(v.w * 65535.0f + 0.5f); + } + } else { + for (const QColorVector &v : combo.clut->table) { + stream << quint16(v.x * 65535.0f + 0.5f); + stream << quint16(v.y * 65535.0f + 0.5f); + stream << quint16(v.z * 65535.0f + 0.5f); + } + } + } + } + + writeTable(combo.outTable, outputEntries, outChannels); + + qsizetype offset = sizeof(Lut16TagData) + 2 * inChannels * inputEntries + + 2 * outChannels * outputEntries + + 2 * outChannels * intPow(gridPointsLut16, inChannels); + if (offset & 0x2) { + stream << quint16(0); + offset += 2; + } + return offset; + } else { + // mAB/mBA tag: + if (isAb) { + if (!combo.clut && combo.inTable && combo.midMatrix && !combo.midTable) + std::swap(combo.inTable, combo.midTable); + } else { + if (!combo.clut && combo.outTable && combo.midMatrix && !combo.midTable) + std::swap(combo.outTable, combo.midTable); + } + quint32 offset = sizeof(mABTagData); + QBuffer buffer2; + buffer2.open(QIODevice::WriteOnly); + QDataStream stream2(&buffer2); + quint32 bOffset = offset; + quint32 matrixOffset = 0; + quint32 mOffset = 0; + quint32 clutOffset = 0; + quint32 aOffset = 0; + // Tags must start on 4 byte offsets, but sampled curves might have sizes 2-byte aligned + auto alignTag = [&]() { + if (offset & 0x2) { + stream2 << quint16(0); + offset += 2; + } + }; + + const QColorSpacePrivate::TransferElement *aCurve, *bCurve; + int aChannels; + if (isAb) { + aCurve = combo.inTable; + aChannels = inChannels; + bCurve = combo.outTable; + Q_ASSERT(outChannels == 3); + } else { + aCurve = combo.outTable; + aChannels = outChannels; + bCurve = combo.inTable; + Q_ASSERT(inChannels == 3); + } + if (bCurve) { + offset += writeColorTrc(stream2, bCurve->trc[0]); + alignTag(); + offset += writeColorTrc(stream2, bCurve->trc[1]); + alignTag(); + offset += writeColorTrc(stream2, bCurve->trc[2]); + alignTag(); + } else { + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + offset += 12 * 3; + } + if (combo.midMatrix || combo.midOffset || combo.midTable) { + matrixOffset = offset; + if (combo.midMatrix) { + stream2 << toFixedS1516(combo.midMatrix->r.x); + stream2 << toFixedS1516(combo.midMatrix->g.x); + stream2 << toFixedS1516(combo.midMatrix->b.x); + stream2 << toFixedS1516(combo.midMatrix->r.y); + stream2 << toFixedS1516(combo.midMatrix->g.y); + stream2 << toFixedS1516(combo.midMatrix->b.y); + stream2 << toFixedS1516(combo.midMatrix->r.z); + stream2 << toFixedS1516(combo.midMatrix->g.z); + stream2 << toFixedS1516(combo.midMatrix->b.z); + } else { + stream2 << toFixedS1516(1.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(1.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(1.0f); + } + if (combo.midOffset) { + stream2 << toFixedS1516(combo.midOffset->x); + stream2 << toFixedS1516(combo.midOffset->y); + stream2 << toFixedS1516(combo.midOffset->z); + } else { + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + stream2 << toFixedS1516(0.0f); + } + offset += 12 * 4; + mOffset = offset; + if (combo.midTable) { + offset += writeColorTrc(stream2, combo.midTable->trc[0]); + alignTag(); + offset += writeColorTrc(stream2, combo.midTable->trc[1]); + alignTag(); + offset += writeColorTrc(stream2, combo.midTable->trc[2]); + alignTag(); + } else { + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + offset += 12 * 3; + } + } + if (combo.clut || aCurve) { + clutOffset = offset; + if (combo.clut) { + stream2 << uchar(combo.clut->gridPointsX); + stream2 << uchar(combo.clut->gridPointsY); + stream2 << uchar(combo.clut->gridPointsZ); + if (inChannels == 4) + stream2 << uchar(combo.clut->gridPointsW); + else + stream2 << uchar(0); + for (int i = 0; i < 12; ++i) + stream2 << uchar(0); + stream2 << uchar(2) << uchar(0) << uchar(0) << uchar(0); + offset += 20; + if (outChannels == 4) { + for (const QColorVector &v : combo.clut->table) { + stream2 << quint16(v.x * 65535.0f + 0.5f); + stream2 << quint16(v.y * 65535.0f + 0.5f); + stream2 << quint16(v.z * 65535.0f + 0.5f); + stream2 << quint16(v.w * 65535.0f + 0.5f); + } + } else { + for (const QColorVector &v : combo.clut->table) { + stream2 << quint16(v.x * 65535.0f + 0.5f); + stream2 << quint16(v.y * 65535.0f + 0.5f); + stream2 << quint16(v.z * 65535.0f + 0.5f); + } + } + offset += 2 * outChannels * combo.clut->table.size(); + alignTag(); + } else { + for (int i = 0; i < 16; ++i) + stream2 << uchar(0); + stream2 << uchar(1) << uchar(0) << uchar(0) << uchar(0); + offset += 20; + } + aOffset = offset; + if (aCurve) { + offset += writeColorTrc(stream2, aCurve->trc[0]); + alignTag(); + offset += writeColorTrc(stream2, aCurve->trc[1]); + alignTag(); + offset += writeColorTrc(stream2, aCurve->trc[2]); + alignTag(); + if (aChannels == 4) { + offset += writeColorTrc(stream2, aCurve->trc[3]); + alignTag(); + } + } else { + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + stream2 << uint(Tag::curv) << uint(0) << uint(0); + if (aChannels == 4) + stream2 << uint(Tag::curv) << uint(0) << uint(0); + offset += 12 * aChannels; + } + } + buffer2.close(); + QByteArray tagData = buffer2.buffer(); + stream << quint32(bOffset); + stream << quint32(matrixOffset); + stream << quint32(mOffset); + stream << quint32(clutOffset); + stream << quint32(aOffset); + stream.writeRawData(tagData.data(), tagData.size()); + + return int(sizeof(mABTagData) + tagData.size()); + } +} + QByteArray toIccProfile(const QColorSpace &space) { if (!space.isValid()) return QByteArray(); const QColorSpacePrivate *spaceDPtr = QColorSpacePrivate::get(space); - // This should catch anything not three component matrix based as we can only get that from parsed ICC if (!spaceDPtr->iccProfile.isEmpty()) return spaceDPtr->iccProfile; - Q_ASSERT(spaceDPtr->isThreeComponentMatrix()); int fixedLengthTagCount = 5; + if (!spaceDPtr->isThreeComponentMatrix()) + fixedLengthTagCount = 2; + else if (spaceDPtr->colorModel == QColorSpace::ColorModel::Gray) + fixedLengthTagCount = 2; bool writeChad = false; - if (!spaceDPtr->whitePoint.isNull() && spaceDPtr->whitePoint != QColorVector::D50()) { + bool writeB2a = true; + if (spaceDPtr->isThreeComponentMatrix() && !spaceDPtr->chad.isIdentity()) { writeChad = true; fixedLengthTagCount++; } + int varLengthTagCount = 4; + if (!spaceDPtr->isThreeComponentMatrix()) + varLengthTagCount = 3; + else if (spaceDPtr->colorModel == QColorSpace::ColorModel::Gray) + varLengthTagCount = 2; - const int tagCount = fixedLengthTagCount + 4; + if (!space.isValidTarget()) { + writeB2a = false; + Q_ASSERT(!spaceDPtr->isThreeComponentMatrix()); + varLengthTagCount--; + } + + const int tagCount = fixedLengthTagCount + varLengthTagCount; const uint profileDataOffset = 128 + 4 + 12 * tagCount; - const uint variableTagTableOffsets = 128 + 4 + 12 * fixedLengthTagCount; + uint variableTagTableOffsets = 128 + 4 + 12 * fixedLengthTagCount; uint currentOffset = 0; - uint rTrcOffset, gTrcOffset, bTrcOffset; - uint rTrcSize, gTrcSize, bTrcSize; - uint descOffset, descSize; + uint rTrcOffset = 0; + uint gTrcOffset = 0; + uint bTrcOffset = 0; + uint kTrcOffset = 0; + uint rTrcSize = 0; + uint gTrcSize = 0; + uint bTrcSize = 0; + uint kTrcSize = 0; + uint descOffset = 0; + uint descSize = 0; + uint mA2bOffset = 0; + uint mB2aOffset = 0; + uint mA2bSize = 0; + uint mB2aSize = 0; QBuffer buffer; buffer.open(QIODevice::WriteOnly); @@ -403,13 +807,25 @@ QByteArray toIccProfile(const QColorSpace &space) stream << uint(0); stream << uint(0x04400000); // Version 4.4 stream << uint(ProfileClass::Display); - stream << uint(Tag::RGB_); + switch (spaceDPtr->colorModel) { + case QColorSpace::ColorModel::Rgb: + stream << uint(ColorSpaceType::Rgb); + break; + case QColorSpace::ColorModel::Gray: + stream << uint(ColorSpaceType::Gray); + break; + case QColorSpace::ColorModel::Cmyk: + stream << uint(ColorSpaceType::Cmyk); + break; + case QColorSpace::ColorModel::Undefined: + Q_UNREACHABLE(); + } stream << (spaceDPtr->isPcsLab ? uint(Tag::Lab_) : 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(0); // Rendering intent stream << uint(0x0000f6d6); // D50 X stream << uint(0x00010000); // D50 Y stream << uint(0x0000d32d); // D50 Z @@ -417,81 +833,132 @@ QByteArray toIccProfile(const QColorSpace &space) 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: currentOffset = profileDataOffset; - 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(34); - currentOffset += 20 + 20 + 20 + 20 + 34 + 2; - if (writeChad) { - stream << uint(Tag::chad) << uint(currentOffset) << uint(44); - currentOffset += 44; - } - // 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); - - // 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::mluc) << uint(0); - stream << uint(1) << uint(12); - stream << uchar('e') << uchar('n') << uchar('U') << uchar('S'); - stream << uint(6) << uint(28); - stream << ushort('N') << ushort('/') << ushort('A'); - stream << ushort(0); // 4-byte alignment - if (writeChad) { - QColorMatrix chad = QColorMatrix::chromaticAdaptation(spaceDPtr->whitePoint); - stream << uint(Tag::sf32) << uint(0); - stream << toFixedS1516(chad.r.x); - stream << toFixedS1516(chad.g.x); - stream << toFixedS1516(chad.b.x); - stream << toFixedS1516(chad.r.y); - stream << toFixedS1516(chad.g.y); - stream << toFixedS1516(chad.b.y); - stream << toFixedS1516(chad.r.z); - stream << toFixedS1516(chad.g.z); - stream << toFixedS1516(chad.b.z); - } - - // 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; + if (spaceDPtr->isThreeComponentMatrix()) { + // Tag table: + stream << uint(tagCount); + if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) { + stream << uint(Tag::rXYZ) << uint(currentOffset + 00) << uint(20); + stream << uint(Tag::gXYZ) << uint(currentOffset + 20) << uint(20); + stream << uint(Tag::bXYZ) << uint(currentOffset + 40) << uint(20); + currentOffset += 20 + 20 + 20; + } + stream << uint(Tag::wtpt) << uint(currentOffset + 00) << uint(20); + stream << uint(Tag::cprt) << uint(currentOffset + 20) << uint(34); + currentOffset += 20 + 34 + 2; + if (writeChad) { + stream << uint(Tag::chad) << uint(currentOffset) << uint(44); + currentOffset += 44; + } + // From here the offset and size will be updated later: + if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) { + stream << uint(Tag::rTRC) << uint(0) << uint(0); + stream << uint(Tag::gTRC) << uint(0) << uint(0); + stream << uint(Tag::bTRC) << uint(0) << uint(0); + } else { + stream << uint(Tag::kTRC) << uint(0) << uint(0); + } + stream << uint(Tag::desc) << uint(0) << uint(0); + + // Tag data: + if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) { + 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::mluc) << uint(0); + stream << uint(1) << uint(12); + stream << uchar('e') << uchar('n') << uchar('U') << uchar('S'); + stream << uint(6) << uint(28); + stream << ushort('N') << ushort('/') << ushort('A'); + stream << ushort(0); // 4-byte alignment + if (writeChad) { + const QColorMatrix &chad = spaceDPtr->chad; + stream << uint(Tag::sf32) << uint(0); + stream << toFixedS1516(chad.r.x); + stream << toFixedS1516(chad.g.x); + stream << toFixedS1516(chad.b.x); + stream << toFixedS1516(chad.r.y); + stream << toFixedS1516(chad.g.y); + stream << toFixedS1516(chad.b.y); + stream << toFixedS1516(chad.r.z); + stream << toFixedS1516(chad.g.z); + stream << toFixedS1516(chad.b.z); + } + + // From now on the data is variable sized: + if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) { + 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; + } + } else { + Q_ASSERT(spaceDPtr->colorModel == QColorSpace::ColorModel::Gray); + kTrcOffset = currentOffset; + kTrcSize = writeColorTrc(stream, spaceDPtr->trc[0]); + currentOffset += kTrcSize; + } } else { - bTrcOffset = currentOffset; - bTrcSize = writeColorTrc(stream, spaceDPtr->trc[2]); - currentOffset += bTrcSize; + // Tag table: + stream << uint(tagCount); + stream << uint(Tag::wtpt) << uint(profileDataOffset + 00) << uint(20); + stream << uint(Tag::cprt) << uint(profileDataOffset + 20) << uint(34); + currentOffset += 20 + 34 + 2; + // From here the offset and size will be updated later: + stream << uint(Tag::A2B0) << uint(0) << uint(0); + if (writeB2a) + stream << uint(Tag::B2A0) << uint(0) << uint(0); + stream << uint(Tag::desc) << uint(0) << uint(0); + + // Fixed tag data + stream << uint(Tag::XYZ_) << uint(0); + stream << toFixedS1516(spaceDPtr->whitePoint.x); + stream << toFixedS1516(spaceDPtr->whitePoint.y); + stream << toFixedS1516(spaceDPtr->whitePoint.z); + stream << uint(Tag::mluc) << uint(0); + stream << uint(1) << uint(12); + stream << uchar('e') << uchar('n') << uchar('U') << uchar('S'); + stream << uint(6) << uint(28); + stream << ushort('N') << ushort('/') << ushort('A'); + stream << ushort(0); // 4-byte alignment + + // From now on the data is variable sized: + mA2bOffset = currentOffset; + mA2bSize = writeMab(stream, spaceDPtr->mAB, true, spaceDPtr->isPcsLab, spaceDPtr->colorModel == QColorSpace::ColorModel::Cmyk); + currentOffset += mA2bSize; + if (writeB2a) { + mB2aOffset = currentOffset; + mB2aSize = writeMab(stream, spaceDPtr->mBA, false, spaceDPtr->isPcsLab, spaceDPtr->colorModel == QColorSpace::ColorModel::Cmyk); + currentOffset += mB2aSize; + } } // Writing description @@ -515,14 +982,34 @@ QByteArray toIccProfile(const QColorSpace &space) // 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 (spaceDPtr->isThreeComponentMatrix()) { + if (spaceDPtr->colorModel == QColorSpace::ColorModel::Rgb) { + *(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; + } else { + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = kTrcOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = kTrcSize; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 4) = descOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 12 + 8) = descSize; + } + } else { + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = mA2bOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = mA2bSize; + variableTagTableOffsets += 12; + if (writeB2a) { + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = mB2aOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = mB2aSize; + variableTagTableOffsets += 12; + } + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 4) = descOffset; + *(quint32_be *)(iccProfile.data() + variableTagTableOffsets + 8) = descSize; + } #if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) const ICCProfileHeader *iccHeader = (const ICCProfileHeader *)iccProfile.constData(); @@ -565,10 +1052,14 @@ static quint32 parseTRC(const QByteArrayView &tagData, QColorTrc &gamma, QColorT if (trcData.type == quint32(Tag::curv)) { Q_STATIC_ASSERT(sizeof(CurvTagData) == 12); const CurvTagData curv = qFromUnaligned<CurvTagData>(tagData.constData()); - if (curv.valueCount > (1 << 16)) + if (curv.valueCount > (1 << 16)) { + qCWarning(lcIcc) << "Invalid count in curv table"; return 0; - if (tagData.size() < qsizetype(12 + 2 * curv.valueCount)) + } + if (tagData.size() < qsizetype(12 + 2 * curv.valueCount)) { + qCWarning(lcIcc) << "Truncated curv table"; return 0; + } const auto valueOffset = sizeof(CurvTagData); if (curv.valueCount == 0) { gamma.m_type = QColorTrc::Type::Function; @@ -702,12 +1193,6 @@ static void parseCLUT(const T *tableData, const float f, QColorCLUT *clut, uchar } } -// very simple version for small values (<=4) of exp. -static constexpr qsizetype intPow(qsizetype x, qsizetype exp) -{ - return (exp <= 1) ? x : x * intPow(x, exp - 1); -} - // Parses lut8 and lut16 type elements template<typename T> static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColorSpacePrivate *colorSpacePrivate, bool isAb) @@ -763,20 +1248,23 @@ static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColo return false; } - if (lut.inputChannels != 3 && !(isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk && lut.inputChannels == 4)) { + const int inputChannels = (isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3; + const int outputChannels = (!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3; + + if (lut.inputChannels != inputChannels) { qCWarning(lcIcc) << "Unsupported lut8/lut16 input channel count" << lut.inputChannels; return false; } - if (lut.outputChannels != 3 && !(!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk && lut.outputChannels == 4)) { + if (lut.outputChannels != outputChannels) { qCWarning(lcIcc) << "Unsupported lut8/lut16 output channel count" << lut.outputChannels; return false; } - const qsizetype clutTableSize = intPow(lut.clutGridPoints, lut.inputChannels); - if (tagEntry.size < (sizeof(T) + precision * lut.inputChannels * inputTableEntries - + precision * lut.outputChannels * outputTableEntries - + precision * lut.outputChannels * clutTableSize)) { + const qsizetype clutTableSize = intPow(lut.clutGridPoints, inputChannels); + if (tagEntry.size < (sizeof(T) + precision * inputChannels * inputTableEntries + + precision * outputChannels * outputTableEntries + + precision * outputChannels * clutTableSize)) { qCWarning(lcIcc) << "Undersized lut8/lut16 tag, no room for tables"; return false; } @@ -787,7 +1275,7 @@ static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColo const uint8_t *tableData = reinterpret_cast<const uint8_t *>(data.constData() + tagEntry.offset + sizeof(T)); - for (int j = 0; j < lut.inputChannels; ++j) { + for (int j = 0; j < inputChannels; ++j) { QList<S> input(inputTableEntries); qFromBigEndian<S>(tableData, inputTableEntries, input.data()); QColorTransferTable table(inputTableEntries, input, QColorTransferTable::OneWay); @@ -803,22 +1291,22 @@ static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColo clutElement.table.resize(clutTableSize); clutElement.gridPointsX = clutElement.gridPointsY = clutElement.gridPointsZ = lut.clutGridPoints; - if (lut.inputChannels == 4) + if (inputChannels == 4) clutElement.gridPointsW = lut.clutGridPoints; if constexpr (std::is_same_v<T, Lut8TagData>) { - parseCLUT(tableData, 1.f / 255.f, &clutElement, lut.outputChannels); + parseCLUT(tableData, 1.f / 255.f, &clutElement, outputChannels); } else { float f = 1.0f / 65535.f; if (colorSpacePrivate->isPcsLab && isAb) // Legacy lut16 conversion to Lab f = 1.0f / 65280.f; - QList<S> clutTable(clutTableSize * lut.outputChannels); + QList<S> clutTable(clutTableSize * outputChannels); qFromBigEndian<S>(tableData, clutTable.size(), clutTable.data()); - parseCLUT(clutTable.constData(), f, &clutElement, lut.outputChannels); + parseCLUT(clutTable.constData(), f, &clutElement, outputChannels); } - tableData += clutTableSize * lut.outputChannels * precision; + tableData += clutTableSize * outputChannels * precision; - for (int j = 0; j < lut.outputChannels; ++j) { + for (int j = 0; j < outputChannels; ++j) { QList<S> output(outputTableEntries); qFromBigEndian<S>(tableData, outputTableEntries, output.data()); QColorTransferTable table(outputTableEntries, output, QColorTransferTable::OneWay); @@ -870,12 +1358,15 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo return false; } - if (mab.inputChannels != 3 && !(isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk && mab.inputChannels == 4)) { + const int inputChannels = (isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3; + const int outputChannels = (!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) ? 4 : 3; + + if (mab.inputChannels != inputChannels) { qCWarning(lcIcc) << "Unsupported mAB/mBA input channel count" << mab.inputChannels; return false; } - if (mab.outputChannels != 3 && !(!isAb && colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk && mab.outputChannels == 4)) { + if (mab.outputChannels != outputChannels) { qCWarning(lcIcc) << "Unsupported mAB/mBA output channel count" << mab.outputChannels; return false; } @@ -925,7 +1416,7 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo bool bCurvesAreLinear = true, aCurvesAreLinear = true, mCurvesAreLinear = true; // B Curves - if (!parseCurves(mab.bCurvesOffset, bTableElement.trc, isAb ? mab.outputChannels : mab.inputChannels)) { + if (!parseCurves(mab.bCurvesOffset, bTableElement.trc, isAb ? outputChannels : inputChannels)) { qCWarning(lcIcc) << "Invalid B curves"; return false; } else { @@ -934,7 +1425,7 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo // A Curves if (mab.aCurvesOffset) { - if (!parseCurves(mab.aCurvesOffset, aTableElement.trc, isAb ? mab.inputChannels : mab.outputChannels)) { + if (!parseCurves(mab.aCurvesOffset, aTableElement.trc, isAb ? inputChannels : outputChannels)) { qCWarning(lcIcc) << "Invalid A curves"; return false; } else { @@ -989,19 +1480,19 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo return false; } const qsizetype clutTableSize = clutElement.gridPointsX * clutElement.gridPointsY * clutElement.gridPointsZ * clutElement.gridPointsW; - if ((mab.clutOffset + 20 + clutTableSize * mab.outputChannels * precision) > tagEntry.size) { + if ((mab.clutOffset + 20 + clutTableSize * outputChannels * precision) > tagEntry.size) { qCWarning(lcIcc) << "CLUT oversized for tag"; return false; } clutElement.table.resize(clutTableSize); if (precision == 2) { - QList<uint16_t> clutTable(clutTableSize * mab.outputChannels); + QList<uint16_t> clutTable(clutTableSize * outputChannels); qFromBigEndian<uint16_t>(data.constData() + tagEntry.offset + mab.clutOffset + 20, clutTable.size(), clutTable.data()); - parseCLUT(clutTable.constData(), (1.f/65535.f), &clutElement, mab.outputChannels); + parseCLUT(clutTable.constData(), (1.f/65535.f), &clutElement, outputChannels); } else { const uint8_t *clutTable = reinterpret_cast<const uint8_t *>(data.constData() + tagEntry.offset + mab.clutOffset + 20); - parseCLUT(clutTable, (1.f/255.f), &clutElement, mab.outputChannels); + parseCLUT(clutTable, (1.f/255.f), &clutElement, outputChannels); } } else if (colorSpacePrivate->colorModel == QColorSpace::ColorModel::Cmyk) { qCWarning(lcIcc) << "Cmyk conversion must have a CLUT"; @@ -1015,7 +1506,7 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo if (!clutElement.isEmpty()) colorSpacePrivate->mAB.append(std::move(clutElement)); } - if (mab.mCurvesOffset && mab.outputChannels == 3) { + if (mab.mCurvesOffset && outputChannels == 3) { if (!mCurvesAreLinear) colorSpacePrivate->mAB.append(std::move(mTableElement)); if (!matrixElement.isIdentity()) @@ -1028,7 +1519,7 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo } else { if (!bCurvesAreLinear) colorSpacePrivate->mBA.append(std::move(bTableElement)); - if (mab.mCurvesOffset && mab.inputChannels == 3) { + if (mab.mCurvesOffset && inputChannels == 3) { if (!matrixElement.isIdentity()) colorSpacePrivate->mBA.append(std::move(matrixElement)); if (!offsetElement.isNull()) diff --git a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp index 48f792b9ede..67480c721d7 100644 --- a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp +++ b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp @@ -254,6 +254,15 @@ void tst_QColorSpace::fromIccProfile() QCOMPARE(iccProfile, iccProfile2); QColorSpace fileColorSpace2 = QColorSpace::fromIccProfile(iccProfile2); QCOMPARE(fileColorSpace2, fileColorSpace); + + // Change description to force generation of new icc profile data. + fileColorSpace2.setDescription("Hello my QTest description"); + iccProfile2 = fileColorSpace2.iccProfile(); + QCOMPARE_NE(iccProfile, iccProfile2); + fileColorSpace2 = QColorSpace::fromIccProfile(iccProfile2); + QVERIFY(fileColorSpace2.isValid()); + // Note, we do not currently compare description in color space equality + QCOMPARE(fileColorSpace2, fileColorSpace); } void tst_QColorSpace::imageConversion_data() diff --git a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp index 3b9e8434057..e94cd095f9d 100644 --- a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp +++ b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp @@ -41,8 +41,10 @@ extern "C" int LLVMFuzzerTestOneInput(const char *data, size_t size) { trans1.isIdentity(); QColorSpace cs2 = cs; cs2.setDescription("Hello"); - bool b = (cs == cs2); - Q_UNUSED(b); + Q_ASSERT(cs == cs2); + QByteArray profileData = cs2.iccProfile(); + QColorSpace cs3 = QColorSpace::fromIccProfile(profileData); + Q_ASSERT(cs == cs3); QColor color(0xfaf8fa00); color = trans1.map(color); QImage img(16, 2, toFormat(cs.colorModel())); |