mirror of https://github.com/JakubMelka/PDF4QT.git
2022 lines
75 KiB
C++
2022 lines
75 KiB
C++
// Copyright (C) 2019-2022 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 "pdfdbgheap.h"
|
||
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QBuffer>
|
||
#include <QCoreApplication>
|
||
#include <QReadWriteLock>
|
||
|
||
#ifdef PDF4QT_COMPILER_CLANG
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wregister"
|
||
#endif
|
||
|
||
#ifdef PDF4QT_COMPILER_MSVC
|
||
#pragma warning(push)
|
||
#pragma warning(disable:5033)
|
||
#endif
|
||
#ifndef CMS_NO_REGISTER_KEYWORD
|
||
#define CMS_NO_REGISTER_KEYWORD
|
||
#endif
|
||
#include <lcms2.h>
|
||
#include <lcms2_plugin.h>
|
||
#ifdef PDF4QT_COMPILER_MSVC
|
||
#pragma warning(pop)
|
||
#endif
|
||
|
||
#ifdef PDF4QT_COMPILER_CLANG
|
||
#pragma clang diagnostic pop
|
||
#endif
|
||
|
||
#ifdef Q_OS_WIN
|
||
#ifndef NOMINMAX
|
||
#define NOMINMAX
|
||
#endif
|
||
#include <Windows.h>
|
||
#include <Icm.h>
|
||
#if defined(PDF4QT_USE_PRAGMA_LIB)
|
||
#pragma comment(lib, "Mscms")
|
||
#endif
|
||
#endif
|
||
|
||
#include <unordered_map>
|
||
|
||
namespace pdf
|
||
{
|
||
|
||
class PDFLittleCMS : public PDFCMS
|
||
{
|
||
public:
|
||
explicit PDFLittleCMS(const PDFCMSManager* manager,
|
||
const PDFCMSSettings& settings,
|
||
const PDFColorConvertor& colorConvertor);
|
||
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;
|
||
virtual PDFColorConvertor getColorConvertor() 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;
|
||
PDFColorConvertor m_colorConvertor;
|
||
|
||
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;
|
||
}
|
||
|
||
PDFColorConvertor PDFLittleCMS::getColorConvertor() const
|
||
{
|
||
return m_colorConvertor;
|
||
}
|
||
|
||
PDFLittleCMS::PDFLittleCMS(const PDFCMSManager* manager,
|
||
const PDFCMSSettings& settings,
|
||
const PDFColorConvertor& colorConvertor) :
|
||
m_manager(manager),
|
||
m_settings(settings),
|
||
m_paperColor(Qt::white),
|
||
m_profiles(),
|
||
m_colorConvertor(colorConvertor)
|
||
{
|
||
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 iPt = 0; iPt < gridPoints; ++iPt)
|
||
{
|
||
const cmsFloat32Number x = iPt * factor;
|
||
s2.SampledPoints[iPt] = 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::territoryToString(locale.territory());
|
||
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();
|
||
}
|
||
|
||
PDFCMSGeneric::PDFCMSGeneric(const PDFColorConvertor& colorConvertor) :
|
||
m_colorConvertor(colorConvertor)
|
||
{
|
||
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
PDFColorConvertor PDFCMSGeneric::getColorConvertor() const
|
||
{
|
||
return m_colorConvertor;
|
||
}
|
||
|
||
PDFCMSManager::PDFCMSManager(QObject* parent) :
|
||
BaseClass(parent),
|
||
m_document(nullptr),
|
||
m_mutex()
|
||
{
|
||
|
||
}
|
||
|
||
void PDFCMSManager::finalize()
|
||
{
|
||
cmsUnregisterPlugins();
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
Q_EMIT colorManagementSystemChanged();
|
||
}
|
||
}
|
||
|
||
PDFColorConvertor PDFCMSManager::getColorConvertor() const
|
||
{
|
||
QMutexLocker lock(&m_mutex);
|
||
|
||
PDFColorConvertor colorConvertor;
|
||
colorConvertor.setBackgroundColor(m_settings.backgroundColor);
|
||
colorConvertor.setForegroundColor(m_settings.foregroundColor);
|
||
colorConvertor.setHighContrastBrightnessFactor(m_settings.sigmoidSlopeFactor);
|
||
colorConvertor.setBitonalThreshold(m_settings.bitonalThreshold);
|
||
return colorConvertor;
|
||
}
|
||
|
||
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<QRecursiveMutex>> 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 (const 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;
|
||
Q_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(getColorConvertor()));
|
||
|
||
case PDFCMSSettings::System::LittleCMS2:
|
||
return PDFCMSPointer(new PDFLittleCMS(this, m_settings, getColorConvertor()));
|
||
|
||
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(QCoreApplication::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);
|
||
|
||
#if defined(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);
|
||
}
|
||
#elif defined(Q_OS_UNIX)
|
||
QDir directory(QStringLiteral("/usr/share/color/icc"));
|
||
if (directory.exists())
|
||
{
|
||
QStringList colorDirectories = directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||
for (const QString colorDirectory : colorDirectories)
|
||
{
|
||
QString colorDirectoryName = directory.absoluteFilePath(colorDirectory);
|
||
directories << QDir::fromNativeSeparators(colorDirectoryName);
|
||
}
|
||
}
|
||
#else
|
||
static_assert(false, "Implement this for another OS!");
|
||
#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
|