PDF4QT/Pdf4QtLib/sources/pdfcms.cpp

1948 lines
73 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (C) 2019-2021 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#include "pdfcms.h"
#include "pdfdocument.h"
#include "pdfexecutionpolicy.h"
#include <QApplication>
#include <QReadWriteLock>
#pragma warning(push)
#pragma warning(disable:5033)
#define CMS_NO_REGISTER_KEYWORD
#include <lcms2.h>
#include <lcms2_plugin.h>
#pragma warning(pop)
#ifdef Q_OS_WIN
#define NOMINMAX
#include <Windows.h>
#include <Icm.h>
#pragma comment(lib, "Mscms")
#endif
#include <unordered_map>
namespace pdf
{
class PDFLittleCMS : public PDFCMS
{
public:
explicit PDFLittleCMS(const PDFCMSManager* manager, const PDFCMSSettings& settings);
virtual ~PDFLittleCMS() override;
virtual bool isCompatible(const PDFCMSSettings& settings) const override;
virtual QColor getPaperColor() const override;
virtual QColor getColorFromDeviceGray(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromDeviceRGB(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromDeviceCMYK(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromXYZ(const PDFColor3& whitePoint, const PDFColor3& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const override;
virtual QColor getColorFromICC(const PDFColor& color, RenderingIntent renderingIntent, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceGray(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceRGB(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromDeviceCMYK(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromXYZ(const PDFColor3& whitePoint, const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override;
virtual bool fillRGBBufferFromICC(const std::vector<float>& colors, RenderingIntent renderingIntent, unsigned char* outputBuffer, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const override;
virtual bool transformColorSpace(const ColorSpaceTransformParams& params) const override;
private:
void init();
static int installCmsPlugins();
static cmsBool optimizePipeline(cmsPipeline** Lut,
cmsUInt32Number Intent,
cmsUInt32Number* InputFormat,
cmsUInt32Number* OutputFormat,
cmsUInt32Number* dwFlags);
enum Profile
{
Output,
Gray,
RGB,
CMYK,
XYZ,
SoftProofing,
ProfileCount
};
/// Returns true, if we are doing soft-proofing
bool isSoftProofing() const;
/// Creates a profile using provided id and a list of profile descriptors.
/// If profile can't be created, then null handle is returned. If \p preferOutputProfile
/// is set to true, and given profile is not output profile, then first output profile
/// is being selected.
/// \param id Id of color profile
/// \param profileDescriptors Profile descriptor list
/// \param preferOutputProfile
cmsHPROFILE createProfile(const QString& id, const PDFColorProfileIdentifiers& profileDescriptors, bool preferOutputProfile) const;
/// Gets transform from cache. If transform doesn't exist, then it is created.
/// \param profile Color profile
/// \param intent Rendering intent
/// \param isRGB888Buffer If true, 8-bit RGB output buffer is used, otherwise FLOAT RGB output buffer is used
cmsHTRANSFORM getTransform(Profile profile, RenderingIntent intent, bool isRGB888Buffer) const;
/// Gets transform for ICC profile from cache. If transform doesn't exist, then it is created.
/// \param iccData Data of icc profile
/// \param iccID Icc profile id
/// \param renderingIntent Rendering intent
/// \param isRGB888Buffer If true, 8-bit RGB output buffer is used, otherwise FLOAT RGB output buffer is used
cmsHTRANSFORM getTransformFromICCProfile(const QByteArray& iccData, const QByteArray& iccID, RenderingIntent renderingIntent, bool isRGB888Buffer) const;
/// Returns transformation flags according to the current settings
cmsUInt32Number getTransformationFlags() const;
/// Calculates effective rendering intent. If rendering intent is auto,
/// then \p intent is used, otherwise intent is overriden.
RenderingIntent getEffectiveRenderingIntent(RenderingIntent intent) const;
/// Gets transform from cache key.
/// \param profile Color profile
/// \param intent Rendering intent
/// \param isRGB888Buffer If true, 8-bit RGB output buffer is used, otherwise FLOAT RGB output buffer is used
static constexpr int getCacheKey(Profile profile, RenderingIntent intent, bool isRGB888Buffer) { return ((int(intent) * ProfileCount + profile) << 1) + (isRGB888Buffer ? 1 : 0); }
/// Returns little CMS rendering intent
/// \param intent Rendering intent
static cmsUInt32Number getLittleCMSRenderingIntent(RenderingIntent intent);
/// Returns little CMS data format for profile
/// \param profile Color profile handle
static cmsUInt32Number getProfileDataFormat(cmsHPROFILE profile);
/// Returns color from output color. Clamps invalid rgb output values to range [0.0, 1.0].
/// \param color01 Rgb color (range 0-1 is assumed).
static QColor getColorFromOutputColor(std::array<float, 3> color01);
/// Returns transform key for transformation between various color spaces
static QByteArray getTransformColorSpaceKey(const ColorSpaceTransformParams& params);
cmsHTRANSFORM getTransformBetweenColorSpaces(const ColorSpaceTransformParams& params) const;
const PDFCMSManager* m_manager;
PDFCMSSettings m_settings;
QColor m_paperColor;
std::array<cmsHPROFILE, ProfileCount> m_profiles;
mutable QReadWriteLock m_transformationCacheLock;
mutable std::unordered_map<int, cmsHTRANSFORM> m_transformationCache;
mutable QReadWriteLock m_customIccProfileCacheLock;
mutable std::map<std::pair<QByteArray, RenderingIntent>, cmsHTRANSFORM> m_customIccProfileCache;
mutable QReadWriteLock m_transformColorSpaceCacheLock;
mutable std::map<QByteArray, cmsHTRANSFORM> m_transformColorSpaceCache;
};
bool PDFLittleCMS::fillRGBBufferFromDeviceGray(const std::vector<float>& colors,
RenderingIntent intent,
unsigned char* outputBuffer,
PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(Gray, getEffectiveRenderingIntent(intent), true);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from gray to output device using CMS failed."));
return false;
}
if (cmsGetTransformInputFormat(transform) == TYPE_GRAY_FLT)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_8);
cmsDoTransform(transform, colors.data(), outputBuffer, static_cast<cmsUInt32Number>(colors.size()));
return true;
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from gray to output device using CMS failed - invalid data format."));
}
return false;
}
bool PDFLittleCMS::fillRGBBufferFromDeviceRGB(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(RGB, getEffectiveRenderingIntent(intent), true);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from RGB to output device using CMS failed."));
return false;
}
const cmsUInt32Number inputFormat = cmsGetTransformInputFormat(transform);
if (inputFormat == TYPE_RGB_FLT && (colors.size()) % T_CHANNELS(inputFormat) == 0)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_8);
cmsDoTransform(transform, colors.data(), outputBuffer, static_cast<cmsUInt32Number>(colors.size()) / T_CHANNELS(inputFormat));
return true;
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from RGB to output device using CMS failed - invalid data format."));
}
return false;
}
bool PDFLittleCMS::fillRGBBufferFromDeviceCMYK(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(CMYK, getEffectiveRenderingIntent(intent), true);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from CMYK to output device using CMS failed."));
return false;
}
const cmsUInt32Number inputFormat = cmsGetTransformInputFormat(transform);
if (inputFormat == TYPE_CMYK_FLT && (colors.size()) % T_CHANNELS(inputFormat) == 0)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_8);
std::vector<float> fixedColors = colors;
for (size_t i = 0, count = fixedColors.size(); i < count; ++i)
{
fixedColors[i] = fixedColors[i] * 100.0f;
}
cmsDoTransform(transform, fixedColors.data(), outputBuffer, static_cast<cmsUInt32Number>(colors.size()) / T_CHANNELS(inputFormat));
return true;
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from CMYK to output device using CMS failed - invalid data format."));
}
return false;
}
bool PDFLittleCMS::fillRGBBufferFromXYZ(const PDFColor3& whitePoint, const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(XYZ, getEffectiveRenderingIntent(intent), true);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from XYZ to output device using CMS failed."));
return false;
}
const cmsUInt32Number inputFormat = cmsGetTransformInputFormat(transform);
if (inputFormat == TYPE_XYZ_FLT && (colors.size()) % T_CHANNELS(inputFormat) == 0)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_8);
PDFColorComponentMatrix_3x3 adaptationMatrix = PDFChromaticAdaptationXYZ::createWhitepointChromaticAdaptation(getDefaultXYZWhitepoint(), whitePoint, m_settings.colorAdaptationXYZ);
std::vector<float> fixedColors = colors;
const size_t count = fixedColors.size() / 3;
for (size_t i = 0; i < count; ++i)
{
const size_t indexX = i * 3;
const size_t indexY = i * 3 + 1;
const size_t indexZ = i * 3 + 2;
const PDFColor3 sourceXYZ = { fixedColors[indexX], fixedColors[indexY], fixedColors[indexZ] };
const PDFColor3 adaptedXYZ = adaptationMatrix * sourceXYZ;
fixedColors[indexX] = adaptedXYZ[0];
fixedColors[indexY] = adaptedXYZ[1];
fixedColors[indexZ] = adaptedXYZ[2];
}
cmsDoTransform(transform, fixedColors.data(), outputBuffer, static_cast<cmsUInt32Number>(colors.size()) / T_CHANNELS(inputFormat));
return true;
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from XYZ to output device using CMS failed - invalid data format."));
}
return false;
}
bool PDFLittleCMS::fillRGBBufferFromICC(const std::vector<float>& colors, RenderingIntent renderingIntent, unsigned char* outputBuffer, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransformFromICCProfile(iccData, iccID, renderingIntent, true);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from icc profile space to output device using CMS failed."));
return false;
}
const cmsUInt32Number format = cmsGetTransformInputFormat(transform);
const cmsUInt32Number channels = T_CHANNELS(format);
const cmsUInt32Number colorSpace = T_COLORSPACE(format);
const bool isCMYK = colorSpace == PT_CMYK;
const float* inputColors = colors.data();
std::vector<float> cmykColors;
if (isCMYK)
{
cmykColors = colors;
for (size_t i = 0; i < cmykColors.size(); ++i)
{
cmykColors[i] = cmykColors[i] * 100.0f;
}
inputColors = cmykColors.data();
}
if (colors.size() % channels == 0)
{
const cmsUInt32Number pixels = static_cast<cmsUInt32Number>(colors.size()) / channels;
cmsDoTransform(transform, inputColors, outputBuffer, pixels);
return true;
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from icc profile space to output device using CMS failed - invalid data format."));
}
return false;
}
bool PDFLittleCMS::transformColorSpace(const PDFCMS::ColorSpaceTransformParams& params) const
{
PDFCMS::ColorSpaceTransformParams transformedParams = params;
transformedParams.intent = getEffectiveRenderingIntent(transformedParams.intent);
cmsHTRANSFORM transform = getTransformBetweenColorSpaces(transformedParams);
if (!transform)
{
return false;
}
const cmsUInt32Number inputProfileFormat = cmsGetTransformInputFormat(transform);
const cmsUInt32Number inputChannels = T_CHANNELS(inputProfileFormat);
const cmsUInt32Number inputColorSpace = T_COLORSPACE(inputProfileFormat);
const bool isInputCMYK = inputColorSpace == PT_CMYK;
const float* inputColors = params.input.begin();
std::vector<float> cmykColors;
const cmsUInt32Number outputProfileFormat = cmsGetTransformOutputFormat(transform);
const cmsUInt32Number outputChannels = T_CHANNELS(outputProfileFormat);
const cmsUInt32Number outputColorSpace = T_COLORSPACE(outputProfileFormat);
const bool isOutputCMYK = outputColorSpace == PT_CMYK;
if (isInputCMYK)
{
cmykColors = std::vector<float>(params.input.cbegin(), params.input.cend());
for (size_t i = 0; i < cmykColors.size(); ++i)
{
cmykColors[i] = cmykColors[i] * 100.0f;
}
inputColors = cmykColors.data();
}
const cmsUInt32Number inputPixelCount = static_cast<cmsUInt32Number>(params.input.size()) / inputChannels;
const cmsUInt32Number outputPixelCount = static_cast<cmsUInt32Number>(params.output.size()) / outputChannels;
if (params.input.size() % inputChannels == 0 &&
params.output.size() % outputChannels == 0 &&
inputPixelCount == outputPixelCount)
{
PDFColorBuffer outputBuffer = params.output;
if (inputPixelCount > params.multithreadingThreshold)
{
struct TransformInfo
{
inline TransformInfo(const float* source, float* target, cmsUInt32Number pixelCount) :
source(source),
target(target),
pixelCount(pixelCount)
{
}
const float* source = nullptr;
float* target = nullptr;
cmsUInt32Number pixelCount = 0;
};
const cmsUInt32Number blockSize = 4096;
std::vector<TransformInfo> infos;
infos.reserve(inputPixelCount / blockSize + 1);
const float* sourcePointer = inputColors;
float* targetPointer = outputBuffer.begin();
cmsUInt32Number remainingPixelCount = inputPixelCount;
while (remainingPixelCount > 0)
{
const cmsUInt32Number currentPixelCount = qMin(blockSize, remainingPixelCount);
infos.emplace_back(sourcePointer, targetPointer, currentPixelCount);
sourcePointer += currentPixelCount * inputChannels;
targetPointer += currentPixelCount * outputChannels;
remainingPixelCount -= currentPixelCount;
}
auto processEntry = [transform](const TransformInfo& info)
{
cmsDoTransform(transform, info.source, info.target, info.pixelCount);
};
PDFExecutionPolicy::execute(PDFExecutionPolicy::Scope::Content, infos.begin(), infos.end(), processEntry);
}
else
{
// Single-threaded transform
cmsDoTransform(transform, inputColors, outputBuffer.begin(), inputPixelCount);
}
if (isOutputCMYK)
{
const PDFColorComponent colorQuotient = 1.0f / 100.0f;
for (PDFColorComponent& color : outputBuffer)
{
color *= colorQuotient;
}
}
return true;
}
else
{
return false;
}
return false;
}
PDFLittleCMS::PDFLittleCMS(const PDFCMSManager* manager, const PDFCMSSettings& settings) :
m_manager(manager),
m_settings(settings),
m_paperColor(Qt::white),
m_profiles()
{
static const int installed = installCmsPlugins();
Q_UNUSED(installed);
init();
}
PDFLittleCMS::~PDFLittleCMS()
{
for (const auto& transformItem : m_transformationCache)
{
cmsHTRANSFORM transform = transformItem.second;
if (transform)
{
cmsDeleteTransform(transform);
}
}
for (const auto& transformItem : m_customIccProfileCache)
{
cmsHTRANSFORM transform = transformItem.second;
if (transform)
{
cmsDeleteTransform(transform);
}
}
for (const auto& transformItem : m_transformColorSpaceCache)
{
cmsHTRANSFORM transform = transformItem.second;
if (transform)
{
cmsDeleteTransform(transform);
}
}
for (cmsHPROFILE profile : m_profiles)
{
if (profile)
{
cmsCloseProfile(profile);
}
}
}
bool PDFLittleCMS::isCompatible(const PDFCMSSettings& settings) const
{
return m_settings == settings;
}
QColor PDFLittleCMS::getPaperColor() const
{
return m_paperColor;
}
QColor PDFLittleCMS::getColorFromDeviceGray(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(Gray, getEffectiveRenderingIntent(intent), false);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from gray to output device using CMS failed."));
return QColor();
}
if (cmsGetTransformInputFormat(transform) == TYPE_GRAY_FLT && color.size() == 1)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
const float grayColor = color[0];
std::array<float, 3> rgbOutputColor = { };
cmsDoTransform(transform, &grayColor, rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from gray to output device using CMS failed - invalid data format."));
}
return QColor();
}
QColor PDFLittleCMS::getColorFromDeviceRGB(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(RGB, getEffectiveRenderingIntent(intent), false);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from RGB to output device using CMS failed."));
return QColor();
}
if (cmsGetTransformInputFormat(transform) == TYPE_RGB_FLT && color.size() == 3)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
std::array<float, 3> rgbInputColor = { color[0], color[1], color[2] };
std::array<float, 3> rgbOutputColor = { };
cmsDoTransform(transform, rgbInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from RGB to output device using CMS failed - invalid data format."));
}
return QColor();
}
QColor PDFLittleCMS::getColorFromDeviceCMYK(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(CMYK, getEffectiveRenderingIntent(intent), false);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from CMYK to output device using CMS failed."));
return QColor();
}
if (cmsGetTransformInputFormat(transform) == TYPE_CMYK_FLT && color.size() == 4)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
std::array<float, 4> cmykInputColor = { color[0] * 100.0f, color[1] * 100.0f, color[2] * 100.0f, color[3] * 100.0f };
std::array<float, 3> rgbOutputColor = { };
cmsDoTransform(transform, cmykInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from CMYK to output device using CMS failed - invalid data format."));
}
return QColor();
}
QColor PDFLittleCMS::getColorFromXYZ(const PDFColor3& whitePoint, const PDFColor3& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransform(XYZ, getEffectiveRenderingIntent(intent), false);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from XYZ to output device using CMS failed."));
return QColor();
}
if (cmsGetTransformInputFormat(transform) == TYPE_XYZ_FLT && color.size() == 3)
{
Q_ASSERT(cmsGetTransformOutputFormat(transform) == TYPE_RGB_FLT);
const PDFColorComponentMatrix_3x3 adaptationMatrix = PDFChromaticAdaptationXYZ::createWhitepointChromaticAdaptation(getDefaultXYZWhitepoint(), whitePoint, m_settings.colorAdaptationXYZ);
const PDFColor3 xyzInputColor = adaptationMatrix * color;
std::array<float, 3> rgbOutputColor = { };
cmsDoTransform(transform, xyzInputColor.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from XYZ to output device using CMS failed - invalid data format."));
}
return QColor();
}
cmsHTRANSFORM PDFLittleCMS::getTransformFromICCProfile(const QByteArray& iccData, const QByteArray& iccID, RenderingIntent renderingIntent, bool isRGB888Buffer) const
{
RenderingIntent effectiveRenderingIntent = getEffectiveRenderingIntent(renderingIntent);
const auto key = std::make_pair(iccID + (isRGB888Buffer ? "RGB_888" : "FLT"), effectiveRenderingIntent);
QReadLocker lock(&m_customIccProfileCacheLock);
auto it = m_customIccProfileCache.find(key);
if (it == m_customIccProfileCache.cend())
{
lock.unlock();
QWriteLocker writeLock(&m_customIccProfileCacheLock);
// Now, we have locked cache for writing. We must find out,
// if some other thread doesn't created the transformation already.
it = m_customIccProfileCache.find(key);
if (it == m_customIccProfileCache.cend())
{
cmsHTRANSFORM transform = cmsHTRANSFORM();
cmsHPROFILE profile = cmsOpenProfileFromMem(iccData.data(), iccData.size());
if (profile)
{
if (const cmsUInt32Number inputDataFormat = getProfileDataFormat(profile))
{
cmsUInt32Number lcmsIntent = getLittleCMSRenderingIntent(effectiveRenderingIntent);
if (isSoftProofing())
{
cmsHPROFILE proofingProfile = m_profiles[SoftProofing];
RenderingIntent proofingIntent = m_settings.proofingIntent;
if (m_settings.proofingIntent == RenderingIntent::Auto)
{
proofingIntent = effectiveRenderingIntent;
}
transform = cmsCreateProofingTransform(profile, inputDataFormat, m_profiles[Output], isRGB888Buffer ? TYPE_RGB_8 : TYPE_RGB_FLT, proofingProfile,
lcmsIntent, getLittleCMSRenderingIntent(proofingIntent), getTransformationFlags());
}
else
{
transform = cmsCreateTransform(profile, inputDataFormat, m_profiles[Output], isRGB888Buffer ? TYPE_RGB_8 : TYPE_RGB_FLT, lcmsIntent, getTransformationFlags());
}
}
cmsCloseProfile(profile);
}
it = m_customIccProfileCache.insert(std::make_pair(key, transform)).first;
}
return it->second;
}
else
{
return it->second;
}
return cmsHTRANSFORM();
}
QColor PDFLittleCMS::getColorFromICC(const PDFColor& color, RenderingIntent renderingIntent, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const
{
cmsHTRANSFORM transform = getTransformFromICCProfile(iccData, iccID, renderingIntent, false);
if (!transform)
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from icc profile space to output device using CMS failed."));
return QColor();
}
std::array<float, 4> inputBuffer = { };
const cmsUInt32Number format = cmsGetTransformInputFormat(transform);
const cmsUInt32Number channels = T_CHANNELS(format);
const cmsUInt32Number colorSpace = T_COLORSPACE(format);
const bool isCMYK = colorSpace == PT_CMYK;
if (channels == color.size() && channels <= inputBuffer.size())
{
for (size_t i = 0; i < color.size(); ++i)
{
inputBuffer[i] = isCMYK ? color[i] * 100.0f : color[i];
}
std::array<float, 3> rgbOutputColor = { };
cmsDoTransform(transform, inputBuffer.data(), rgbOutputColor.data(), 1);
return getColorFromOutputColor(rgbOutputColor);
}
else
{
reporter->reportRenderErrorOnce(RenderErrorType::Error, PDFTranslationContext::tr("Conversion from icc profile space to output device using CMS failed - invalid data format."));
}
return QColor();
}
void PDFLittleCMS::init()
{
// Jakub Melka: initialize all color profiles
m_profiles[Output] = createProfile(m_settings.outputCS, m_manager->getOutputProfiles(), false);
m_profiles[Gray] = createProfile(m_settings.deviceGray, m_manager->getGrayProfiles(), m_settings.isConsiderOutputIntent);
m_profiles[RGB] = createProfile(m_settings.deviceRGB, m_manager->getRGBProfiles(), m_settings.isConsiderOutputIntent);
m_profiles[CMYK] = createProfile(m_settings.deviceCMYK, m_manager->getCMYKProfiles(), m_settings.isConsiderOutputIntent);
m_profiles[SoftProofing] = createProfile(m_settings.softProofingProfile, m_manager->getCMYKProfiles(), false);
m_profiles[XYZ] = cmsCreateXYZProfile();
cmsUInt16Number outOfGamutR = m_settings.outOfGamutColor.redF() * 0xFFFF;
cmsUInt16Number outOfGamutG = m_settings.outOfGamutColor.greenF() * 0xFFFF;
cmsUInt16Number outOfGamutB = m_settings.outOfGamutColor.blueF() * 0xFFFF;
cmsUInt16Number alarmCodes[cmsMAXCHANNELS] = { outOfGamutR, outOfGamutG, outOfGamutB };
cmsSetAlarmCodes(alarmCodes);
if (m_settings.isWhitePaperColorTransformed)
{
m_paperColor = getColorFromDeviceRGB(PDFColor(1.0f, 1.0f, 1.0f), RenderingIntent::AbsoluteColorimetric, nullptr);
// We must check color of the paper, it can be invalid, if error occurs...
if (!m_paperColor.isValid())
{
m_paperColor = QColor(Qt::white);
}
}
// 64 should be enough, because we can have 4 input color spaces (gray, RGB, CMYK and XYZ),
// and 4 rendering intents. We have 4 * 4 = 16 input tables, so 64 will suffice enough
// (because we then have 25% load factor).
m_transformationCache.reserve(64);
}
int PDFLittleCMS::installCmsPlugins()
{
static cmsPluginOptimization optimizationPlugin = { };
optimizationPlugin.base.Magic = cmsPluginMagicNumber;
optimizationPlugin.base.Type = cmsPluginOptimizationSig;
optimizationPlugin.base.Next = nullptr;
optimizationPlugin.base.ExpectedVersion = LCMS_VERSION;
optimizationPlugin.OptimizePtr = &PDFLittleCMS::optimizePipeline;
cmsPlugin(&optimizationPlugin);
return 0;
}
cmsBool PDFLittleCMS::optimizePipeline(cmsPipeline** Lut, cmsUInt32Number Intent, cmsUInt32Number* InputFormat, cmsUInt32Number* OutputFormat, cmsUInt32Number* dwFlags)
{
if (!(*dwFlags & cmsFLAGS_LOWRESPRECALC))
{
// Optimize only on low resolution precalculation
return FALSE;
}
Q_UNUSED(Intent);
// We will find, if we can optimize...
bool shouldOptimize = false;
for (auto stage = cmsPipelineGetPtrToFirstStage(*Lut); stage; stage = cmsStageNext(stage))
{
if (cmsStageType(stage) == cmsSigCurveSetElemType)
{
_cmsStageToneCurvesData* data = reinterpret_cast<_cmsStageToneCurvesData*>(cmsStageData(stage));
for (cmsUInt32Number i = 0; i < data->nCurves; ++i)
{
const cmsToneCurve* curve = data->TheCurves[i];
const cmsInt32Number type = cmsGetToneCurveParametricType(curve);
if (type != 0 && !cmsIsToneCurveMultisegment(curve))
{
shouldOptimize = true;
}
}
}
}
if (shouldOptimize)
{
cmsContext contextId = cmsGetPipelineContextID(*Lut);
cmsPipeline* pipeline = cmsPipelineAlloc(contextId, T_CHANNELS(*InputFormat), T_CHANNELS(*OutputFormat));
if (!pipeline)
{
return FALSE;
}
for (auto stage = cmsPipelineGetPtrToFirstStage(*Lut); stage; stage = cmsStageNext(stage))
{
if (cmsStageType(stage) == cmsSigCurveSetElemType)
{
_cmsStageToneCurvesData* data = reinterpret_cast<_cmsStageToneCurvesData*>(cmsStageData(stage));
std::vector<cmsToneCurve*> curves(data->nCurves, nullptr);
for (cmsUInt32Number i = 0; i < data->nCurves; ++i)
{
const cmsToneCurve* curve = data->TheCurves[i];
const cmsInt32Number type = cmsGetToneCurveParametricType(curve);
if (type != 0 && !cmsIsToneCurveMultisegment(curve))
{
std::array<cmsCurveSegment, 3> segments = { };
const cmsFloat64Number* params = cmsGetToneCurveParams(curve);
cmsCurveSegment& s1 = segments[0];
cmsCurveSegment& s2 = segments[1];
cmsCurveSegment& s3 = segments[2];
const cmsFloat32Number eps = cmsFloat32Number(1e-5);
const cmsFloat32Number low = 0.0f - eps;
const cmsFloat32Number high = 1.0f + eps;
s1.Type = type;
s1.nGridPoints = 0;
s1.SampledPoints = nullptr;
std::copy(params, params + (sizeof(s1.Params) / sizeof(*s1.Params)), s1.Params);
s1.x0 = std::numeric_limits<cmsFloat32Number>::min();
s1.x1 = low;
const cmsUInt32Number gridPoints = 1024;
const cmsFloat32Number factor = 1.0f / cmsFloat32Number(gridPoints - 1);
s2.Type = 0;
s2.nGridPoints = gridPoints;
s2.SampledPoints = static_cast<cmsFloat32Number*>(_cmsCalloc(contextId, gridPoints, sizeof(*s2.SampledPoints)));
std::copy(params, params + (sizeof(s2.Params) / sizeof(*s2.Params)), s2.Params);
s2.x0 = low;
s2.x1 = high;
for (cmsUInt32Number i = 0; i < gridPoints; ++i)
{
const cmsFloat32Number x = i * factor;
s2.SampledPoints[i] = cmsEvalToneCurveFloat(curve, interpolate(x, 0.0, 1.0, low, high));
}
s3.Type = type;
s3.nGridPoints = 0;
s3.SampledPoints = nullptr;
std::copy(params, params + (sizeof(s3.Params) / sizeof(*s3.Params)), s3.Params);
s3.x0 = high;
s3.x1 = std::numeric_limits<cmsFloat32Number>::max();
curves[i] = cmsBuildSegmentedToneCurve(contextId, cmsUInt32Number(segments.size()), segments.data());
_cmsFree(contextId, s2.SampledPoints);
}
else
{
curves[i] = cmsDupToneCurve(curve);
}
}
cmsStageAllocToneCurves(contextId, cmsFloat32Number(curves.size()), curves.data());
for (cmsToneCurve* curve : curves)
{
cmsFreeToneCurve(curve);
}
}
else
{
cmsPipelineInsertStage(pipeline, cmsAT_END, cmsStageDup(stage));
}
}
cmsPipelineFree(*Lut);
*Lut = pipeline;
}
return FALSE;
}
bool PDFLittleCMS::isSoftProofing() const
{
return (m_settings.isSoftProofing || m_settings.isGamutChecking) && m_profiles[SoftProofing];
}
cmsHPROFILE PDFLittleCMS::createProfile(const QString& id, const PDFColorProfileIdentifiers& profileDescriptors, bool preferOutputProfile) const
{
auto it = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [&id](const PDFColorProfileIdentifier& identifier) { return identifier.id == id; });
if (preferOutputProfile && it != profileDescriptors.end())
{
const PDFColorProfileIdentifier& identifier = *it;
if (!identifier.isOutputIntentProfile)
{
// Find first output intent color profile
auto itOutputIntentColorProfile = std::find_if(profileDescriptors.cbegin(), profileDescriptors.cend(), [](const PDFColorProfileIdentifier& identifier) { return identifier.isOutputIntentProfile; });
if (itOutputIntentColorProfile != profileDescriptors.end())
{
it = itOutputIntentColorProfile;
}
}
}
if (it != profileDescriptors.cend())
{
const PDFColorProfileIdentifier& identifier = *it;
switch (identifier.type)
{
case PDFColorProfileIdentifier::Type::Gray:
{
cmsCIExyY whitePoint{ };
if (cmsWhitePointFromTemp(&whitePoint, identifier.temperature))
{
cmsToneCurve* gammaCurve = cmsBuildGamma(cmsContext(), identifier.gamma);
cmsHPROFILE profile = cmsCreateGrayProfile(&whitePoint, gammaCurve);
cmsFreeToneCurve(gammaCurve);
return profile;
}
break;
}
case PDFColorProfileIdentifier::Type::sRGB:
return cmsCreate_sRGBProfile();
case PDFColorProfileIdentifier::Type::RGB:
{
cmsCIExyY whitePoint{ };
if (cmsWhitePointFromTemp(&whitePoint, identifier.temperature))
{
cmsCIExyYTRIPLE primaries;
primaries.Red = { identifier.primaryR.x(), identifier.primaryR.y(), 1.0 };
primaries.Green = { identifier.primaryG.x(), identifier.primaryG.y(), 1.0 };
primaries.Blue = { identifier.primaryB.x(), identifier.primaryB.y(), 1.0 };
cmsToneCurve* gammaCurve = cmsBuildGamma(cmsContext(), identifier.gamma);
cmsToneCurve* toneCurves[3] = { gammaCurve, cmsDupToneCurve(gammaCurve), cmsDupToneCurve(gammaCurve) };
cmsHPROFILE profile = cmsCreateRGBProfile(&whitePoint, &primaries, toneCurves);
cmsFreeToneCurveTriple(toneCurves);
return profile;
}
break;
}
case PDFColorProfileIdentifier::Type::FileGray:
case PDFColorProfileIdentifier::Type::FileRGB:
case PDFColorProfileIdentifier::Type::FileCMYK:
{
QFile file(identifier.id);
if (file.open(QFile::ReadOnly))
{
QByteArray fileContent = file.readAll();
file.close();
return cmsOpenProfileFromMem(fileContent.data(), fileContent.size());
}
break;
}
case PDFColorProfileIdentifier::Type::MemoryGray:
case PDFColorProfileIdentifier::Type::MemoryRGB:
case PDFColorProfileIdentifier::Type::MemoryCMYK:
return cmsOpenProfileFromMem(identifier.profileMemoryData.data(), identifier.profileMemoryData.size());
default:
Q_ASSERT(false);
break;
}
}
return cmsHPROFILE();
}
cmsHTRANSFORM PDFLittleCMS::getTransform(Profile profile, RenderingIntent intent, bool isRGB888Buffer) const
{
const int key = getCacheKey(profile, intent, isRGB888Buffer);
QReadLocker lock(&m_transformationCacheLock);
auto it = m_transformationCache.find(key);
if (it == m_transformationCache.cend())
{
lock.unlock();
QWriteLocker writeLock(&m_transformationCacheLock);
// Now, we have locked cache for writing. We must find out,
// if some other thread doesn't created the transformation already.
it = m_transformationCache.find(key);
if (it == m_transformationCache.cend())
{
cmsHTRANSFORM transform = cmsHTRANSFORM();
cmsHPROFILE input = m_profiles[profile];
cmsHPROFILE output = m_profiles[Output];
if (input && output)
{
if (isSoftProofing())
{
cmsHPROFILE proofingProfile = m_profiles[SoftProofing];
RenderingIntent proofingIntent = m_settings.proofingIntent;
if (m_settings.proofingIntent == RenderingIntent::Auto)
{
proofingIntent = intent;
}
transform = cmsCreateProofingTransform(input, getProfileDataFormat(input), output, isRGB888Buffer ? TYPE_RGB_8 : TYPE_RGB_FLT, proofingProfile,
getLittleCMSRenderingIntent(intent), getLittleCMSRenderingIntent(proofingIntent), getTransformationFlags());
}
else
{
transform = cmsCreateTransform(input, getProfileDataFormat(input), output, isRGB888Buffer ? TYPE_RGB_8 : TYPE_RGB_FLT, getLittleCMSRenderingIntent(intent), getTransformationFlags());
}
}
it = m_transformationCache.insert(std::make_pair(key, transform)).first;
}
// We must return it here to avoid race condition (after current block,
// lock is not locked, because we unlocked lock for reading).
return it->second;
}
return it->second;
}
cmsUInt32Number PDFLittleCMS::getTransformationFlags() const
{
// Flag cmsFLAGS_NONEGATIVES is used here to avoid invalid transformation
// between CMYK color space and RGB color space in the Ghent output suite examples.
cmsUInt32Number flags = cmsFLAGS_NOCACHE | cmsFLAGS_NONEGATIVES;
if (m_settings.isBlackPointCompensationActive)
{
flags |= cmsFLAGS_BLACKPOINTCOMPENSATION;
}
switch (m_settings.accuracy)
{
case PDFCMSSettings::Accuracy::Low:
flags |= cmsFLAGS_LOWRESPRECALC;
break;
case PDFCMSSettings::Accuracy::Medium:
break;
case PDFCMSSettings::Accuracy::High:
flags |= cmsFLAGS_HIGHRESPRECALC;
break;
default:
Q_ASSERT(false);
break;
}
if (m_settings.isGamutChecking)
{
flags |= cmsFLAGS_GAMUTCHECK;
}
if (m_settings.isSoftProofing)
{
flags |= cmsFLAGS_SOFTPROOFING;
}
return flags;
}
RenderingIntent PDFLittleCMS::getEffectiveRenderingIntent(RenderingIntent intent) const
{
if (m_settings.intent != RenderingIntent::Auto)
{
return m_settings.intent;
}
return intent;
}
cmsUInt32Number PDFLittleCMS::getLittleCMSRenderingIntent(RenderingIntent intent)
{
switch (intent)
{
case RenderingIntent::Perceptual:
return INTENT_PERCEPTUAL;
case RenderingIntent::AbsoluteColorimetric:
return INTENT_ABSOLUTE_COLORIMETRIC;
case RenderingIntent::RelativeColorimetric:
return INTENT_RELATIVE_COLORIMETRIC;
case RenderingIntent::Saturation:
return INTENT_SATURATION;
default:
Q_ASSERT(false);
break;
}
return INTENT_PERCEPTUAL;
}
cmsUInt32Number PDFLittleCMS::getProfileDataFormat(cmsHPROFILE profile)
{
cmsColorSpaceSignature signature = cmsGetColorSpace(profile);
switch (signature)
{
case cmsSigGrayData:
return TYPE_GRAY_FLT;
case cmsSigRgbData:
return TYPE_RGB_FLT;
case cmsSigCmykData:
return TYPE_CMYK_FLT;
case cmsSigXYZData:
return TYPE_XYZ_FLT;
default:
break;
}
return 0;
}
QColor PDFLittleCMS::getColorFromOutputColor(std::array<float, 3> color01)
{
QColor color(QColor::Rgb);
color.setRgbF(qBound(0.0f, color01[0], 1.0f), qBound(0.0f, color01[1], 1.0f), qBound(0.0f, color01[2], 1.0f));
return color;
}
QByteArray PDFLittleCMS::getTransformColorSpaceKey(const PDFCMS::ColorSpaceTransformParams& params)
{
QByteArray key;
QBuffer buffer(&key);
buffer.open(QBuffer::WriteOnly);
QDataStream stream(&buffer);
stream << params.sourceType;
stream << params.sourceIccId;
stream << params.targetType;
stream << params.targetIccId;
stream << params.intent;
buffer.close();
return key;
}
cmsHTRANSFORM PDFLittleCMS::getTransformBetweenColorSpaces(const PDFCMS::ColorSpaceTransformParams& params) const
{
QByteArray key = getTransformColorSpaceKey(params);
QReadLocker lock(&m_transformColorSpaceCacheLock);
auto it = m_transformColorSpaceCache.find(key);
if (it == m_transformColorSpaceCache.cend())
{
lock.unlock();
QWriteLocker writeLock(&m_transformColorSpaceCacheLock);
// Now, we have locked cache for writing. We must find out,
// if some other thread doesn't created the transformation already.
it = m_transformColorSpaceCache.find(key);
if (it == m_transformColorSpaceCache.cend())
{
cmsHPROFILE inputProfile = cmsHPROFILE();
cmsHPROFILE outputProfile = cmsHPROFILE();
cmsHTRANSFORM transform = cmsHTRANSFORM();
switch (params.sourceType)
{
case ColorSpaceType::DeviceGray:
inputProfile = m_profiles[Gray];
break;
case ColorSpaceType::DeviceRGB:
inputProfile = m_profiles[RGB];
break;
case ColorSpaceType::DeviceCMYK:
inputProfile = m_profiles[CMYK];
break;
case ColorSpaceType::XYZ:
inputProfile = m_profiles[XYZ];
break;
case ColorSpaceType::ICC:
inputProfile = cmsOpenProfileFromMem(params.sourceIccData.data(), params.sourceIccData.size());
break;
default:
Q_ASSERT(false);
break;
}
switch (params.targetType)
{
case ColorSpaceType::DeviceGray:
outputProfile = m_profiles[Gray];
break;
case ColorSpaceType::DeviceRGB:
outputProfile = m_profiles[RGB];
break;
case ColorSpaceType::DeviceCMYK:
outputProfile = m_profiles[CMYK];
break;
case ColorSpaceType::XYZ:
outputProfile = m_profiles[XYZ];
break;
case ColorSpaceType::ICC:
outputProfile = cmsOpenProfileFromMem(params.targetIccData.data(), params.targetIccData.size());
break;
default:
Q_ASSERT(false);
break;
}
if (inputProfile && outputProfile)
{
transform = cmsCreateTransform(inputProfile, getProfileDataFormat(inputProfile), outputProfile, getProfileDataFormat(outputProfile), getLittleCMSRenderingIntent(params.intent), getTransformationFlags());
}
if (params.sourceType == ColorSpaceType::ICC)
{
cmsCloseProfile(inputProfile);
}
if (params.targetType == ColorSpaceType::ICC)
{
cmsCloseProfile(outputProfile);
}
it = m_transformColorSpaceCache.insert(std::make_pair(key, transform)).first;
}
return it->second;
}
else
{
return it->second;
}
return cmsHTRANSFORM();
}
QString getInfoFromProfile(cmsHPROFILE profile, cmsInfoType infoType)
{
QLocale locale;
QString country = QLocale::countryToString(locale.country());
QString language = QLocale::languageToString(locale.language());
char countryCode[3] = { };
char languageCode[3] = { };
if (country.size() == 2)
{
countryCode[0] = country[0].toLatin1();
countryCode[1] = country[1].toLatin1();
}
if (language.size() == 2)
{
languageCode[0] = language[0].toLatin1();
languageCode[1] = language[1].toLatin1();
}
// Jakub Melka: try to get profile info from current language/country.
// If it fails, then pick any language/any country.
cmsUInt32Number bufferSize = cmsGetProfileInfo(profile, infoType, languageCode, countryCode, nullptr, 0);
if (bufferSize)
{
std::vector<wchar_t> buffer(bufferSize, 0);
cmsGetProfileInfo(profile, infoType, languageCode, countryCode, buffer.data(), static_cast<cmsUInt32Number>(buffer.size()));
return QString::fromWCharArray(buffer.data());
}
bufferSize = cmsGetProfileInfo(profile, infoType, cmsNoLanguage, cmsNoCountry, nullptr, 0);
if (bufferSize)
{
std::vector<wchar_t> buffer(bufferSize, 0);
cmsGetProfileInfo(profile, infoType, cmsNoLanguage, cmsNoCountry, buffer.data(), static_cast<cmsUInt32Number>(buffer.size()));
return QString::fromWCharArray(buffer.data());
}
return QString();
}
bool PDFCMSGeneric::isCompatible(const PDFCMSSettings& settings) const
{
return settings.system == PDFCMSSettings::System::Generic;
}
QColor PDFCMSGeneric::getPaperColor() const
{
return QColor(Qt::white);
}
QColor PDFCMSGeneric::getColorFromDeviceGray(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(color);
Q_UNUSED(intent);
Q_UNUSED(reporter);
return QColor();
}
QColor PDFCMSGeneric::getColorFromDeviceRGB(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(color);
Q_UNUSED(intent);
Q_UNUSED(reporter);
return QColor();
}
QColor PDFCMSGeneric::getColorFromDeviceCMYK(const PDFColor& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(color);
Q_UNUSED(intent);
Q_UNUSED(reporter);
return QColor();
}
QColor PDFCMSGeneric::getColorFromXYZ(const PDFColor3& whitePoint, const PDFColor3& color, RenderingIntent intent, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(color);
Q_UNUSED(intent);
Q_UNUSED(reporter);
Q_UNUSED(whitePoint);
return QColor();
}
QColor PDFCMSGeneric::getColorFromICC(const PDFColor& color,
RenderingIntent renderingIntent,
const QByteArray& iccID,
const QByteArray& iccData,
PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(color);
Q_UNUSED(renderingIntent);
Q_UNUSED(iccID);
Q_UNUSED(iccData);
Q_UNUSED(reporter);
return QColor();
}
bool PDFCMSGeneric::fillRGBBufferFromDeviceGray(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(colors);
Q_UNUSED(intent);
Q_UNUSED(outputBuffer);
Q_UNUSED(reporter);
return false;
}
bool PDFCMSGeneric::fillRGBBufferFromDeviceRGB(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(colors);
Q_UNUSED(intent);
Q_UNUSED(outputBuffer);
Q_UNUSED(reporter);
return false;
}
bool PDFCMSGeneric::fillRGBBufferFromDeviceCMYK(const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(colors);
Q_UNUSED(intent);
Q_UNUSED(outputBuffer);
Q_UNUSED(reporter);
return false;
}
bool PDFCMSGeneric::fillRGBBufferFromXYZ(const PDFColor3& whitePoint, const std::vector<float>& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(whitePoint);
Q_UNUSED(colors);
Q_UNUSED(intent);
Q_UNUSED(outputBuffer);
Q_UNUSED(reporter);
return false;
}
bool PDFCMSGeneric::fillRGBBufferFromICC(const std::vector<float>& colors, RenderingIntent renderingIntent, unsigned char* outputBuffer, const QByteArray& iccID, const QByteArray& iccData, PDFRenderErrorReporter* reporter) const
{
Q_UNUSED(colors);
Q_UNUSED(renderingIntent);
Q_UNUSED(outputBuffer);
Q_UNUSED(iccID);
Q_UNUSED(iccData);
Q_UNUSED(reporter);
return false;
}
bool PDFCMSGeneric::transformColorSpace(const PDFCMS::ColorSpaceTransformParams& params) const
{
Q_UNUSED(params);
return false;
}
PDFCMSManager::PDFCMSManager(QObject* parent) :
BaseClass(parent),
m_document(nullptr),
m_mutex(QMutex::Recursive)
{
}
PDFCMSPointer PDFCMSManager::getCurrentCMS() const
{
QMutexLocker lock(&m_mutex);
return m_CMS.get(this, &PDFCMSManager::getCurrentCMSImpl);
}
void PDFCMSManager::setSettings(const PDFCMSSettings& settings)
{
if (m_settings != settings)
{
// We must ensure, that mutex is not locked, while we are
// sending signal about CMS change.
{
QMutexLocker lock(&m_mutex);
m_settings = settings;
clearCache();
}
emit colorManagementSystemChanged();
}
}
const PDFColorProfileIdentifiers& PDFCMSManager::getOutputProfiles() const
{
QMutexLocker lock(&m_mutex);
return m_outputProfiles.get(this, &PDFCMSManager::getOutputProfilesImpl);
}
const PDFColorProfileIdentifiers& PDFCMSManager::getGrayProfiles() const
{
QMutexLocker lock(&m_mutex);
return m_grayProfiles.get(this, &PDFCMSManager::getGrayProfilesImpl);
}
const PDFColorProfileIdentifiers& PDFCMSManager::getRGBProfiles() const
{
QMutexLocker lock(&m_mutex);
return m_RGBProfiles.get(this, &PDFCMSManager::getRGBProfilesImpl);
}
const PDFColorProfileIdentifiers& PDFCMSManager::getCMYKProfiles() const
{
QMutexLocker lock(&m_mutex);
return m_CMYKProfiles.get(this, &PDFCMSManager::getCMYKProfilesImpl);
}
const PDFColorProfileIdentifiers& PDFCMSManager::getExternalProfiles() const
{
// Jakub Melka: do not protect this by mutex, this function is private
// and must be called only from mutex-protected code.
return m_externalProfiles.get(this, &PDFCMSManager::getExternalProfilesImpl);
}
PDFCMSSettings PDFCMSManager::getDefaultSettings() const
{
PDFCMSSettings settings;
auto getFirstProfileId = [](const PDFColorProfileIdentifiers& identifiers)
{
if (!identifiers.empty())
{
return identifiers.front().id;
}
return QString();
};
settings.system = PDFCMSSettings::System::LittleCMS2;
settings.outputCS = getFirstProfileId(getOutputProfiles());
settings.deviceGray = getFirstProfileId(getGrayProfiles());
settings.deviceRGB = getFirstProfileId(getRGBProfiles());
settings.deviceCMYK = getFirstProfileId(getCMYKProfiles());
return settings;
}
void PDFCMSManager::setDocument(const PDFDocument* document)
{
std::optional<QMutexLocker> lock;
lock.emplace(&m_mutex);
if (m_document == document)
{
return;
}
m_document = document;
int i = 0;
PDFColorProfileIdentifiers outputIntentProfiles;
if (m_document)
{
for (const PDFOutputIntent& outputIntent : m_document->getCatalog()->getOutputIntents())
{
QByteArray content;
try
{
// Try to read the profile from the output intent stream. If it fails, then do nothing.
PDFObject outputProfileObject = m_document->getObject(outputIntent.getOutputProfile());
if (outputProfileObject.isStream())
{
content = m_document->getDecodedStream(outputProfileObject.getStream());
}
}
catch (PDFException)
{
continue;
}
if (content.isEmpty())
{
// Decoding of output profile failed. Continue
// with next output profile.
continue;
}
cmsHPROFILE profile = cmsOpenProfileFromMem(content.data(), content.size());
if (profile)
{
PDFColorProfileIdentifier::Type csiType = PDFColorProfileIdentifier::Type::Invalid;
const cmsColorSpaceSignature colorSpace = cmsGetColorSpace(profile);
switch (colorSpace)
{
case cmsSigGrayData:
csiType = PDFColorProfileIdentifier::Type::MemoryGray;
break;
case cmsSigRgbData:
csiType = PDFColorProfileIdentifier::Type::MemoryRGB;
break;
case cmsSigCmykData:
csiType = PDFColorProfileIdentifier::Type::MemoryCMYK;
break;
default:
break;
}
QString description = getInfoFromProfile(profile, cmsInfoDescription);
cmsCloseProfile(profile);
// If we have a valid profile, then add it
if (csiType != PDFColorProfileIdentifier::Type::Invalid)
{
outputIntentProfiles.emplace_back(PDFColorProfileIdentifier::createOutputIntent(csiType, qMove(description), QString("@@OUTPUT_INTENT_PROFILE_%1").arg(++i), qMove(content)));
}
}
}
}
bool outputIntentProfilesChanged = false;
if (m_outputIntentProfiles != outputIntentProfiles)
{
m_outputIntentProfiles = qMove(outputIntentProfiles);
clearCache();
outputIntentProfilesChanged = true;
}
if (outputIntentProfilesChanged)
{
lock = std::nullopt;
emit colorManagementSystemChanged();
}
}
QString PDFCMSManager::getSystemName(PDFCMSSettings::System system)
{
switch (system)
{
case PDFCMSSettings::System::Generic:
return tr("Generic");
case PDFCMSSettings::System::LittleCMS2:
{
const int major = LCMS_VERSION / 1000;
const int minor = (LCMS_VERSION % 1000) / 10;
return tr("Little CMS %1.%2").arg(major).arg(minor);
}
default:
{
Q_ASSERT(false);
break;
}
}
return QString();
}
PDFCMSPointer PDFCMSManager::getCurrentCMSImpl() const
{
switch (m_settings.system)
{
case PDFCMSSettings::System::Generic:
return PDFCMSPointer(new PDFCMSGeneric());
case PDFCMSSettings::System::LittleCMS2:
return PDFCMSPointer(new PDFLittleCMS(this, m_settings));
default:
Q_ASSERT(false);
break;
}
return PDFCMSPointer(new PDFCMSGeneric());
}
void PDFCMSManager::clearCache()
{
QMutexLocker lock(&m_mutex);
m_CMS.dirty();
m_outputProfiles.dirty();
m_grayProfiles.dirty();
m_RGBProfiles.dirty();
m_CMYKProfiles.dirty();
m_externalProfiles.dirty();
}
PDFColorProfileIdentifiers PDFCMSManager::getOutputProfilesImpl() const
{
// Currently, we only support sRGB output color profile.
return { PDFColorProfileIdentifier::createSRGB() };
}
PDFColorProfileIdentifiers PDFCMSManager::getGrayProfilesImpl() const
{
// Jakub Melka: We create gray profiles for temperature 5000K, 6500K and 9300K.
// We also use linear gamma and gamma value 2.2.
PDFColorProfileIdentifiers result =
{
PDFColorProfileIdentifier::createGray(tr("Gray D65, γ = 2.2"), "@GENERIC_Gray_D65_g22", 6500.0, 2.2),
PDFColorProfileIdentifier::createGray(tr("Gray D50, γ = 2.2"), "@GENERIC_Gray_D50_g22", 5000.0, 2.2),
PDFColorProfileIdentifier::createGray(tr("Gray D93, γ = 2.2"), "@GENERIC_Gray_D93_g22", 9300.0, 2.2),
PDFColorProfileIdentifier::createGray(tr("Gray D65, γ = 1.0 (linear)"), "@GENERIC_Gray_D65_g10", 6500.0, 1.0),
PDFColorProfileIdentifier::createGray(tr("Gray D50, γ = 1.0 (linear)"), "@GENERIC_Gray_D50_g10", 5000.0, 1.0),
PDFColorProfileIdentifier::createGray(tr("Gray D93, γ = 1.0 (linear)"), "@GENERIC_Gray_D93_g10", 9300.0, 1.0)
};
PDFColorProfileIdentifiers externalGrayProfiles = getFilteredExternalProfiles(PDFColorProfileIdentifier::Type::FileGray);
result.insert(result.end(), std::make_move_iterator(externalGrayProfiles.begin()), std::make_move_iterator(externalGrayProfiles.end()));
PDFColorProfileIdentifiers outputIntentRGBProfiles = getFilteredOutputIntentProfiles(PDFColorProfileIdentifier::Type::MemoryGray);
result.insert(result.end(), std::make_move_iterator(outputIntentRGBProfiles.begin()), std::make_move_iterator(outputIntentRGBProfiles.end()));
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getRGBProfilesImpl() const
{
// Jakub Melka: We create RGB profiles for common standards and also for
// default standard sRGB. See https://en.wikipedia.org/wiki/Color_spaces_with_RGB_primaries.
PDFColorProfileIdentifiers result =
{
PDFColorProfileIdentifier::createSRGB(),
PDFColorProfileIdentifier::createRGB(tr("HDTV (ITU-R BT.709)"), "@GENERIC_RGB_HDTV", 6500, QPointF(0.64, 0.33), QPointF(0.30, 0.60), QPointF(0.15, 0.06), 20.0 / 9.0),
PDFColorProfileIdentifier::createRGB(tr("Adobe RGB 1998"), "@GENERIC_RGB_Adobe1998", 6500, QPointF(0.64, 0.33), QPointF(0.30, 0.60), QPointF(0.15, 0.06), 563.0 / 256.0),
PDFColorProfileIdentifier::createRGB(tr("PAL / SECAM"), "@GENERIC_RGB_PalSecam", 6500, QPointF(0.64, 0.33), QPointF(0.29, 0.60), QPointF(0.15, 0.06), 14.0 / 5.0),
PDFColorProfileIdentifier::createRGB(tr("NTSC"), "@GENERIC_RGB_NTSC", 6500, QPointF(0.64, 0.34), QPointF(0.31, 0.595), QPointF(0.155, 0.07), 20.0 / 9.0),
PDFColorProfileIdentifier::createRGB(tr("Adobe Wide Gamut RGB"), "@GENERIC_RGB_AdobeWideGamut", 5000, QPointF(0.735, 0.265), QPointF(0.115, 0.826), QPointF(0.157, 0.018), 563.0 / 256.0),
PDFColorProfileIdentifier::createRGB(tr("ProPhoto RGB"), "@GENERIC_RGB_ProPhoto", 5000, QPointF(0.7347, 0.2653), QPointF(0.1596, 0.8404), QPointF(0.0366, 0.0001), 9.0 / 5.0)
};
PDFColorProfileIdentifiers externalRGBProfiles = getFilteredExternalProfiles(PDFColorProfileIdentifier::Type::FileRGB);
result.insert(result.end(), externalRGBProfiles.begin(), externalRGBProfiles.end());
PDFColorProfileIdentifiers outputIntentRGBProfiles = getFilteredOutputIntentProfiles(PDFColorProfileIdentifier::Type::MemoryRGB);
result.insert(result.end(), std::make_move_iterator(outputIntentRGBProfiles.begin()), std::make_move_iterator(outputIntentRGBProfiles.end()));
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getCMYKProfilesImpl() const
{
PDFColorProfileIdentifiers result;
PDFColorProfileIdentifiers externalCMYKProfiles = getFilteredExternalProfiles(PDFColorProfileIdentifier::Type::FileCMYK);
result.insert(result.end(), externalCMYKProfiles.begin(), externalCMYKProfiles.end());
PDFColorProfileIdentifiers outputIntentCMYKProfiles = getFilteredOutputIntentProfiles(PDFColorProfileIdentifier::Type::MemoryCMYK);
result.insert(result.end(), std::make_move_iterator(outputIntentCMYKProfiles.begin()), std::make_move_iterator(outputIntentCMYKProfiles.end()));
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getExternalColorProfiles(QString profileDirectory) const
{
PDFColorProfileIdentifiers result;
QDir directory(profileDirectory);
QDir applicationDirectory(QApplication::applicationDirPath());
if (!profileDirectory.isEmpty() && directory.exists())
{
QStringList iccProfiles = directory.entryList({ "*.icc" }, QDir::Files | QDir::Readable | QDir::NoDotAndDotDot, QDir::NoSort);
for (const QString& fileName : iccProfiles)
{
QString filePath = QDir::cleanPath(applicationDirectory.relativeFilePath(directory.absoluteFilePath(fileName)));
// Try to read the profile from the file. If it fails, then do nothing.
QFile file(filePath);
if (file.open(QFile::ReadOnly))
{
QByteArray content = file.readAll();
file.close();
cmsHPROFILE profile = cmsOpenProfileFromMem(content.data(), content.size());
if (profile)
{
PDFColorProfileIdentifier::Type csiType = PDFColorProfileIdentifier::Type::Invalid;
const cmsColorSpaceSignature colorSpace = cmsGetColorSpace(profile);
switch (colorSpace)
{
case cmsSigGrayData:
csiType = PDFColorProfileIdentifier::Type::FileGray;
break;
case cmsSigRgbData:
csiType = PDFColorProfileIdentifier::Type::FileRGB;
break;
case cmsSigCmykData:
csiType = PDFColorProfileIdentifier::Type::FileCMYK;
break;
default:
break;
}
QString description = getInfoFromProfile(profile, cmsInfoDescription);
cmsCloseProfile(profile);
// If we have a valid profile, then add it
if (csiType != PDFColorProfileIdentifier::Type::Invalid)
{
result.emplace_back(PDFColorProfileIdentifier::createFile(csiType, qMove(description), filePath));
}
}
}
}
}
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getExternalProfilesImpl() const
{
PDFColorProfileIdentifiers result;
QStringList directories(m_settings.profileDirectory);
#ifdef Q_OS_WIN
std::array<WCHAR, _MAX_PATH> buffer = { };
DWORD bufferSize = DWORD(buffer.size() * sizeof(WCHAR));
if (GetColorDirectoryW(NULL, buffer.data(), &bufferSize))
{
const DWORD charactersWithNull = bufferSize / sizeof(WCHAR);
const DWORD charactersWithoutNull = bufferSize > 0 ? charactersWithNull - 1 : 0;
QString directory = QString::fromWCharArray(buffer.data(), int(charactersWithoutNull));
directories << QDir::fromNativeSeparators(directory);
}
#endif
for (const QString& directory : directories)
{
PDFColorProfileIdentifiers externalProfiles = getExternalColorProfiles(directory);
result.insert(result.end(), externalProfiles.begin(), externalProfiles.end());
}
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getFilteredExternalProfiles(PDFColorProfileIdentifier::Type type) const
{
PDFColorProfileIdentifiers result;
const PDFColorProfileIdentifiers& externalProfiles = getExternalProfiles();
std::copy_if(externalProfiles.cbegin(), externalProfiles.cend(), std::back_inserter(result), [type](const PDFColorProfileIdentifier& identifier) { return identifier.type == type; });
return result;
}
PDFColorProfileIdentifiers PDFCMSManager::getFilteredOutputIntentProfiles(PDFColorProfileIdentifier::Type type) const
{
PDFColorProfileIdentifiers result;
std::copy_if(m_outputIntentProfiles.cbegin(), m_outputIntentProfiles.cend(), std::back_inserter(result), [type](const PDFColorProfileIdentifier& identifier) { return identifier.type == type; });
return result;
}
PDFColorProfileIdentifier PDFColorProfileIdentifier::createGray(QString name, QString id, PDFReal temperature, PDFReal gamma)
{
PDFColorProfileIdentifier result;
result.type = Type::Gray;
result.name = qMove(name);
result.id = qMove(id);
result.temperature = temperature;
result.gamma = gamma;
return result;
}
PDFColorProfileIdentifier PDFColorProfileIdentifier::createSRGB()
{
PDFColorProfileIdentifier result;
result.type = Type::sRGB;
result.name = PDFCMSManager::tr("sRGB");
result.id = "@GENERIC_sRGB";
return result;
}
PDFColorProfileIdentifier PDFColorProfileIdentifier::createRGB(QString name, QString id, PDFReal temperature, QPointF primaryR, QPointF primaryG, QPointF primaryB, PDFReal gamma)
{
PDFColorProfileIdentifier result;
result.type = Type::RGB;
result.name = qMove(name);
result.id = qMove(id);
result.temperature = temperature;
result.primaryR = primaryR;
result.primaryG = primaryG;
result.primaryB = primaryB;
result.gamma = gamma;
return result;
}
PDFColorProfileIdentifier PDFColorProfileIdentifier::createFile(Type type, QString name, QString id)
{
PDFColorProfileIdentifier result;
result.type = type;
result.name = qMove(name);
result.id = qMove(id);
return result;
}
PDFColorProfileIdentifier PDFColorProfileIdentifier::createOutputIntent(PDFColorProfileIdentifier::Type type, QString name, QString id, QByteArray profileData)
{
PDFColorProfileIdentifier result;
result.type = type;
result.name = qMove(name);
result.id = qMove(id);
result.profileMemoryData = qMove(profileData);
result.isOutputIntentProfile = true;
return result;
}
PDFColor3 PDFCMS::getDefaultXYZWhitepoint()
{
const cmsCIEXYZ* whitePoint = cmsD50_XYZ();
return PDFColor3{ PDFColorComponent(whitePoint->X), PDFColorComponent(whitePoint->Y), PDFColorComponent(whitePoint->Z) };
}
PDFColorComponentMatrix_3x3 PDFChromaticAdaptationXYZ::createWhitepointChromaticAdaptation(const PDFColor3& targetWhitePoint,
const PDFColor3& sourceWhitePoint,
PDFCMSSettings::ColorAdaptationXYZ method)
{
PDFColorComponentMatrix_3x3 matrix;
matrix.makeIdentity();
switch (method)
{
case pdf::PDFCMSSettings::ColorAdaptationXYZ::None:
// No scaling performed, just return identity matrix
break;
case pdf::PDFCMSSettings::ColorAdaptationXYZ::XYZScaling:
matrix.makeDiagonal(std::array{ targetWhitePoint[0] / sourceWhitePoint[0],
targetWhitePoint[1] / sourceWhitePoint[1],
targetWhitePoint[2] / sourceWhitePoint[2]
});
break;
case pdf::PDFCMSSettings::ColorAdaptationXYZ::CAT97:
{
// CAT97 matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 cat97Matrix(
0.8562f, 0.3372f, -0.1934f,
-0.8360f, 1.8327f, 0.0033f,
0.0357f, -0.0469f, 1.0112f);
// Inverse of CAT97 matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseCat97Matrix(
0.9873999149199271f, -0.1768250198556842f, 0.1894251049357571f,
0.4504351090445315f, 0.464932897752711f, 0.08463199320275755f,
-0.01396832510725165f, 0.027806572501434f, 0.9861617526058175f);
PDFColor3 adaptedTargetWhitePoint = cat97Matrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = cat97Matrix * sourceWhitePoint;
PDFColor3 gain = {
adaptedTargetWhitePoint[0] / adaptedSourceWhitePoint[0],
adaptedTargetWhitePoint[1] / adaptedSourceWhitePoint[1],
adaptedTargetWhitePoint[2] / adaptedSourceWhitePoint[2]
};
PDFColorComponentMatrix_3x3 gainMatrix;
gainMatrix.makeDiagonal(gain);
matrix = inverseCat97Matrix * gainMatrix * cat97Matrix;
break;
}
case pdf::PDFCMSSettings::ColorAdaptationXYZ::CAT02:
{
// CAT02 matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 cat02Matrix(
0.7328f, 0.4296f, -0.1624f,
-0.7036f, 1.6975f, 0.0061f,
0.0030f, 0.0136f, 0.9834f);
// Inverse of CAT02 matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseCat02Matrix(
1.096123820835514f, -0.2788690002182872f, 0.182745179382773f,
0.4543690419753592f, 0.4735331543074117f, 0.0720978037172291f,
-0.009627608738429352f, -0.005698031216113419f, 1.015325639954543f);
PDFColor3 adaptedTargetWhitePoint = cat02Matrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = cat02Matrix * sourceWhitePoint;
PDFColor3 gain = {
adaptedTargetWhitePoint[0] / adaptedSourceWhitePoint[0],
adaptedTargetWhitePoint[1] / adaptedSourceWhitePoint[1],
adaptedTargetWhitePoint[2] / adaptedSourceWhitePoint[2]
};
PDFColorComponentMatrix_3x3 gainMatrix;
gainMatrix.makeDiagonal(gain);
matrix = inverseCat02Matrix * gainMatrix * cat02Matrix;
break;
}
case pdf::PDFCMSSettings::ColorAdaptationXYZ::Bradford:
{
// Bradford matrix, as defined in https://en.wikipedia.org/wiki/LMS_color_space
constexpr PDFColorComponentMatrix_3x3 bradfordMatrix(
0.8951f, 0.2264f, -0.1614f,
-0.7502f, 1.7135f, 0.0367f,
0.0389f, -0.0685f, 1.0296f);
// Inverse of bradford matrix (using wxMaxima to compute it)
constexpr PDFColorComponentMatrix_3x3 inverseBradfordMatrix(
1.004360519274085f, -0.1262294327613208f, 0.1619428982062721f,
0.4399123264001572f, 0.527481594455384f, 0.05015858096782513f,
-0.008678739162151443f, 0.03986287311053728f, 0.9684695843590444f);
PDFColor3 adaptedTargetWhitePoint = bradfordMatrix * targetWhitePoint;
PDFColor3 adaptedSourceWhitePoint = bradfordMatrix * sourceWhitePoint;
PDFColor3 gain = {
adaptedTargetWhitePoint[0] / adaptedSourceWhitePoint[0],
adaptedTargetWhitePoint[1] / adaptedSourceWhitePoint[1],
adaptedTargetWhitePoint[2] / adaptedSourceWhitePoint[2]
};
PDFColorComponentMatrix_3x3 gainMatrix;
gainMatrix.makeDiagonal(gain);
matrix = inverseBradfordMatrix * gainMatrix * bradfordMatrix;
break;
}
default:
Q_ASSERT(false);
break;
}
return matrix;
}
} // namespace pdf