diff --git a/core/fpdfapi/font/BUILD.gn b/core/fpdfapi/font/BUILD.gn --- a/core/fpdfapi/font/BUILD.gn +++ b/core/fpdfapi/font/BUILD.gn @@ -62,10 +62,12 @@ "cpdf_simplefont_unittest.cpp", "cpdf_tounicodemap_unittest.cpp", "cpdf_truetypefont_unittest.cpp", + "cpdf_type3font_unittest.cpp", ] deps = [ ":font", "../../fxge", + "../page", "../page:unit_test_support", "../parser", "../parser:unit_test_support", diff --git a/core/fpdfapi/font/cpdf_font.h b/core/fpdfapi/font/cpdf_font.h --- a/core/fpdfapi/font/cpdf_font.h +++ b/core/fpdfapi/font/cpdf_font.h @@ -20,6 +20,7 @@ #include "core/fxcrt/fx_coordinates.h" #include "core/fxcrt/fx_string.h" #include "core/fxcrt/observed_ptr.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" #include "core/fxge/cfx_font.h" @@ -43,7 +44,8 @@ public: virtual ~FormIface() = default; - virtual void ParseContentForType3Char(CPDF_Type3Char* pChar) = 0; + virtual void ParseContentForType3Char(CPDF_Type3Char* pChar, + RenderingIntent intent) = 0; virtual bool HasPageObjects() const = 0; virtual CFX_FloatRect CalcBoundingBox() const = 0; virtual std::optional, CFX_Matrix>> diff --git a/core/fpdfapi/font/cpdf_type3font.cpp b/core/fpdfapi/font/cpdf_type3font.cpp --- a/core/fpdfapi/font/cpdf_type3font.cpp +++ b/core/fpdfapi/font/cpdf_type3font.cpp @@ -96,12 +96,17 @@ CheckFontMetrics(); } -CPDF_Type3Char* CPDF_Type3Font::LoadChar(uint32_t charcode) { - if (char_loading_depth_ >= kMaxType3FormLevel) { +CPDF_Type3Char* CPDF_Type3Font::LoadChar(uint32_t charcode, + RenderingIntent intent) { + if (charcode >= char_width_l_.size() || + static_cast(intent) > + static_cast(RenderingIntent::kAbsoluteColorimetric) || + char_loading_depth_ >= kMaxType3FormLevel) { return nullptr; } - auto it = cache_map_.find(charcode); + const GlyphKey key{charcode, intent}; + auto it = cache_map_.find(key); if (it != cache_map_.end()) { return it->second.get(); } @@ -132,9 +137,9 @@ { AutoRestorer restorer(&char_loading_depth_); char_loading_depth_++; - pForm->ParseContentForType3Char(pNewChar.get()); + pForm->ParseContentForType3Char(pNewChar.get(), intent); } - it = cache_map_.find(charcode); + it = cache_map_.find(key); if (it != cache_map_.end()) { return it->second.get(); } @@ -145,7 +150,7 @@ } CPDF_Type3Char* pCachedChar = pNewChar.get(); - cache_map_[charcode] = std::move(pNewChar); + cache_map_[key] = std::move(pNewChar); return pCachedChar; } diff --git a/core/fpdfapi/font/cpdf_type3font.h b/core/fpdfapi/font/cpdf_type3font.h --- a/core/fpdfapi/font/cpdf_type3font.h +++ b/core/fpdfapi/font/cpdf_type3font.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "core/fpdfapi/font/cpdf_simplefont.h" #include "core/fxcrt/fx_coordinates.h" @@ -36,7 +37,9 @@ void SetPageResources(CPDF_Dictionary* pResources) { page_resources_.Reset(pResources); } - CPDF_Type3Char* LoadChar(uint32_t charcode); + CPDF_Type3Char* LoadChar( + uint32_t charcode, + RenderingIntent intent = RenderingIntent::kRelativeColorimetric); void CheckType3FontMetrics(); CFX_Matrix& GetFontMatrix() { return font_matrix_; } @@ -56,7 +59,10 @@ RetainPtr char_procs_; RetainPtr page_resources_; RetainPtr font_resources_; - std::map> cache_map_; + // Type 3 uses one-byte character codes. At most four parsed variants per + // character preserve inherited RI while keeping glyph-local ri/q/Q intact. + using GlyphKey = std::pair; + std::map> cache_map_; std::array char_width_l_ = {}; }; diff --git a/core/fpdfapi/font/cpdf_type3font_unittest.cpp b/core/fpdfapi/font/cpdf_type3font_unittest.cpp --- /dev/null +++ b/core/fpdfapi/font/cpdf_type3font_unittest.cpp @@ -0,0 +1,147 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fpdfapi/font/cpdf_type3font.h" + +#include +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_type3char.h" +#include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_pageobject.h" +#include "core/fpdfapi/page/test_with_page_module.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_test_document.h" +#include "core/fpdfapi/render/cpdf_type3cache.h" +#include "core/fxcrt/data_vector.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using CPDFType3FontTest = TestWithPageModule; + +constexpr RenderingIntent kIntents[] = { + RenderingIntent::kPerceptual, + RenderingIntent::kRelativeColorimetric, + RenderingIntent::kSaturation, + RenderingIntent::kAbsoluteColorimetric, +}; + +class CountingFormFactory final : public CPDF_Font::FormFactoryIface { + public: + std::unique_ptr CreateForm( + CPDF_Document* document, + RetainPtr resources, + RetainPtr stream) override { + ++count; + return std::make_unique(document, std::move(resources), + std::move(stream)); + } + + int count = 0; +}; + +RetainPtr MakeType3Font(CPDF_TestDocument* document, + CountingFormFactory* factory, + std::string_view content) { + auto stream = document->NewIndirect( + DataVector(content.begin(), content.end()), + pdfium::MakeRetain()); + auto dict = pdfium::MakeRetain(); + dict->SetNewFor("Type", "Font"); + dict->SetNewFor("Subtype", "Type3"); + dict->SetNewFor("Encoding", "WinAnsiEncoding"); + dict->SetMatrixFor("FontMatrix", CFX_Matrix(0.001f, 0, 0, 0.001f, 0, 0)); + dict->SetRectFor("FontBBox", CFX_FloatRect(0, 0, 600, 600)); + auto char_procs = dict->SetNewFor("CharProcs"); + char_procs->SetNewFor("A", document, stream->GetObjNum()); + return CPDF_Font::Create(document, std::move(dict), factory); +} + +} // namespace + +TEST_F(CPDFType3FontTest, IntentVariantsPreserveExplicitIntentAndSavedState) { + CPDF_TestDocument document; + CountingFormFactory factory; + auto font = MakeType3Font( + &document, &factory, + "600 0 d0 0 0 1 rg 0 0 20 20 re f " + "q /AbsoluteColorimetric ri 30 0 20 20 re f Q 60 0 20 20 re f"); + ASSERT_TRUE(font); + CPDF_Type3Font* type3 = font->AsType3Font(); + ASSERT_TRUE(type3); + std::array variants{}; + for (size_t i = 0; i < std::size(kIntents); ++i) { + variants[i] = type3->LoadChar(65, kIntents[i]); + ASSERT_TRUE(variants[i]); + const auto* form = static_cast(variants[i]->form()); + ASSERT_TRUE(form); + ASSERT_EQ(form->GetPageObjectCount(), 3u); + EXPECT_EQ(form->GetPageObjectByIndex(0)->general_state().GetRenderIntent(), + kIntents[i]); + EXPECT_EQ(form->GetPageObjectByIndex(1)->general_state().GetRenderIntent(), + RenderingIntent::kAbsoluteColorimetric); + EXPECT_EQ(form->GetPageObjectByIndex(2)->general_state().GetRenderIntent(), + kIntents[i]); + for (size_t j = 0; j < i; ++j) { + EXPECT_NE(variants[j], variants[i]); + } + } + EXPECT_EQ(factory.count, 4); + for (int repeat = 0; repeat < 32; ++repeat) { + for (size_t i = 0; i < std::size(kIntents); ++i) { + EXPECT_EQ(type3->LoadChar(65, kIntents[i]), variants[i]); + } + } + EXPECT_EQ(factory.count, 4); + // Metrics use the default variant and do not create a fifth entry. + EXPECT_EQ(type3->GetCharWidth(65), variants[1]->width()); + EXPECT_EQ(type3->GetCharBBox(65), variants[1]->bbox()); + EXPECT_EQ(factory.count, 4); + EXPECT_FALSE(type3->LoadChar(256, RenderingIntent::kPerceptual)); + EXPECT_FALSE(type3->LoadChar(65, static_cast(4))); + EXPECT_EQ(factory.count, 4); + // Earlier variants remain alive and retain their own inherited state. + const auto* first_form = static_cast(variants[0]->form()); + EXPECT_EQ(first_form->GetPageObjectByIndex(2)->general_state().GetRenderIntent(), + RenderingIntent::kPerceptual); +} + +TEST_F(CPDFType3FontTest, BitmapCacheUsesTheSameIntentVariantAsFontCache) { + CPDF_TestDocument document; + CountingFormFactory factory; + auto font = MakeType3Font( + &document, &factory, + "600 0 0 0 10 10 d1 10 0 0 10 0 0 cm " + "BI /W 1 /H 1 /IM true /BPC 1 ID \xff EI"); + ASSERT_TRUE(font); + CPDF_Type3Font* type3 = font->AsType3Font(); + ASSERT_TRUE(type3); + auto cache = pdfium::MakeRetain(type3); + const CFX_Matrix matrix(10, 0, 0, 10, 0, 0); + std::array bitmaps{}; + for (size_t i = 0; i < std::size(kIntents); ++i) { + auto* glyph = type3->LoadChar(65, kIntents[i]); + ASSERT_TRUE(glyph); + ASSERT_TRUE(glyph->LoadBitmapFromSoleImageOfForm()); + bitmaps[i] = cache->LoadGlyphBitmap(65, matrix, kIntents[i]); + ASSERT_TRUE(bitmaps[i]); + EXPECT_EQ(cache->LoadGlyphBitmap(65, matrix, kIntents[i]), bitmaps[i]); + for (size_t j = 0; j < i; ++j) { + EXPECT_NE(bitmaps[j], bitmaps[i]); + } + } + EXPECT_EQ(factory.count, 4); + for (size_t i = 0; i < std::size(kIntents); ++i) { + EXPECT_EQ(cache->LoadGlyphBitmap(65, matrix, kIntents[i]), bitmaps[i]); + } + EXPECT_EQ(factory.count, 4); + EXPECT_FALSE(cache->LoadGlyphBitmap(256, matrix, kIntents[0])); + EXPECT_FALSE(cache->LoadGlyphBitmap(65, matrix, static_cast(4))); +} diff --git a/core/fpdfapi/page/BUILD.gn b/core/fpdfapi/page/BUILD.gn --- a/core/fpdfapi/page/BUILD.gn +++ b/core/fpdfapi/page/BUILD.gn @@ -150,6 +150,7 @@ "cpdf_pageimagecache_unittest.cpp", "cpdf_pageobjectholder_unittest.cpp", "cpdf_psengine_unittest.cpp", + "cpdf_renderingintent_unittest.cpp", "cpdf_streamcontentparser_unittest.cpp", "cpdf_streamparser_unittest.cpp", ] diff --git a/core/fpdfapi/page/cpdf_allstates.cpp b/core/fpdfapi/page/cpdf_allstates.cpp --- a/core/fpdfapi/page/cpdf_allstates.cpp +++ b/core/fpdfapi/page/cpdf_allstates.cpp @@ -27,6 +27,11 @@ CPDF_AllStates::~CPDF_AllStates() = default; +void CPDF_AllStates::SetRenderIntent(const ByteString& name) { + mutable_general_state().SetRenderIntent(name); + mutable_color_state().SetRenderIntent(general_state().GetRenderIntent()); +} + void CPDF_AllStates::SetDefaultStates() { graphic_states_.SetDefaultStates(); } @@ -76,7 +81,9 @@ break; } case FXBSTR_ID('R', 'I', 0, 0): - mutable_general_state().SetRenderIntent(pObject->GetString()); + if (pObject->IsName()) { + SetRenderIntent(pObject->GetString()); + } break; case FXBSTR_ID('F', 'o', 'n', 't'): { const CPDF_Array* font = pObject->AsArray(); diff --git a/core/fpdfapi/page/cpdf_allstates.h b/core/fpdfapi/page/cpdf_allstates.h --- a/core/fpdfapi/page/cpdf_allstates.h +++ b/core/fpdfapi/page/cpdf_allstates.h @@ -22,6 +22,7 @@ ~CPDF_AllStates(); void SetDefaultStates(); + void SetRenderIntent(const ByteString& name); void ProcessExtGS(const CPDF_Dictionary* pGS, CPDF_StreamContentParser* pParser); diff --git a/core/fpdfapi/page/cpdf_color.cpp b/core/fpdfapi/page/cpdf_color.cpp --- a/core/fpdfapi/page/cpdf_color.cpp +++ b/core/fpdfapi/page/cpdf_color.cpp @@ -100,8 +100,9 @@ CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceGray); } -std::optional CPDF_Color::GetColorRef() const { - std::optional> maybe_rgb = GetRGB(); +std::optional CPDF_Color::GetColorRef( + RenderingIntent intent) const { + std::optional> maybe_rgb = GetRGB(intent); if (!maybe_rgb.has_value()) { return std::nullopt; } @@ -113,17 +114,18 @@ FXSYS_roundf(r * 255.0f)); } -std::optional> CPDF_Color::GetRGB() const { +std::optional> CPDF_Color::GetRGB( + RenderingIntent intent) const { if (IsPatternInternal()) { if (std::holds_alternative>(color_data_)) { const auto& pattern_value = std::get>(color_data_); - return cs_->AsPatternCS()->GetPatternRGB(*pattern_value); + return cs_->AsPatternCS()->GetPatternRGB(*pattern_value, intent); } } else { if (std::holds_alternative>(color_data_)) { const auto& buffer = std::get>(color_data_); - return cs_->GetRGB(buffer); + return cs_->GetRGB(buffer, intent); } } return std::nullopt; diff --git a/core/fpdfapi/page/cpdf_color.h b/core/fpdfapi/page/cpdf_color.h --- a/core/fpdfapi/page/cpdf_color.h +++ b/core/fpdfapi/page/cpdf_color.h @@ -14,6 +14,7 @@ #include #include +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxge/dib/fx_dib.h" @@ -44,8 +45,10 @@ // Wrapper around GetRGB() that returns the RGB value as FX_COLORREF. The // GetRGB() return value is clamped to fit into FX_COLORREF, where the color // components are 8-bit fields within an unsigned integer. - std::optional GetColorRef() const; - std::optional> GetRGB() const; + std::optional GetColorRef( + RenderingIntent intent = RenderingIntent::kRelativeColorimetric) const; + std::optional> GetRGB( + RenderingIntent intent = RenderingIntent::kRelativeColorimetric) const; // Should only be called if IsPattern() returns true. RetainPtr GetPattern() const; diff --git a/core/fpdfapi/page/cpdf_colorspace.cpp b/core/fpdfapi/page/cpdf_colorspace.cpp --- a/core/fpdfapi/page/cpdf_colorspace.cpp +++ b/core/fpdfapi/page/cpdf_colorspace.cpp @@ -126,7 +126,8 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, std::set* pVisited) override; @@ -135,7 +136,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const override; + bool bTransMask, + RenderingIntent intent) const override; private: static constexpr float kDefaultGamma = 1.0f; @@ -154,13 +156,15 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; void TranslateImageLine(pdfium::span dest_span, pdfium::span src_span, int pixels, int image_width, int image_height, - bool bTransMask) const override; + bool bTransMask, + RenderingIntent intent) const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, std::set* pVisited) override; @@ -184,7 +188,8 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; void GetDefaultValue(int iComponent, float* value, float* min, @@ -194,7 +199,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const override; + bool bTransMask, + RenderingIntent intent) const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, std::set* pVisited) override; @@ -216,14 +222,16 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; RetainPtr GetIccProfile() const override; void TranslateImageLine(pdfium::span dest_span, pdfium::span src_span, int pixels, int image_width, int image_height, - bool bTransMask) const override; + bool bTransMask, + RenderingIntent intent) const override; bool IsNormal() const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, @@ -253,7 +261,8 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; void GetDefaultValue(int iComponent, float* value, float* min, @@ -276,7 +285,8 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent) const override; void GetDefaultValue(int iComponent, float* value, float* min, @@ -638,7 +648,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { // Only applies to CMYK colorspaces. None of the colorspaces that use this // generic base implementation are CMYK. CHECK(!bTransMask); @@ -652,7 +663,7 @@ for (uint32_t j = 0; j < components_; j++) { src[j] = static_cast(*src_buf++) / divisor; } - auto rgb = GetRGBOrZerosOnError(src); + auto rgb = GetRGBOrZerosOnError(src, intent); *dest_buf++ = static_cast(rgb.blue * 255); *dest_buf++ = static_cast(rgb.green * 255); *dest_buf++ = static_cast(rgb.red * 255); @@ -717,7 +728,8 @@ } std::optional> CPDF_CalGray::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { const float gray = pBuf[0]; return FX_RGB_STRUCT{gray, gray, gray}; } @@ -727,7 +739,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { CHECK(!bTransMask); // Only applies to CMYK colorspaces. auto gray_span = src_span.first(static_cast(pixels)); @@ -778,7 +791,8 @@ } std::optional> CPDF_CalRGB::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { float a = pBuf[0]; float b = pBuf[1]; float c = pBuf[2]; @@ -810,7 +824,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { CHECK(!bTransMask); // Only applies to CMYK colorspaces. fxcodec::ReverseRGB(dest_span, src_span, pixels); } @@ -865,7 +880,8 @@ } std::optional> CPDF_LabCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { float Lstar = pBuf[0]; float astar = pBuf[1]; float bstar = pBuf[2]; @@ -901,7 +917,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { CHECK(!bTransMask); // Only applies to CMYK colorspaces. auto bgr_span = fxcrt::reinterpret_span>(dest_span); @@ -917,7 +934,7 @@ // Better code than the equivalent GetRGBOrZerosOnError() since that // is implemented in a base class and can't devirtualize the GetRGB() // call despite this class being marked final. - auto rgb = GetRGB(lab).value_or(FX_RGB_STRUCT{}); + auto rgb = GetRGB(lab, intent).value_or(FX_RGB_STRUCT{}); bgr_ref.blue = static_cast(rgb.blue * 255); bgr_ref.green = static_cast(rgb.green * 255); bgr_ref.red = static_cast(rgb.red * 255); @@ -969,17 +986,20 @@ } std::optional> CPDF_ICCBasedCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { if (profile_->IsSRGB()) { return FX_RGB_STRUCT{pBuf[0], pBuf[1], pBuf[2]}; } if (profile_->IsSupported()) { float rgb[3]; - profile_->Translate(pBuf.first(ComponentCount()), rgb); - return FX_RGB_STRUCT{rgb[0], rgb[1], rgb[2]}; + if (profile_->Translate(pBuf.first(ComponentCount()), rgb, intent)) { + return FX_RGB_STRUCT{rgb[0], rgb[1], rgb[2]}; + } + return std::nullopt; } if (base_cs_) { - return base_cs_->GetRGB(pBuf); + return base_cs_->GetRGB(pBuf, intent); } return FX_RGB_STRUCT{}; } @@ -993,7 +1013,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { CHECK(!bTransMask); // Only applies to CMYK colorspaces. if (profile_->IsSRGB()) { @@ -1003,12 +1024,14 @@ if (!profile_->IsSupported()) { if (base_cs_) { base_cs_->TranslateImageLine(dest_span, src_span, pixels, image_width, - image_height, false); + image_height, false, intent); } return; } - profile_->TranslateScanline(dest_span, src_span, pixels); + if (!profile_->TranslateScanline(dest_span, src_span, pixels, intent)) { + std::ranges::fill(dest_span.first(static_cast(pixels) * 3), 0); + } } bool CPDF_ICCBasedCS::IsNormal() const { @@ -1131,7 +1154,8 @@ } std::optional> CPDF_SeparationCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { if (is_none_type_) { return std::nullopt; } @@ -1140,7 +1164,7 @@ return std::nullopt; } std::vector results(base_cs_->ComponentCount(), pBuf[0]); - return base_cs_->GetRGB(results); + return base_cs_->GetRGB(results, intent); } // Using at least 16 elements due to the call alt_cs_->GetRGB() below. @@ -1150,7 +1174,7 @@ return std::nullopt; } if (base_cs_) { - return base_cs_->GetRGB(results); + return base_cs_->GetRGB(results, intent); } return std::nullopt; } @@ -1199,7 +1223,8 @@ } std::optional> CPDF_DeviceNCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { if (!func_) { return std::nullopt; } @@ -1211,5 +1236,5 @@ if (nresults == 0) { return std::nullopt; } - return base_cs_->GetRGB(results); -} + return base_cs_->GetRGB(results, intent); +} diff --git a/core/fpdfapi/page/cpdf_colorspace.h b/core/fpdfapi/page/cpdf_colorspace.h --- a/core/fpdfapi/page/cpdf_colorspace.h +++ b/core/fpdfapi/page/cpdf_colorspace.h @@ -21,6 +21,7 @@ #include "core/fpdfapi/parser/cpdf_object.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/observed_ptr.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" @@ -103,13 +104,16 @@ // Wrapper around GetRGB() that returns black (0, 0, 0) when an actual value // can not be determined. FX_RGB_STRUCT GetRGBOrZerosOnError( - pdfium::span pBuf) const { - return GetRGB(pBuf).value_or(FX_RGB_STRUCT{}); + pdfium::span pBuf, + RenderingIntent intent = RenderingIntent::kRelativeColorimetric) const { + return GetRGB(pBuf, intent).value_or(FX_RGB_STRUCT{}); } // Use CPDF_Pattern::GetPatternColorRef() instead of GetRGB() for patterns. virtual std::optional> GetRGB( - pdfium::span pBuf) const = 0; + pdfium::span pBuf, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric) const = 0; virtual RetainPtr GetIccProfile() const; @@ -118,12 +122,14 @@ float* min, float* max) const; - virtual void TranslateImageLine(pdfium::span dest_span, - pdfium::span src_span, - int pixels, - int image_width, - int image_height, - bool bTransMask) const; + virtual void TranslateImageLine( + pdfium::span dest_span, + pdfium::span src_span, + int pixels, + int image_width, + int image_height, + bool bTransMask, + RenderingIntent intent = RenderingIntent::kRelativeColorimetric) const; virtual void EnableStdConversion(bool bEnabled); virtual bool IsNormal() const; diff --git a/core/fpdfapi/page/cpdf_colorstate.cpp b/core/fpdfapi/page/cpdf_colorstate.cpp --- a/core/fpdfapi/page/cpdf_colorstate.cpp +++ b/core/fpdfapi/page/cpdf_colorstate.cpp @@ -26,6 +26,24 @@ void CPDF_ColorState::SetDefault() { ref_.GetPrivateCopy()->SetDefault(); +} + +void CPDF_ColorState::SetRenderIntent(RenderingIntent intent) { + if (ref_ && ref_.GetObject()->render_intent_ == intent) { + return; + } + ColorData* data = ref_.GetPrivateCopy(); + data->render_intent_ = intent; + if (!data->fill_color_.IsNull()) { + if (auto color = data->fill_color_.GetColorRef(intent)) { + data->fill_color_ref_ = *color; + } + } + if (!data->stroke_color_.IsNull()) { + if (auto color = data->stroke_color_.GetColorRef(intent)) { + data->stroke_color_ref_ = *color; + } + } } FX_COLORREF CPDF_ColorState::GetFillColorRef() const { @@ -127,14 +145,16 @@ if (!color.IsPattern()) { color.SetValueForNonPattern(std::move(values)); } - return color.GetColorRef().value_or(0xFFFFFFFF); + return color.GetColorRef(ref_.GetObject()->render_intent_) + .value_or(0xFFFFFFFF); } FX_COLORREF CPDF_ColorState::SetPattern(RetainPtr pattern, pdfium::span values, CPDF_Color& color) { color.SetValueForPattern(pattern, values); - std::optional colorref = color.GetColorRef(); + std::optional colorref = + color.GetColorRef(ref_.GetObject()->render_intent_); if (colorref.has_value()) { return colorref.value(); } @@ -146,7 +166,8 @@ CPDF_ColorState::ColorData::ColorData() = default; CPDF_ColorState::ColorData::ColorData(const ColorData& src) - : fill_color_ref_(src.fill_color_ref_), + : render_intent_(src.render_intent_), + fill_color_ref_(src.fill_color_ref_), stroke_color_ref_(src.stroke_color_ref_), fill_color_(src.fill_color_), stroke_color_(src.stroke_color_) {} @@ -154,6 +175,7 @@ CPDF_ColorState::ColorData::~ColorData() = default; void CPDF_ColorState::ColorData::SetDefault() { + render_intent_ = RenderingIntent::kRelativeColorimetric; fill_color_ref_ = 0; stroke_color_ref_ = 0; fill_color_.SetColorSpace( diff --git a/core/fpdfapi/page/cpdf_colorstate.h b/core/fpdfapi/page/cpdf_colorstate.h --- a/core/fpdfapi/page/cpdf_colorstate.h +++ b/core/fpdfapi/page/cpdf_colorstate.h @@ -27,6 +27,7 @@ void Emplace(); void SetDefault(); + void SetRenderIntent(RenderingIntent intent); FX_COLORREF GetFillColorRef() const; void SetFillColorRef(FX_COLORREF colorref); @@ -62,6 +63,7 @@ void SetDefault(); + RenderingIntent render_intent_ = RenderingIntent::kRelativeColorimetric; FX_COLORREF fill_color_ref_ = 0; FX_COLORREF stroke_color_ref_ = 0; CPDF_Color fill_color_; diff --git a/core/fpdfapi/page/cpdf_devicecs.cpp b/core/fpdfapi/page/cpdf_devicecs.cpp --- a/core/fpdfapi/page/cpdf_devicecs.cpp +++ b/core/fpdfapi/page/cpdf_devicecs.cpp @@ -44,7 +44,8 @@ } std::optional> CPDF_DeviceCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { switch (GetFamily()) { case Family::kDeviceGray: { const float pix = NormalizeChannel(pBuf.front()); @@ -83,7 +84,8 @@ int pixels, int image_width, int image_height, - bool bTransMask) const { + bool bTransMask, + RenderingIntent intent) const { auto rgb_out = fxcrt::reinterpret_span>(dest_span); switch (GetFamily()) { case Family::kDeviceGray: diff --git a/core/fpdfapi/page/cpdf_devicecs.h b/core/fpdfapi/page/cpdf_devicecs.h --- a/core/fpdfapi/page/cpdf_devicecs.h +++ b/core/fpdfapi/page/cpdf_devicecs.h @@ -19,13 +19,18 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; - void TranslateImageLine(pdfium::span dest_span, - pdfium::span src_span, - int pixels, - int image_width, - int image_height, - bool bTransMask) const override; + pdfium::span pBuf, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric) const override; + void TranslateImageLine( + pdfium::span dest_span, + pdfium::span src_span, + int pixels, + int image_width, + int image_height, + bool bTransMask, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric) const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, std::set* pVisited) override; diff --git a/core/fpdfapi/page/cpdf_dib.cpp b/core/fpdfapi/page/cpdf_dib.cpp --- a/core/fpdfapi/page/cpdf_dib.cpp +++ b/core/fpdfapi/page/cpdf_dib.cpp @@ -138,7 +138,36 @@ CPDF_DIB::JpxSMaskInlineData::~JpxSMaskInlineData() = default; +// static +RenderingIntent CPDF_DIB::ResolveRenderingIntent( + const CPDF_Dictionary* dict, RenderingIntent inherited_intent) { + if (!dict) { + return inherited_intent; + } + RetainPtr value = dict->GetDirectObjectFor("Intent"); + if (!value || !value->IsName()) { + return inherited_intent; + } + const ByteString name = value->GetString(); + if (name == "Perceptual") { + return RenderingIntent::kPerceptual; + } + if (name == "RelativeColorimetric") { + return RenderingIntent::kRelativeColorimetric; + } + if (name == "Saturation") { + return RenderingIntent::kSaturation; + } + if (name == "AbsoluteColorimetric") { + return RenderingIntent::kAbsoluteColorimetric; + } + // An unrecognized intent name falls back to the PDF default (not the + // inherited state); an absent or non-name entry above keeps inheritance. + return RenderingIntent::kRelativeColorimetric; +} + bool CPDF_DIB::Load() { + rendering_intent_ = RenderingIntent::kRelativeColorimetric; if (!LoadInternal(nullptr, nullptr)) { return false; } @@ -204,7 +233,9 @@ bool bStdCS, CPDF_ColorSpace::Family GroupFamily, bool bLoadMask, - const CFX_Size& max_size_required) { + const CFX_Size& max_size_required, + RenderingIntent intent) { + rendering_intent_ = intent; std_cs_ = bStdCS; has_mask_ = bHasMask; group_family_ = GroupFamily; @@ -813,6 +844,7 @@ } dict_ = stream_->GetDict(); + rendering_intent_ = ResolveRenderingIntent(dict_.Get(), rendering_intent_); SetWidth(dict_->GetIntegerFor("Width")); SetHeight(dict_->GetIntegerFor("Height")); if (!IsValidDimension(GetWidth()) || !IsValidDimension(GetHeight())) { @@ -873,7 +905,7 @@ std::vector colors = ReadArrayElementsToVector(pMatte.Get(), components_); - auto rgb = color_space_->GetRGBOrZerosOnError(colors); + auto rgb = color_space_->GetRGBOrZerosOnError(colors, rendering_intent_); matte_color_ = ArgbEncode(0, FXSYS_roundf(rgb.red * 255), FXSYS_roundf(rgb.green * 255), FXSYS_roundf(rgb.blue * 255)); @@ -915,7 +947,8 @@ mask_ = pdfium::MakeRetain(document_, std::move(mask_stream)); LoadState ret = mask_->StartLoadDIBBase(false, nullptr, nullptr, true, - CPDF_ColorSpace::Family::kUnknown, false, {0, 0}); + CPDF_ColorSpace::Family::kUnknown, false, {0, 0}, + rendering_intent_); if (ret == LoadState::kContinue) { if (status_ == LoadState::kFail) { status_ = LoadState::kContinue; @@ -957,7 +990,7 @@ float color_values[3]; std::ranges::fill(color_values, comp_data_[0].decode_min_); - auto rgb = color_space_->GetRGBOrZerosOnError(color_values); + auto rgb = color_space_->GetRGBOrZerosOnError(color_values, rendering_intent_); FX_ARGB argb0 = ArgbEncode(255, FXSYS_roundf(rgb.red * 255), FXSYS_roundf(rgb.green * 255), FXSYS_roundf(rgb.blue * 255)); @@ -972,7 +1005,7 @@ color_values[0] += comp_data_[0].decode_step_; color_values[1] += comp_data_[0].decode_step_; color_values[2] += comp_data_[0].decode_step_; - auto result = color_space_->GetRGBOrZerosOnError(color_values); + auto result = color_space_->GetRGBOrZerosOnError(color_values, rendering_intent_); argb1 = ArgbEncode(255, FXSYS_roundf(result.red * 255), FXSYS_roundf(result.green * 255), FXSYS_roundf(result.blue * 255)); @@ -1006,9 +1039,9 @@ color_space_->ComponentCount() > 1) { const size_t nComponents = color_space_->ComponentCount(); std::vector temp_buf(nComponents, color_values[0]); - rgb = color_space_->GetRGBOrZerosOnError(temp_buf); + rgb = color_space_->GetRGBOrZerosOnError(temp_buf, rendering_intent_); } else { - rgb = color_space_->GetRGBOrZerosOnError(color_values); + rgb = color_space_->GetRGBOrZerosOnError(color_values, rendering_intent_); } SetPaletteArgb(i, ArgbEncode(255, FXSYS_roundf(rgb.red * 255), FXSYS_roundf(rgb.green * 255), @@ -1078,7 +1111,7 @@ rgb.green = (1.0f - color_values[1]) * k; rgb.blue = (1.0f - color_values[2]) * k; } else if (family_ != CPDF_ColorSpace::Family::kPattern) { - rgb = color_space_->GetRGBOrZerosOnError(color_values); + rgb = color_space_->GetRGBOrZerosOnError(color_values, rendering_intent_); } const float R = std::clamp(rgb.red, 0.0f, 1.0f); const float G = std::clamp(rgb.green, 0.0f, 1.0f); @@ -1105,7 +1138,8 @@ if (ComponentCountMatchesColorSpace()) { color_space_->TranslateImageLine(dest_scan, src_scan, GetWidth(), - GetWidth(), GetHeight(), TransMask()); + GetWidth(), GetHeight(), TransMask(), + rendering_intent_); } return true; } diff --git a/core/fpdfapi/page/cpdf_dib.h b/core/fpdfapi/page/cpdf_dib.h --- a/core/fpdfapi/page/cpdf_dib.h +++ b/core/fpdfapi/page/cpdf_dib.h @@ -14,6 +14,7 @@ #include "core/fpdfapi/page/cpdf_colorspace.h" #include "core/fxcrt/data_vector.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" @@ -56,6 +57,11 @@ uint32_t GetMatteColor() const { return matte_color_; } bool IsJBigImage() const; + // Missing or non-name image Intent preserves graphics-state inheritance. + // Standard names select that intent; unknown names use the PDF default. + static RenderingIntent ResolveRenderingIntent( + const CPDF_Dictionary* dict, RenderingIntent inherited_intent); + bool Load(); LoadState StartLoadDIBBase(bool bHasMask, const CPDF_Dictionary* pFormResources, @@ -63,7 +69,9 @@ bool bStdCS, CPDF_ColorSpace::Family GroupFamily, bool bLoadMask, - const CFX_Size& max_size_required); + const CFX_Size& max_size_required, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric); LoadState ContinueLoadDIBBase(PauseIndicatorIface* pPause); RetainPtr DetachMask(); @@ -129,6 +137,7 @@ uint32_t components_ = 0; CPDF_ColorSpace::Family family_ = CPDF_ColorSpace::Family::kUnknown; CPDF_ColorSpace::Family group_family_ = CPDF_ColorSpace::Family::kUnknown; + RenderingIntent rendering_intent_ = RenderingIntent::kRelativeColorimetric; uint32_t matte_color_ = 0; LoadState status_ = LoadState::kFail; bool load_mask_ = false; diff --git a/core/fpdfapi/page/cpdf_dib_unittest.cpp b/core/fpdfapi/page/cpdf_dib_unittest.cpp --- a/core/fpdfapi/page/cpdf_dib_unittest.cpp +++ b/core/fpdfapi/page/cpdf_dib_unittest.cpp @@ -13,6 +13,7 @@ #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/cpdf_test_document.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" @@ -90,3 +91,39 @@ EXPECT_THAT(dib->GetScanline(0), testing::ElementsAre(0x55, 0x33, 0x11, 0xbb, 0x99, 0x77)); } + +TEST_F(CPDFDIBTest, ImageIntentDistinguishesUnknownNameFromMissingOrInvalidType) { + auto dict = pdfium::MakeRetain(); + constexpr RenderingIntent inherited = RenderingIntent::kSaturation; + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(nullptr, inherited), inherited); + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(dict.Get(), inherited), inherited); + struct IntentCase { + const char* name; + RenderingIntent intent; + }; + const IntentCase cases[] = { + {"Perceptual", RenderingIntent::kPerceptual}, + {"RelativeColorimetric", RenderingIntent::kRelativeColorimetric}, + {"Saturation", RenderingIntent::kSaturation}, + {"AbsoluteColorimetric", RenderingIntent::kAbsoluteColorimetric}, + }; + for (const auto& test : cases) { + dict->SetNewFor("Intent", test.name); + for (const auto& source : cases) { + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(dict.Get(), source.intent), + test.intent); + } + } + // Unknown names fall back to RelativeColorimetric, including old + // four-character prefix matches. PDF names are case sensitive. + for (const char* name : {"Unknown", "Abso", "AbsoluteColorimetricSuffix", + "perceptual", ""}) { + dict->SetNewFor("Intent", name); + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(dict.Get(), inherited), + RenderingIntent::kRelativeColorimetric); + } + dict->SetNewFor("Intent", 3); + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(dict.Get(), inherited), inherited); + dict->SetNewFor("Intent", "Perceptual"); + EXPECT_EQ(CPDF_DIB::ResolveRenderingIntent(dict.Get(), inherited), inherited); +} diff --git a/core/fpdfapi/page/cpdf_form.cpp b/core/fpdfapi/page/cpdf_form.cpp --- a/core/fpdfapi/page/cpdf_form.cpp +++ b/core/fpdfapi/page/cpdf_form.cpp @@ -9,6 +9,7 @@ #include #include +#include "core/fpdfapi/page/cpdf_allstates.h" #include "core/fpdfapi/page/cpdf_contentparser.h" #include "core/fpdfapi/page/cpdf_imageobject.h" #include "core/fpdfapi/page/cpdf_pageobject.h" @@ -70,8 +71,19 @@ ParseContentInternal(pGraphicStates, pParentMatrix, nullptr, recursion_state); } -void CPDF_Form::ParseContentForType3Char(CPDF_Type3Char* pType3Char) { - ParseContentInternal(nullptr, nullptr, pType3Char, nullptr); +void CPDF_Form::ParseContentForType3Char(CPDF_Type3Char* pType3Char, + RenderingIntent intent) { + // Retain the existing empty glyph color/graphics state, inheriting only RI. + // Parsing with that initial RI lets ordinary ri and q/Q operators override + // and restore it, without mutating an already-cached glyph object graph. + CPDF_AllStates states; + states.mutable_general_state().Emplace(); + states.mutable_graph_state().Emplace(); + states.mutable_text_state().Emplace(); + states.mutable_color_state().Emplace(); + states.mutable_general_state().SetRenderIntent(intent); + states.mutable_color_state().SetRenderIntent(intent); + ParseContentInternal(&states, nullptr, pType3Char, nullptr); } void CPDF_Form::ParseContentInternal(const CPDF_AllStates* pGraphicStates, diff --git a/core/fpdfapi/page/cpdf_form.h b/core/fpdfapi/page/cpdf_form.h --- a/core/fpdfapi/page/cpdf_form.h +++ b/core/fpdfapi/page/cpdf_form.h @@ -46,7 +46,8 @@ ~CPDF_Form() override; // CPDF_Font::FormIface: - void ParseContentForType3Char(CPDF_Type3Char* pType3Char) override; + void ParseContentForType3Char(CPDF_Type3Char* pType3Char, + RenderingIntent intent) override; bool HasPageObjects() const override; CFX_FloatRect CalcBoundingBox() const override; std::optional, CFX_Matrix>> diff --git a/core/fpdfapi/page/cpdf_generalstate.cpp b/core/fpdfapi/page/cpdf_generalstate.cpp --- a/core/fpdfapi/page/cpdf_generalstate.cpp +++ b/core/fpdfapi/page/cpdf_generalstate.cpp @@ -13,23 +13,6 @@ #include "core/fpdfapi/parser/cpdf_object.h" namespace { - -int RI_StringToId(const ByteString& ri) { - uint32_t id = ri.GetID(); - if (id == FXBSTR_ID('A', 'b', 's', 'o')) { - return 1; - } - - if (id == FXBSTR_ID('S', 'a', 't', 'u')) { - return 2; - } - - if (id == FXBSTR_ID('P', 'e', 'r', 'c')) { - return 3; - } - - return 0; -} BlendMode GetBlendTypeInternal(const ByteString& mode) { switch (mode.GetID()) { @@ -80,8 +63,32 @@ CPDF_GeneralState::~CPDF_GeneralState() = default; +RenderingIntent CPDF_GeneralState::GetRenderIntent() const { + return ref_ ? ref_.GetObject()->render_intent_ + : RenderingIntent::kRelativeColorimetric; +} + void CPDF_GeneralState::SetRenderIntent(const ByteString& ri) { - ref_.GetPrivateCopy()->render_intent_ = RI_StringToId(ri); + RenderingIntent intent; + if (ri == "Perceptual") { + intent = RenderingIntent::kPerceptual; + } else if (ri == "RelativeColorimetric") { + intent = RenderingIntent::kRelativeColorimetric; + } else if (ri == "Saturation") { + intent = RenderingIntent::kSaturation; + } else if (ri == "AbsoluteColorimetric") { + intent = RenderingIntent::kAbsoluteColorimetric; + } else { + // PDF requires unknown intent names to use RelativeColorimetric. + intent = RenderingIntent::kRelativeColorimetric; + } + SetRenderIntent(intent); +} + +void CPDF_GeneralState::SetRenderIntent(RenderingIntent intent) { + if (intent != GetRenderIntent()) { + ref_.GetPrivateCopy()->render_intent_ = intent; + } } ByteString CPDF_GeneralState::GetBlendMode() const { diff --git a/core/fpdfapi/page/cpdf_generalstate.h b/core/fpdfapi/page/cpdf_generalstate.h --- a/core/fpdfapi/page/cpdf_generalstate.h +++ b/core/fpdfapi/page/cpdf_generalstate.h @@ -12,6 +12,7 @@ #include "constants/transparency.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/shared_copy_on_write.h" #include "core/fxcrt/span.h" @@ -30,7 +31,9 @@ void Emplace() { ref_.Emplace(); } bool HasRef() const { return !!ref_; } + RenderingIntent GetRenderIntent() const; void SetRenderIntent(const ByteString& ri); + void SetRenderIntent(RenderingIntent intent); ByteString GetBlendMode() const; BlendMode GetBlendType() const; @@ -98,7 +101,7 @@ float fill_alpha_ = 1.0f; RetainPtr tr_; RetainPtr transfer_func_; - int render_intent_ = 0; + RenderingIntent render_intent_ = RenderingIntent::kRelativeColorimetric; bool stroke_adjust_ = false; bool alpha_source_ = false; bool text_knockout_ = false; diff --git a/core/fpdfapi/page/cpdf_iccprofile.cpp b/core/fpdfapi/page/cpdf_iccprofile.cpp --- a/core/fpdfapi/page/cpdf_iccprofile.cpp +++ b/core/fpdfapi/page/cpdf_iccprofile.cpp @@ -42,24 +42,51 @@ } src_components_ = components; - transform_ = std::move(transform); + const size_t index = + static_cast(RenderingIntent::kRelativeColorimetric); + transforms_[index] = std::move(transform); + transform_attempted_[index] = true; } CPDF_IccProfile::~CPDF_IccProfile() = default; bool CPDF_IccProfile::IsNormal() const { - return transform_->IsNormal(); + return transforms_[static_cast( + RenderingIntent::kRelativeColorimetric)] + ->IsNormal(); } -void CPDF_IccProfile::Translate(pdfium::span src_values, - pdfium::span dest_values) { - transform_->Translate(src_values, dest_values); +fxcodec::IccTransform* CPDF_IccProfile::GetTransform(RenderingIntent intent) { + const size_t index = static_cast(intent); + if (!transform_attempted_[index]) { + transform_attempted_[index] = true; + transforms_[index] = fxcodec::IccTransform::CreateTransformSRGB( + stream_acc_->GetSpan(), intent); + } + return transforms_[index].get(); } -void CPDF_IccProfile::TranslateScanline(pdfium::span pDest, - pdfium::span pSrc, - int pixels) { - transform_->TranslateScanline(pDest, pSrc, pixels); +bool CPDF_IccProfile::Translate(pdfium::span src_values, + pdfium::span dest_values, + RenderingIntent intent) { + auto* transform = GetTransform(intent); + if (!transform) { + return false; + } + transform->Translate(src_values, dest_values); + return true; +} + +bool CPDF_IccProfile::TranslateScanline(pdfium::span dest, + pdfium::span src, + int pixels, + RenderingIntent intent) { + auto* transform = GetTransform(intent); + if (!transform) { + return false; + } + transform->TranslateScanline(dest, src, pixels); + return true; } RetainPtr CPDF_IccProfile::GetStreamAcc() const { diff --git a/core/fpdfapi/page/cpdf_iccprofile.h b/core/fpdfapi/page/cpdf_iccprofile.h --- a/core/fpdfapi/page/cpdf_iccprofile.h +++ b/core/fpdfapi/page/cpdf_iccprofile.h @@ -9,8 +9,10 @@ #include +#include #include +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" @@ -26,15 +28,20 @@ bool IsValid() const { return IsSRGB() || IsSupported(); } bool IsSRGB() const { return is_srgb_; } - bool IsSupported() const { return !!transform_; } + bool IsSupported() const { + return !!transforms_[static_cast( + RenderingIntent::kRelativeColorimetric)]; + } uint32_t GetComponents() const { return src_components_; } bool IsNormal() const; - void Translate(pdfium::span src_values, - pdfium::span dest_values); - void TranslateScanline(pdfium::span pDest, + bool Translate(pdfium::span src_values, + pdfium::span dest_values, + RenderingIntent intent); + bool TranslateScanline(pdfium::span pDest, pdfium::span pSrc, - int pixels); + int pixels, + RenderingIntent intent); RetainPtr GetStreamAcc() const; @@ -43,11 +50,14 @@ uint32_t expected_components); ~CPDF_IccProfile() override; - // Keeps stream alive for the lifetime of this object, so `transform_` can + fxcodec::IccTransform* GetTransform(RenderingIntent intent); + + // Keeps stream alive for the lifetime of this object, so `transforms_` can // safely access the stream data. RetainPtr const stream_acc_; // Uses data from `stream_acc_`. - std::unique_ptr transform_; + std::array, 4> transforms_; + std::array transform_attempted_ = {}; const bool is_srgb_; uint32_t src_components_ = 0; }; diff --git a/core/fpdfapi/page/cpdf_image.cpp b/core/fpdfapi/page/cpdf_image.cpp --- a/core/fpdfapi/page/cpdf_image.cpp +++ b/core/fpdfapi/page/cpdf_image.cpp @@ -363,11 +363,12 @@ bool bStdCS, CPDF_ColorSpace::Family GroupFamily, bool bLoadMask, - const CFX_Size& max_size_required) { + const CFX_Size& max_size_required, + RenderingIntent intent) { RetainPtr source = CreateNewDIB(); CPDF_DIB::LoadState ret = source->StartLoadDIBBase(true, pFormResource, pPageResource, bStdCS, - GroupFamily, bLoadMask, max_size_required); + GroupFamily, bLoadMask, max_size_required, intent); if (ret == CPDF_DIB::LoadState::kFail) { dibbase_.Reset(); return false; diff --git a/core/fpdfapi/page/cpdf_image.h b/core/fpdfapi/page/cpdf_image.h --- a/core/fpdfapi/page/cpdf_image.h +++ b/core/fpdfapi/page/cpdf_image.h @@ -10,6 +10,7 @@ #include #include "core/fpdfapi/page/cpdf_colorspace.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" @@ -66,7 +67,9 @@ bool bStdCS, CPDF_ColorSpace::Family GroupFamily, bool bLoadMask, - const CFX_Size& max_size_required); + const CFX_Size& max_size_required, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric); // Returns whether to Continue() or not. bool Continue(PauseIndicatorIface* pPause); diff --git a/core/fpdfapi/page/cpdf_imageloader.cpp b/core/fpdfapi/page/cpdf_imageloader.cpp --- a/core/fpdfapi/page/cpdf_imageloader.cpp +++ b/core/fpdfapi/page/cpdf_imageloader.cpp @@ -30,15 +30,16 @@ const CFX_Size& max_size_required) { cache_ = pPageImageCache; image_object_ = pImage; + const RenderingIntent intent = pImage->general_state().GetRenderIntent(); bool should_continue; if (cache_) { should_continue = cache_->StartGetCachedBitmap( image_object_->GetImage(), pFormResource, pPageResource, bStdCS, - eFamily, bLoadMask, max_size_required); + eFamily, bLoadMask, max_size_required, intent); } else { should_continue = image_object_->GetImage()->StartLoadDIBBase( pFormResource, pPageResource, bStdCS, eFamily, bLoadMask, - max_size_required); + max_size_required, intent); } if (!should_continue) { Finish(); diff --git a/core/fpdfapi/page/cpdf_indexedcs.cpp b/core/fpdfapi/page/cpdf_indexedcs.cpp --- a/core/fpdfapi/page/cpdf_indexedcs.cpp +++ b/core/fpdfapi/page/cpdf_indexedcs.cpp @@ -89,7 +89,8 @@ } std::optional> CPDF_IndexedCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { int32_t index = static_cast(pBuf[0]); if (index < 0 || index > max_index_) { return std::nullopt; @@ -112,5 +113,5 @@ comp.min + comp.max * lookup_table_[index * component_min_max_.size() + i] / 255; } - return base_cs_->GetRGB(comps); + return base_cs_->GetRGB(comps, intent); } diff --git a/core/fpdfapi/page/cpdf_indexedcs.h b/core/fpdfapi/page/cpdf_indexedcs.h --- a/core/fpdfapi/page/cpdf_indexedcs.h +++ b/core/fpdfapi/page/cpdf_indexedcs.h @@ -29,7 +29,9 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric) const override; const CPDF_IndexedCS* AsIndexedCS() const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, diff --git a/core/fpdfapi/page/cpdf_meshstream.cpp b/core/fpdfapi/page/cpdf_meshstream.cpp --- a/core/fpdfapi/page/cpdf_meshstream.cpp +++ b/core/fpdfapi/page/cpdf_meshstream.cpp @@ -102,11 +102,13 @@ ShadingType type, const std::vector>& funcs, RetainPtr pShadingStream, - RetainPtr pCS) + RetainPtr pCS, + RenderingIntent rendering_intent) : type_(type), funcs_(funcs), shading_stream_(std::move(pShadingStream)), cs_(std::move(pCS)), + rendering_intent_(rendering_intent), stream_(pdfium::MakeRetain(shading_stream_)) {} CPDF_MeshStream::~CPDF_MeshStream() = default; @@ -215,7 +217,7 @@ component_max_; } if (funcs_.empty()) { - return cs_->GetRGBOrZerosOnError(color_value); + return cs_->GetRGBOrZerosOnError(color_value, rendering_intent_); } return {color_value[0], 0.0f, 0.0f}; } diff --git a/core/fpdfapi/page/cpdf_meshstream.h b/core/fpdfapi/page/cpdf_meshstream.h --- a/core/fpdfapi/page/cpdf_meshstream.h +++ b/core/fpdfapi/page/cpdf_meshstream.h @@ -14,6 +14,7 @@ #include #include "core/fpdfapi/page/cpdf_shadingpattern.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxge/dib/fx_dib.h" @@ -40,7 +41,9 @@ CPDF_MeshStream(ShadingType type, const std::vector>& funcs, RetainPtr pShadingStream, - RetainPtr pCS); + RetainPtr pCS, + RenderingIntent rendering_intent = + RenderingIntent::kRelativeColorimetric); ~CPDF_MeshStream(); bool Load(); @@ -79,6 +82,7 @@ const std::vector>& funcs_; RetainPtr const shading_stream_; RetainPtr const cs_; + const RenderingIntent rendering_intent_; uint32_t coord_bits_ = 0; uint32_t component_bits_ = 0; uint32_t flag_bits_ = 0; diff --git a/core/fpdfapi/page/cpdf_pageimagecache.cpp b/core/fpdfapi/page/cpdf_pageimagecache.cpp --- a/core/fpdfapi/page/cpdf_pageimagecache.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache.cpp @@ -174,19 +174,32 @@ bool bStdCS, CPDF_ColorSpace::Family eFamily, bool bLoadMask, - const CFX_Size& max_size_required) { + const CFX_Size& max_size_required, + RenderingIntent intent) { // A cross-document image may have come from the embedder. if (page_->GetDocument() != pImage->GetDocument()) { return false; } RetainPtr pStream = pImage->GetStream(); - const auto it = image_cache_.find(pStream); + const RenderingIntent effective_intent = + CPDF_DIB::ResolveRenderingIntent(pImage->GetDict().Get(), intent); + auto it = image_cache_.find(pStream); + if (it != image_cache_.end() && + it->second->GetRenderingIntent() != effective_intent) { + // Keep one decoded variant per stream. ClearImageCacheEntry also accounts + // for its bytes and clears any borrowed current-entry pointer. Bitmap + // consumers retain their own references, so replacing this entry cannot + // alter pixels already being rendered. + ClearImageCacheEntry(pStream.Get()); + it = image_cache_.end(); + } cur_find_cache_ = it != image_cache_.end(); if (cur_find_cache_) { cur_image_cache_entry_ = it->second.get(); } else { - cur_image_cache_entry_ = std::make_unique(std::move(pImage)); + cur_image_cache_entry_ = + std::make_unique(std::move(pImage), effective_intent); } CPDF_DIB::LoadState ret = cur_image_cache_entry_->StartGetCachedBitmap( this, pFormResources, pPageResources, bStdCS, eFamily, bLoadMask, @@ -247,8 +260,9 @@ return cur_image_cache_entry_->DetachMask(); } -CPDF_PageImageCache::Entry::Entry(RetainPtr pImage) - : image_(std::move(pImage)) {} +CPDF_PageImageCache::Entry::Entry(RetainPtr pImage, + RenderingIntent intent) + : image_(std::move(pImage)), rendering_intent_(intent) {} CPDF_PageImageCache::Entry::~Entry() = default; @@ -282,7 +296,7 @@ cur_bitmap_ = image_->CreateNewDIB(); CPDF_DIB::LoadState ret = cur_bitmap_.AsRaw()->StartLoadDIBBase( true, pFormResources, pPageResources, bStdCS, eFamily, bLoadMask, - max_size_required); + max_size_required, rendering_intent_); cached_set_max_size_required_ = (max_size_required.width != 0 && max_size_required.height != 0); if (ret == CPDF_DIB::LoadState::kContinue) { diff --git a/core/fpdfapi/page/cpdf_pageimagecache.h b/core/fpdfapi/page/cpdf_pageimagecache.h --- a/core/fpdfapi/page/cpdf_pageimagecache.h +++ b/core/fpdfapi/page/cpdf_pageimagecache.h @@ -40,7 +40,9 @@ bool bStdCS, CPDF_ColorSpace::Family eFamily, bool bLoadMask, - const CFX_Size& max_size_required); + const CFX_Size& max_size_required, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric); bool Continue(PauseIndicatorIface* pPause); @@ -51,7 +53,7 @@ private: class Entry { public: - explicit Entry(RetainPtr pImage); + Entry(RetainPtr pImage, RenderingIntent intent); ~Entry(); void Reset(); @@ -60,6 +62,7 @@ uint32_t GetTimeCount() const { return time_count_; } void SetTimeCount(uint32_t count) { time_count_ = count; } CPDF_Image* GetImage() const { return image_.Get(); } + RenderingIntent GetRenderingIntent() const { return rendering_intent_; } CPDF_DIB::LoadState StartGetCachedBitmap( CPDF_PageImageCache* pPageImageCache, @@ -86,6 +89,7 @@ uint32_t matte_color_ = 0; uint32_t cache_size_ = 0; RetainPtr const image_; + const RenderingIntent rendering_intent_; RetainPtr cur_bitmap_; RetainPtr cur_mask_; RetainPtr cached_bitmap_; diff --git a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp --- a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp @@ -14,6 +14,12 @@ #include "core/fpdfapi/page/cpdf_page.h" #include "core/fpdfapi/page/cpdf_pagemodule.h" #include "core/fpdfapi/page/test_with_page_module.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_test_document.h" +#include "core/fxcrt/data_vector.h" #include "core/fpdfapi/parser/cpdf_parser.h" #include "core/fpdfapi/render/cpdf_docrenderdata.h" #include "core/fxcrt/cfx_fileaccess_stream.h" @@ -252,3 +258,74 @@ } } // namespace pdfium + +namespace pdfium { + +TEST_F(CPDFPageImageCacheTest, ImageIntentReplacesSingleStreamCacheVariant) { + CPDF_TestDocument document; + auto page = pdfium::MakeRetain( + &document, pdfium::MakeRetain()); + page->AddPageImageCache(); + CPDF_PageImageCache* cache = page->GetPageImageCache(); + auto dict = pdfium::MakeRetain(); + dict->SetNewFor("Type", "XObject"); + dict->SetNewFor("Subtype", "Image"); + dict->SetNewFor("ColorSpace", "DeviceRGB"); + dict->SetNewFor("Width", 1); + dict->SetNewFor("Height", 1); + dict->SetNewFor("BitsPerComponent", 8); + auto stream = pdfium::MakeRetain( + DataVector{0x11, 0x22, 0x33}, dict); + auto image = pdfium::MakeRetain(&document, stream); + const auto load = [&](RenderingIntent intent) { + bool more = cache->StartGetCachedBitmap( + image, nullptr, nullptr, false, CPDF_ColorSpace::Family::kUnknown, + false, {0, 0}, intent); + while (more) { + more = cache->Continue(nullptr); + } + return cache->DetachCurBitmap(); + }; + + RetainPtr relative = load(RenderingIntent::kRelativeColorimetric); + ASSERT_TRUE(relative); + EXPECT_EQ(load(RenderingIntent::kRelativeColorimetric).Get(), relative.Get()); + RetainPtr perceptual = load(RenderingIntent::kPerceptual); + ASSERT_TRUE(perceptual); + EXPECT_NE(perceptual.Get(), relative.Get()); + RetainPtr relative_again = + load(RenderingIntent::kRelativeColorimetric); + ASSERT_TRUE(relative_again); + EXPECT_NE(relative_again.Get(), relative.Get()); + EXPECT_NE(relative_again.Get(), perceptual.Get()); + // Entry replacement must not invalidate a bitmap held by an earlier loader. + EXPECT_EQ(relative->GetScanline(0)[0], 0x33); + EXPECT_EQ(perceptual->GetScanline(0)[2], 0x11); + + for (int i = 0; i < 128; ++i) { + const RenderingIntent intent = + i % 2 ? RenderingIntent::kPerceptual : RenderingIntent::kSaturation; + RetainPtr bitmap = load(intent); + ASSERT_TRUE(bitmap); + // The budget should contain exactly the active single-stream variant, + // without charging every old intent again or underflowing on eviction. + cache->CacheOptimization( + static_cast(bitmap->GetEstimatedImageMemoryBurden())); + EXPECT_EQ(load(intent).Get(), bitmap.Get()); + } + + // Explicit /Intent takes priority. Different graphics-state intents now + // resolve to the same variant and therefore reuse its decoded bitmap. + dict->SetNewFor("Intent", "AbsoluteColorimetric"); + RetainPtr explicit_intent = load(RenderingIntent::kPerceptual); + ASSERT_TRUE(explicit_intent); + EXPECT_EQ(load(RenderingIntent::kSaturation).Get(), explicit_intent.Get()); + EXPECT_EQ(load(RenderingIntent::kRelativeColorimetric).Get(), + explicit_intent.Get()); + cache->CacheOptimization(0); + EXPECT_NE(load(RenderingIntent::kPerceptual).Get(), explicit_intent.Get()); + EXPECT_EQ(explicit_intent->GetScanline(0)[1], 0x22); + page->ClearView(); +} + +} // namespace pdfium diff --git a/core/fpdfapi/page/cpdf_patterncs.cpp b/core/fpdfapi/page/cpdf_patterncs.cpp --- a/core/fpdfapi/page/cpdf_patterncs.cpp +++ b/core/fpdfapi/page/cpdf_patterncs.cpp @@ -48,7 +48,8 @@ } std::optional> CPDF_PatternCS::GetRGB( - pdfium::span pBuf) const { + pdfium::span pBuf, + RenderingIntent intent) const { NOTREACHED(); } @@ -57,10 +58,11 @@ } std::optional> CPDF_PatternCS::GetPatternRGB( - const PatternValue& value) const { + const PatternValue& value, + RenderingIntent intent) const { if (!base_cs_) { return std::nullopt; } - return base_cs_->GetRGB(value.GetComps()); + return base_cs_->GetRGB(value.GetComps(), intent); } diff --git a/core/fpdfapi/page/cpdf_patterncs.h b/core/fpdfapi/page/cpdf_patterncs.h --- a/core/fpdfapi/page/cpdf_patterncs.h +++ b/core/fpdfapi/page/cpdf_patterncs.h @@ -27,14 +27,17 @@ // CPDF_ColorSpace: std::optional> GetRGB( - pdfium::span pBuf) const override; + pdfium::span pBuf, + RenderingIntent intent = + RenderingIntent::kRelativeColorimetric) const override; const CPDF_PatternCS* AsPatternCS() const override; uint32_t v_Load(CPDF_Document* doc, const CPDF_Array* pArray, std::set* pVisited) override; std::optional> GetPatternRGB( - const PatternValue& value) const; + const PatternValue& value, + RenderingIntent intent = RenderingIntent::kRelativeColorimetric) const; private: CPDF_PatternCS(); diff --git a/core/fpdfapi/page/cpdf_renderingintent_unittest.cpp b/core/fpdfapi/page/cpdf_renderingintent_unittest.cpp --- /dev/null +++ b/core/fpdfapi/page/cpdf_renderingintent_unittest.cpp @@ -0,0 +1,92 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fpdfapi/page/cpdf_allstates.h" +#include "core/fpdfapi/page/cpdf_colorspace.h" +#include "core/fpdfapi/page/test_with_page_module.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// A deterministic color space makes the color-cache and copy-on-write behavior +// observable without relying on ICC tables or their precision. +class IntentColorSpace final : public CPDF_ColorSpace { + public: + IntentColorSpace() : CPDF_ColorSpace(Family::kDeviceRGB) {} + std::optional> GetRGB( + pdfium::span values, + RenderingIntent intent) const override { + return FX_RGB_STRUCT{static_cast(intent) / 3.0f, 0, 0}; + } + + private: + uint32_t v_Load(CPDF_Document*, + const CPDF_Array*, + std::set*) override { + return 3; + } +}; + +} // namespace + +using CPDFRenderingIntentTest = TestWithPageModule; + +TEST_F(CPDFRenderingIntentTest, DefaultAndExactNames) { + CPDF_GeneralState state; + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kRelativeColorimetric); + state.SetRenderIntent("Perceptual"); + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kPerceptual); + state.SetRenderIntent("Saturation"); + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kSaturation); + state.SetRenderIntent("AbsoluteColorimetric"); + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kAbsoluteColorimetric); + for (const char* name : {"AbsoluteColorimetricSuffix", "Rela", ""}) { + state.SetRenderIntent("AbsoluteColorimetric"); + state.SetRenderIntent(name); + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kRelativeColorimetric); + } + state.SetRenderIntent("RelativeColorimetric"); + EXPECT_EQ(state.GetRenderIntent(), RenderingIntent::kRelativeColorimetric); +} + +TEST_F(CPDFRenderingIntentTest, ColorCacheAndSavedStateAreIndependent) { + CPDF_AllStates state; + state.SetDefaultStates(); + auto colorspace = pdfium::MakeRetain(); + state.mutable_color_state().SetFillColor(colorspace, {0, 0, 0}); + state.mutable_color_state().SetStrokeColor(colorspace, {0, 0, 0}); + const auto initial_fill = state.color_state().GetFillColorRef(); + const auto initial_stroke = state.color_state().GetStrokeColorRef(); + CPDF_AllStates saved(state); + state.SetRenderIntent("Perceptual"); + EXPECT_NE(state.color_state().GetFillColorRef(), initial_fill); + EXPECT_NE(state.color_state().GetStrokeColorRef(), initial_stroke); + EXPECT_EQ(saved.color_state().GetFillColorRef(), initial_fill); + EXPECT_EQ(saved.color_state().GetStrokeColorRef(), initial_stroke); + EXPECT_EQ(saved.general_state().GetRenderIntent(), + RenderingIntent::kRelativeColorimetric); + state = saved; + EXPECT_EQ(state.color_state().GetFillColorRef(), initial_fill); + state.SetRenderIntent("Saturation"); + const auto saturation_fill = state.color_state().GetFillColorRef(); + state.mutable_color_state().SetFillColor(colorspace, {1, 0, 0}); + EXPECT_EQ(state.color_state().GetFillColorRef(), saturation_fill); + state.SetRenderIntent("PerceptualInvalid"); + EXPECT_EQ(state.color_state().GetFillColorRef(), initial_fill); +} + +TEST_F(CPDFRenderingIntentTest, EmptyColorsSurviveIntentChanges) { + CPDF_ColorState state; + state.SetRenderIntent(RenderingIntent::kPerceptual); + EXPECT_FALSE(state.HasFillColor()); + EXPECT_FALSE(state.HasStrokeColor()); + state.Emplace(); + state.SetRenderIntent(RenderingIntent::kAbsoluteColorimetric); + EXPECT_FALSE(state.HasFillColor()); + EXPECT_FALSE(state.HasStrokeColor()); + auto colorspace = pdfium::MakeRetain(); + state.SetFillColor(colorspace, {0, 0, 0}); + EXPECT_TRUE(state.HasFillColor()); + EXPECT_EQ(state.GetFillColorRef(), FXSYS_BGR(0, 0, 255)); +} diff --git a/core/fpdfapi/page/cpdf_shadingpattern.cpp b/core/fpdfapi/page/cpdf_shadingpattern.cpp --- a/core/fpdfapi/page/cpdf_shadingpattern.cpp +++ b/core/fpdfapi/page/cpdf_shadingpattern.cpp @@ -11,6 +11,7 @@ #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/page/cpdf_function.h" +#include "core/fpdfapi/page/cpdf_generalstate.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" @@ -97,6 +98,26 @@ : pattern_obj()->GetDict()->GetDirectObjectFor("Shading"); } +RenderingIntent CPDF_ShadingPattern::GetRenderIntent( + RenderingIntent inherited_intent) const { + if (shading_) { + return inherited_intent; + } + + RetainPtr dict = pattern_obj()->GetDict(); + RetainPtr state_dict = + dict ? dict->GetDictFor("ExtGState") : nullptr; + RetainPtr intent = + state_dict ? state_dict->GetDirectObjectFor("RI") : nullptr; + if (!intent || !intent->IsName()) { + return inherited_intent; + } + + CPDF_GeneralState state; + state.SetRenderIntent(intent->GetString()); + return state.GetRenderIntent(); +} + bool CPDF_ShadingPattern::Validate() const { if (shading_type_ == kInvalidShading) { return false; diff --git a/core/fpdfapi/page/cpdf_shadingpattern.h b/core/fpdfapi/page/cpdf_shadingpattern.h --- a/core/fpdfapi/page/cpdf_shadingpattern.h +++ b/core/fpdfapi/page/cpdf_shadingpattern.h @@ -55,6 +55,7 @@ ShadingType GetShadingType() const { return shading_type_; } bool IsShadingObject() const { return shading_; } RetainPtr GetShadingObject() const; + RenderingIntent GetRenderIntent(RenderingIntent inherited_intent) const; RetainPtr GetCS() const { return cs_; } const std::vector>& GetFuncs() const { return functions_; diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.cpp b/core/fpdfapi/page/cpdf_streamcontentparser.cpp --- a/core/fpdfapi/page/cpdf_streamcontentparser.cpp +++ b/core/fpdfapi/page/cpdf_streamcontentparser.cpp @@ -1094,7 +1094,12 @@ GetNumbers(3)); } -void CPDF_StreamContentParser::Handle_SetRenderIntent() {} +void CPDF_StreamContentParser::Handle_SetRenderIntent() { + RetainPtr name = GetObject(0); + if (name && name->IsName()) { + cur_states_->SetRenderIntent(name->GetString()); + } +} void CPDF_StreamContentParser::Handle_CloseStrokePath() { Handle_ClosePath(); diff --git a/core/fpdfapi/page/cpdf_tilingpattern.cpp b/core/fpdfapi/page/cpdf_tilingpattern.cpp --- a/core/fpdfapi/page/cpdf_tilingpattern.cpp +++ b/core/fpdfapi/page/cpdf_tilingpattern.cpp @@ -53,6 +53,8 @@ all_states.mutable_graph_state().Emplace(); all_states.mutable_text_state().Emplace(); all_states.mutable_general_state() = pPageObj->general_state(); + all_states.mutable_color_state().SetRenderIntent( + all_states.general_state().GetRenderIntent()); form->ParseContent(&all_states, &matrix, nullptr); bbox_ = dict->GetRectFor("BBox"); return form; diff --git a/core/fpdfapi/render/cpdf_rendershading.cpp b/core/fpdfapi/render/cpdf_rendershading.cpp --- a/core/fpdfapi/render/cpdf_rendershading.cpp +++ b/core/fpdfapi/render/cpdf_rendershading.cpp @@ -67,6 +67,7 @@ const std::vector>& funcs, const RetainPtr& pCS, int alpha, + RenderingIntent rendering_intent, std::array* output) { const uint32_t results_count = GetValidatedOutputsCount(funcs, pCS); if (results_count == 0) { @@ -91,7 +92,7 @@ result_span = result_span.subspan(nresults.value()); } } - auto rgb = pCS->GetRGBOrZerosOnError(result_array); + auto rgb = pCS->GetRGBOrZerosOnError(result_array, rendering_intent); shading_steps[i] = ArgbEncode(alpha, FXSYS_roundf(rgb.red * 255), FXSYS_roundf(rgb.green * 255), FXSYS_roundf(rgb.blue * 255)); @@ -111,7 +112,8 @@ const CPDF_Dictionary* dict, const std::vector>& funcs, const RetainPtr& pCS, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); RetainPtr pCoords = dict->GetArrayFor("Coords"); @@ -141,7 +143,7 @@ float axis_len_square = (x_span * x_span) + (y_span * y_span); std::array shading_steps; - if (!GetShadingSteps(t_min, t_max, funcs, pCS, alpha, &shading_steps)) { + if (!GetShadingSteps(t_min, t_max, funcs, pCS, alpha, rendering_intent, &shading_steps)) { return; } @@ -179,7 +181,8 @@ const CPDF_Dictionary* dict, const std::vector>& funcs, const RetainPtr& pCS, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); RetainPtr pCoords = dict->GetArrayFor("Coords"); @@ -205,7 +208,7 @@ const bool bEndExtend = pArray && pArray->GetBooleanAt(1, false); std::array shading_steps; - if (!GetShadingSteps(t_min, t_max, funcs, pCS, alpha, &shading_steps)) { + if (!GetShadingSteps(t_min, t_max, funcs, pCS, alpha, rendering_intent, &shading_steps)) { return; } @@ -279,7 +282,8 @@ const CPDF_Dictionary* dict, const std::vector>& funcs, const RetainPtr& pCS, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); const uint32_t total_results = GetValidatedOutputsCount(funcs, pCS); @@ -327,7 +331,7 @@ result_span = result_span.subspan(nresults.value()); } } - auto rgb = pCS->GetRGBOrZerosOnError(result_array); + auto rgb = pCS->GetRGBOrZerosOnError(result_array, rendering_intent); dib_buf[column] = ArgbEncode(alpha, static_cast(rgb.red * 255), static_cast(rgb.green * 255), static_cast(rgb.blue * 255)); @@ -461,11 +465,12 @@ RetainPtr pShadingStream, const std::vector>& funcs, RetainPtr pCS, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); CPDF_MeshStream stream(kFreeFormGouraudTriangleMeshShading, funcs, - std::move(pShadingStream), pCS); + std::move(pShadingStream), pCS, rendering_intent); if (!stream.Load()) { return; } @@ -473,7 +478,7 @@ std::array shading_steps; if (!funcs.empty()) { if (!GetShadingSteps(stream.component_min(0), stream.component_max(0), - funcs, pCS, alpha, &shading_steps)) { + funcs, pCS, alpha, rendering_intent, &shading_steps)) { return; } } @@ -522,7 +527,8 @@ RetainPtr pShadingStream, const std::vector>& funcs, RetainPtr pCS, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); int row_verts = pShadingStream->GetDict()->GetIntegerFor("VerticesPerRow"); @@ -531,7 +537,7 @@ } CPDF_MeshStream stream(kLatticeFormGouraudTriangleMeshShading, funcs, - std::move(pShadingStream), pCS); + std::move(pShadingStream), pCS, rendering_intent); if (!stream.Load()) { return; } @@ -539,7 +545,7 @@ std::array shading_steps; if (!funcs.empty()) { if (!GetShadingSteps(stream.component_min(0), stream.component_max(0), - funcs, pCS, alpha, &shading_steps)) { + funcs, pCS, alpha, rendering_intent, &shading_steps)) { return; } } @@ -853,7 +859,8 @@ const std::vector>& funcs, RetainPtr pCS, bool bNoPathSmooth, - int alpha) { + int alpha, + RenderingIntent rendering_intent) { DCHECK_EQ(pBitmap->GetFormat(), FXDIB_Format::kBgra); DCHECK(type == kCoonsPatchMeshShading || type == kTensorProductPatchMeshShading); @@ -864,7 +871,7 @@ return; } - CPDF_MeshStream stream(type, funcs, std::move(pShadingStream), pCS); + CPDF_MeshStream stream(type, funcs, std::move(pShadingStream), pCS, rendering_intent); if (!stream.Load()) { return; } @@ -872,7 +879,7 @@ std::array shading_steps; if (!funcs.empty()) { if (!GetShadingSteps(stream.component_min(0), stream.component_max(0), - funcs, pCS, alpha, &shading_steps)) { + funcs, pCS, alpha, rendering_intent, &shading_steps)) { return; } } @@ -1012,7 +1019,8 @@ const CFX_Matrix& mtMatrix, const FX_RECT& clip_rect, int alpha, - const CPDF_RenderOptions& options) { + const CPDF_RenderOptions& options, + RenderingIntent rendering_intent) { RetainPtr pColorSpace = pPattern->GetCS(); if (!pColorSpace) { return; @@ -1027,7 +1035,7 @@ std::vector comps = ReadArrayElementsToVector( pBackColor.Get(), pColorSpace->ComponentCount()); - auto rgb = pColorSpace->GetRGBOrZerosOnError(comps); + auto rgb = pColorSpace->GetRGBOrZerosOnError(comps, rendering_intent); background = ArgbEncode(255, static_cast(rgb.red * 255), static_cast(rgb.green * 255), static_cast(rgb.blue * 255)); @@ -1039,7 +1047,10 @@ mtMatrix.TransformRect(dict->GetRectFor("BBox")).GetOuterRect()); } #if defined(PDF_USE_SKIA) - if (pDevice->RenderCapShading() && + // The native shading API has no rendering-intent argument. Use the + // color-managed bitmap path when its default intent is not appropriate. + if (rendering_intent == RenderingIntent::kRelativeColorimetric && + pDevice->RenderCapShading() && pDevice->DrawShading(*pPattern, mtMatrix, clip_rect_bbox, alpha)) { return; } @@ -1061,15 +1072,15 @@ return; case kFunctionBasedShading: DrawFuncShading(pBitmap, final_matrix, dict.Get(), funcs, pColorSpace, - alpha); + alpha, rendering_intent); break; case kAxialShading: DrawAxialShading(pBitmap, final_matrix, dict.Get(), funcs, pColorSpace, - alpha); + alpha, rendering_intent); break; case kRadialShading: DrawRadialShading(pBitmap, final_matrix, dict.Get(), funcs, pColorSpace, - alpha); + alpha, rendering_intent); break; case kFreeFormGouraudTriangleMeshShading: { // The shading object can be a stream or a dictionary. We do not handle @@ -1078,7 +1089,7 @@ ToStream(pPattern->GetShadingObject()); if (pStream) { DrawFreeGouraudShading(pBitmap, final_matrix, std::move(pStream), funcs, - pColorSpace, alpha); + pColorSpace, alpha, rendering_intent); } break; } @@ -1089,7 +1100,7 @@ ToStream(pPattern->GetShadingObject()); if (pStream) { DrawLatticeGouraudShading(pBitmap, final_matrix, std::move(pStream), - funcs, pColorSpace, alpha); + funcs, pColorSpace, alpha, rendering_intent); } break; } @@ -1102,7 +1113,8 @@ if (pStream) { DrawCoonPatchMeshes(pPattern->GetShadingType(), pBitmap, final_matrix, std::move(pStream), funcs, pColorSpace, - options.GetOptions().bNoPathSmooth, alpha); + options.GetOptions().bNoPathSmooth, alpha, + rendering_intent); } break; } diff --git a/core/fpdfapi/render/cpdf_rendershading.h b/core/fpdfapi/render/cpdf_rendershading.h --- a/core/fpdfapi/render/cpdf_rendershading.h +++ b/core/fpdfapi/render/cpdf_rendershading.h @@ -6,6 +6,8 @@ #ifndef CORE_FPDFAPI_RENDER_CPDF_RENDERSHADING_H_ #define CORE_FPDFAPI_RENDER_CPDF_RENDERSHADING_H_ + +#include "core/fxcrt/rendering_intent.h" class CFX_Matrix; class CFX_RenderDevice; @@ -24,7 +26,8 @@ const CFX_Matrix& mtMatrix, const FX_RECT& clip_rect, int alpha, - const CPDF_RenderOptions& options); + const CPDF_RenderOptions& options, + RenderingIntent rendering_intent); CPDF_RenderShading() = delete; CPDF_RenderShading(const CPDF_RenderShading&) = delete; diff --git a/core/fpdfapi/render/cpdf_renderstatus.cpp b/core/fpdfapi/render/cpdf_renderstatus.cpp --- a/core/fpdfapi/render/cpdf_renderstatus.cpp +++ b/core/fpdfapi/render/cpdf_renderstatus.cpp @@ -20,6 +20,7 @@ #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/font/cpdf_type3char.h" #include "core/fpdfapi/font/cpdf_type3font.h" +#include "core/fpdfapi/page/cpdf_allstates.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/page/cpdf_form.h" #include "core/fpdfapi/page/cpdf_formobject.h" @@ -962,7 +963,8 @@ continue; } - CPDF_Type3Char* pType3Char = pType3Font->LoadChar(charcode); + const RenderingIntent intent = textobj->general_state().GetRenderIntent(); + CPDF_Type3Char* pType3Char = pType3Font->LoadChar(charcode, intent); if (!pType3Char) { continue; } @@ -1064,7 +1066,7 @@ CPDF_DocRenderData::FromDocument(doc)->GetCachedType3(pType3Font); const CFX_GlyphBitmap* pBitmap = - pCache->LoadGlyphBitmap(charcode, matrix); + pCache->LoadGlyphBitmap(charcode, matrix, intent); if (!pBitmap) { continue; } @@ -1164,6 +1166,7 @@ CPDF_PathObject path; path.mutable_graph_state() = textobj->graph_state(); path.mutable_color_state() = textobj->color_state(); + path.mutable_general_state() = textobj->general_state(); CFX_Matrix matrix = charpos.GetEffectiveMatrix(CFX_Matrix( font_size, 0, 0, font_size, charpos.origin_.x, charpos.origin_.y)); @@ -1200,7 +1203,8 @@ FXSYS_roundf(255 * (stroke ? pPageObj->general_state().GetStrokeAlpha() : pPageObj->general_state().GetFillAlpha())); CPDF_RenderShading::Draw(device_, context_, cur_obj_, pattern, matrix, rect, - alpha, options_); + alpha, options_, pattern->GetRenderIntent( + pPageObj->general_state().GetRenderIntent())); } void CPDF_RenderStatus::ProcessShading(const CPDF_ShadingObject* pShadingObj, @@ -1214,7 +1218,7 @@ CPDF_RenderShading::Draw( device_, context_, cur_obj_, pShadingObj->pattern(), matrix, rect, FXSYS_roundf(255 * pShadingObj->general_state().GetFillAlpha()), - options_); + options_, pShadingObj->general_state().GetRenderIntent()); } void CPDF_RenderStatus::DrawTilingPattern(CPDF_TilingPattern* pattern, @@ -1448,7 +1452,20 @@ CPDF_Form form(context_->GetDocument(), context_->GetMutablePageResources(), pGroup); - form.ParseContent(); + // Keep the parser's ordinary empty-stream initial state, inheriting only the + // color-conversion intent. In particular, do not inherit the active mask, + // alpha, clipping path, or current colors into the mask's content stream. + CPDF_AllStates mask_states; + mask_states.mutable_general_state().Emplace(); + mask_states.mutable_graph_state().Emplace(); + mask_states.mutable_text_state().Emplace(); + mask_states.mutable_color_state().Emplace(); + const RenderingIntent rendering_intent = + cur_obj_ ? cur_obj_->general_state().GetRenderIntent() + : initial_states_.general_state().GetRenderIntent(); + mask_states.mutable_general_state().SetRenderIntent(rendering_intent); + mask_states.mutable_color_state().SetRenderIntent(rendering_intent); + form.ParseContent(&mask_states, nullptr, nullptr); bool bLuminosity = smask_dict->GetByteStringFor(pdfium::transparency::kSoftMaskSubType) != @@ -1574,7 +1591,10 @@ std::vector floats = ReadArrayElementsToVector(pBC.Get(), count); floats.resize(comps); - auto rgb = pCS->GetRGBOrZerosOnError(floats); + const RenderingIntent rendering_intent = + cur_obj_ ? cur_obj_->general_state().GetRenderIntent() + : initial_states_.general_state().GetRenderIntent(); + auto rgb = pCS->GetRGBOrZerosOnError(floats, rendering_intent); return ArgbEncode(255, static_cast(rgb.red * 255), static_cast(rgb.green * 255), static_cast(rgb.blue * 255)); diff --git a/core/fpdfapi/render/cpdf_type3cache.cpp b/core/fpdfapi/render/cpdf_type3cache.cpp --- a/core/fpdfapi/render/cpdf_type3cache.cpp +++ b/core/fpdfapi/render/cpdf_type3cache.cpp @@ -82,12 +82,19 @@ const CFX_GlyphBitmap* CPDF_Type3Cache::LoadGlyphBitmap( uint32_t charcode, - const CFX_Matrix& mtMatrix) { + const CFX_Matrix& mtMatrix, + RenderingIntent intent) { + if (charcode >= 256 || static_cast(intent) > + static_cast( + RenderingIntent::kAbsoluteColorimetric)) { + return nullptr; + } SizeKey keygen = { FXSYS_roundf(mtMatrix.a * 10000), FXSYS_roundf(mtMatrix.b * 10000), FXSYS_roundf(mtMatrix.c * 10000), FXSYS_roundf(mtMatrix.d * 10000), + intent, }; CPDF_Type3GlyphMap* pSizeCache; auto it = size_map_.find(keygen); @@ -104,7 +111,7 @@ } std::unique_ptr pNewBitmap = - RenderGlyph(pSizeCache, charcode, mtMatrix); + RenderGlyph(pSizeCache, charcode, mtMatrix, intent); CFX_GlyphBitmap* pGlyphBitmap = pNewBitmap.get(); pSizeCache->SetBitmap(charcode, std::move(pNewBitmap)); return pGlyphBitmap; @@ -113,8 +120,9 @@ std::unique_ptr CPDF_Type3Cache::RenderGlyph( CPDF_Type3GlyphMap* pSize, uint32_t charcode, - const CFX_Matrix& mtMatrix) { - CPDF_Type3Char* pChar = font_->LoadChar(charcode); + const CFX_Matrix& mtMatrix, + RenderingIntent intent) { + CPDF_Type3Char* pChar = font_->LoadChar(charcode, intent); if (!pChar) { return nullptr; } diff --git a/core/fpdfapi/render/cpdf_type3cache.h b/core/fpdfapi/render/cpdf_type3cache.h --- a/core/fpdfapi/render/cpdf_type3cache.h +++ b/core/fpdfapi/render/cpdf_type3cache.h @@ -15,6 +15,7 @@ #include "core/fxcrt/bytestring.h" #include "core/fxcrt/observed_ptr.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/retain_ptr.h" class CFX_GlyphBitmap; @@ -27,17 +28,20 @@ CONSTRUCT_VIA_MAKE_RETAIN; const CFX_GlyphBitmap* LoadGlyphBitmap(uint32_t charcode, - const CFX_Matrix& mtMatrix); + const CFX_Matrix& mtMatrix, + RenderingIntent intent); private: - using SizeKey = std::tuple; + // Preserve the existing size key, with at most four RI variants per size. + using SizeKey = std::tuple; explicit CPDF_Type3Cache(CPDF_Type3Font* font); ~CPDF_Type3Cache() override; std::unique_ptr RenderGlyph(CPDF_Type3GlyphMap* pSize, uint32_t charcode, - const CFX_Matrix& mtMatrix); + const CFX_Matrix& mtMatrix, + RenderingIntent intent); RetainPtr const font_; std::map> size_map_; diff --git a/core/fxcodec/icc/icc_transform.cpp b/core/fxcodec/icc/icc_transform.cpp --- a/core/fxcodec/icc/icc_transform.cpp +++ b/core/fxcodec/icc/icc_transform.cpp @@ -54,7 +54,8 @@ // static std::unique_ptr IccTransform::CreateTransformSRGB( - pdfium::span span) { + pdfium::span span, + RenderingIntent intent) { ScopedCmsProfile srcProfile(cmsOpenProfileFromMem( span.data(), pdfium::checked_cast(span.size()))); if (!srcProfile) { @@ -96,9 +97,9 @@ cmsHTRANSFORM hTransform = nullptr; switch (dstCS) { case cmsSigRgbData: - hTransform = - cmsCreateTransform(srcProfile.get(), srcFormat, dstProfile.get(), - TYPE_BGR_8, INTENT_PERCEPTUAL, /*dwFlags=*/0); + hTransform = cmsCreateTransform( + srcProfile.get(), srcFormat, dstProfile.get(), TYPE_BGR_8, + static_cast(intent), /*dwFlags=*/0); break; case cmsSigGrayData: case cmsSigCmykData: diff --git a/core/fxcodec/icc/icc_transform.h b/core/fxcodec/icc/icc_transform.h --- a/core/fxcodec/icc/icc_transform.h +++ b/core/fxcodec/icc/icc_transform.h @@ -12,6 +12,7 @@ #include #include "core/fxcodec/fx_codec_def.h" +#include "core/fxcrt/rendering_intent.h" #include "core/fxcrt/span.h" #if defined(USE_SYSTEM_LCMS2) @@ -25,7 +26,8 @@ class IccTransform { public: static std::unique_ptr CreateTransformSRGB( - pdfium::span span); + pdfium::span span, + RenderingIntent intent = RenderingIntent::kRelativeColorimetric); ~IccTransform(); diff --git a/core/fxcrt/BUILD.gn b/core/fxcrt/BUILD.gn --- a/core/fxcrt/BUILD.gn +++ b/core/fxcrt/BUILD.gn @@ -113,6 +113,7 @@ "ptr_util.h", "raw_span.h", "retain_ptr.h", + "rendering_intent.h", "scoped_set_insertion.h", "shared_copy_on_write.h", "span.h", diff --git a/core/fxcrt/rendering_intent.h b/core/fxcrt/rendering_intent.h --- /dev/null +++ b/core/fxcrt/rendering_intent.h @@ -0,0 +1,16 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CORE_FXCRT_RENDERING_INTENT_H_ +#define CORE_FXCRT_RENDERING_INTENT_H_ + +// Values match the ICC rendering intents, as used by LittleCMS. +enum class RenderingIntent { + kPerceptual = 0, + kRelativeColorimetric = 1, + kSaturation = 2, + kAbsoluteColorimetric = 3, +}; + +#endif // CORE_FXCRT_RENDERING_INTENT_H_