// 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 . #include "pdfcms.h" #include "pdfdocument.h" #include "pdfexecutionpolicy.h" #include #include #pragma warning(push) #pragma warning(disable:5033) #define CMS_NO_REGISTER_KEYWORD #include #include #pragma warning(pop) #ifdef Q_OS_WIN #define NOMINMAX #include #include #pragma comment(lib, "Mscms") #endif #include 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& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override; virtual bool fillRGBBufferFromDeviceRGB(const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override; virtual bool fillRGBBufferFromDeviceCMYK(const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override; virtual bool fillRGBBufferFromXYZ(const PDFColor3& whitePoint, const std::vector& colors, RenderingIntent intent, unsigned char* outputBuffer, PDFRenderErrorReporter* reporter) const override; virtual bool fillRGBBufferFromICC(const std::vector& 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 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 m_profiles; mutable QReadWriteLock m_transformationCacheLock; mutable std::unordered_map m_transformationCache; mutable QReadWriteLock m_customIccProfileCacheLock; mutable std::map, cmsHTRANSFORM> m_customIccProfileCache; mutable QReadWriteLock m_transformColorSpaceCacheLock; mutable std::map m_transformColorSpaceCache; }; bool PDFLittleCMS::fillRGBBufferFromDeviceGray(const std::vector& 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(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& 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(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& 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 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(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& 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 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(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& 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 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(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 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(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(params.input.size()) / inputChannels; const cmsUInt32Number outputPixelCount = static_cast(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 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 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 rgbInputColor = { color[0], color[1], color[2] }; std::array 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 cmykInputColor = { color[0] * 100.0f, color[1] * 100.0f, color[2] * 100.0f, color[3] * 100.0f }; std::array 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 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 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 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 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 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::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(_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::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 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 buffer(bufferSize, 0); cmsGetProfileInfo(profile, infoType, languageCode, countryCode, buffer.data(), static_cast(buffer.size())); return QString::fromWCharArray(buffer.data()); } bufferSize = cmsGetProfileInfo(profile, infoType, cmsNoLanguage, cmsNoCountry, nullptr, 0); if (bufferSize) { std::vector buffer(bufferSize, 0); cmsGetProfileInfo(profile, infoType, cmsNoLanguage, cmsNoCountry, buffer.data(), static_cast(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& 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& 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& 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& 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& 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 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 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