| // Copyright 2025 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/jpx_decode_conversion.h" | |
| #include <optional> | |
| #include "core/fpdfapi/page/cpdf_colorspace.h" | |
| #include "core/fxcodec/jpx/cjpx_decoder.h" | |
| #include "core/fxcrt/check_op.h" | |
| #include "core/fxcrt/notreached.h" | |
| #include "core/fxcrt/retain_ptr.h" | |
| #include "core/fxge/dib/fx_dib.h" | |
| namespace { | |
| enum class JpxDecodeAction { | |
| kDoNothing, | |
| kUseGray, | |
| kUseIndexed, | |
| kUseRgb, | |
| kUseCmyk, | |
| kConvertArgbToRgb, | |
| }; | |
| // ISO 32000-1:2008 section 7.4.9 says the PDF and JPX colorspaces should have | |
| // the same number of color channels. This helper function checks the | |
| // colorspaces match, but also tolerates unknowns. | |
| bool IsJPXColorSpaceOrUnspecifiedOrUnknown(COLOR_SPACE actual, | |
| COLOR_SPACE expected) { | |
| return actual == expected || actual == OPJ_CLRSPC_UNSPECIFIED || | |
| actual == OPJ_CLRSPC_UNKNOWN; | |
| } | |
| // Decides which JpxDecodeAction to use based on the colorspace information from | |
| // the PDF and the JPX image. Called only when the PDF's image object contains a | |
| // "/ColorSpace" entry. | |
| std::optional<JpxDecodeAction> GetJpxDecodeActionFromColorSpaces( | |
| const CJPX_Decoder::JpxImageInfo& jpx_info, | |
| const CPDF_ColorSpace* pdf_colorspace) { | |
| if (pdf_colorspace == | |
| CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceGray)) { | |
| if (!IsJPXColorSpaceOrUnspecifiedOrUnknown(/*actual=*/jpx_info.colorspace, | |
| /*expected=*/OPJ_CLRSPC_GRAY)) { | |
| return std::nullopt; | |
| } | |
| return JpxDecodeAction::kUseGray; | |
| } | |
| if (pdf_colorspace == | |
| CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceRGB)) { | |
| if (!IsJPXColorSpaceOrUnspecifiedOrUnknown(/*actual=*/jpx_info.colorspace, | |
| /*expected=*/OPJ_CLRSPC_SRGB)) { | |
| return std::nullopt; | |
| } | |
| // The channel count of a JPX image can be different from the PDF color | |
| // space's component count. | |
| if (jpx_info.channels > 3) { | |
| return JpxDecodeAction::kConvertArgbToRgb; | |
| } | |
| return JpxDecodeAction::kUseRgb; | |
| } | |
| if (pdf_colorspace == | |
| CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceCMYK)) { | |
| if (!IsJPXColorSpaceOrUnspecifiedOrUnknown(/*actual=*/jpx_info.colorspace, | |
| /*expected=*/OPJ_CLRSPC_CMYK)) { | |
| return std::nullopt; | |
| } | |
| return JpxDecodeAction::kUseCmyk; | |
| } | |
| // Many PDFs generated by iOS meets this condition. Handle the discrepancy. | |
| // See https://crbug.com/345431077 for example. | |
| if (pdf_colorspace->ComponentCount() == 3 && jpx_info.channels == 4 && | |
| jpx_info.colorspace == OPJ_CLRSPC_SRGB) { | |
| return JpxDecodeAction::kConvertArgbToRgb; | |
| } | |
| if (pdf_colorspace->GetFamily() == CPDF_ColorSpace::Family::kIndexed && | |
| pdf_colorspace->ComponentCount() == 1) { | |
| return JpxDecodeAction::kUseIndexed; | |
| } | |
| return JpxDecodeAction::kDoNothing; | |
| } | |
| JpxDecodeAction GetJpxDecodeActionFromImageColorSpace( | |
| const CJPX_Decoder::JpxImageInfo& jpx_info) { | |
| switch (jpx_info.colorspace) { | |
| case OPJ_CLRSPC_UNKNOWN: | |
| case OPJ_CLRSPC_UNSPECIFIED: | |
| return jpx_info.channels == 3 ? JpxDecodeAction::kUseRgb | |
| : JpxDecodeAction::kDoNothing; | |
| case OPJ_CLRSPC_SYCC: | |
| case OPJ_CLRSPC_EYCC: | |
| return JpxDecodeAction::kDoNothing; | |
| case OPJ_CLRSPC_SRGB: | |
| return jpx_info.channels > 3 ? JpxDecodeAction::kConvertArgbToRgb | |
| : JpxDecodeAction::kUseRgb; | |
| case OPJ_CLRSPC_GRAY: | |
| return JpxDecodeAction::kUseGray; | |
| case OPJ_CLRSPC_CMYK: | |
| return JpxDecodeAction::kUseCmyk; | |
| } | |
| NOTREACHED(); | |
| } | |
| int GetComponentCountFromJpxImageInfo( | |
| const CJPX_Decoder::JpxImageInfo& jpx_info) { | |
| switch (jpx_info.colorspace) { | |
| case OPJ_CLRSPC_UNKNOWN: | |
| case OPJ_CLRSPC_UNSPECIFIED: | |
| return jpx_info.channels; | |
| case OPJ_CLRSPC_GRAY: | |
| return 1; | |
| case OPJ_CLRSPC_SRGB: | |
| case OPJ_CLRSPC_SYCC: | |
| case OPJ_CLRSPC_EYCC: | |
| return 3; | |
| case OPJ_CLRSPC_CMYK: | |
| return 4; | |
| } | |
| NOTREACHED(); | |
| } | |
| std::optional<FXDIB_Format> GetFormatFromJpxDecodeActionAndImageInfo( | |
| JpxDecodeAction action, | |
| uint32_t channels) { | |
| if (action == JpxDecodeAction::kUseGray || | |
| action == JpxDecodeAction::kUseIndexed) { | |
| return FXDIB_Format::k8bppRgb; | |
| } | |
| if (action == JpxDecodeAction::kUseRgb && channels == 3) { | |
| return FXDIB_Format::kBgr; | |
| } | |
| if (action == JpxDecodeAction::kUseRgb && channels == 4) { | |
| return FXDIB_Format::kBgrx; | |
| } | |
| if (action == JpxDecodeAction::kConvertArgbToRgb) { | |
| CHECK_GE(channels, 4); | |
| return FXDIB_Format::kBgrx; | |
| } | |
| return std::nullopt; | |
| } | |
| } // namespace | |
| // static | |
| std::optional<JpxDecodeConversion> JpxDecodeConversion::Create( | |
| const CJPX_Decoder::JpxImageInfo& jpx_info, | |
| const CPDF_ColorSpace* pdf_colorspace) { | |
| // When the PDF does not provide a color space, check the image color space. | |
| std::optional<JpxDecodeAction> maybe_action = | |
| pdf_colorspace | |
| ? GetJpxDecodeActionFromColorSpaces(jpx_info, pdf_colorspace) | |
| : GetJpxDecodeActionFromImageColorSpace(jpx_info); | |
| if (!maybe_action.has_value()) { | |
| return std::nullopt; | |
| } | |
| JpxDecodeConversion conversion; | |
| switch (maybe_action.value()) { | |
| case JpxDecodeAction::kDoNothing: | |
| break; | |
| case JpxDecodeAction::kUseGray: | |
| conversion.override_colorspace_ = | |
| CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceGray); | |
| break; | |
| case JpxDecodeAction::kUseIndexed: | |
| break; | |
| case JpxDecodeAction::kUseRgb: | |
| DCHECK_GE(jpx_info.channels, 3); | |
| conversion.swap_rgb_ = true; | |
| conversion.override_colorspace_ = nullptr; | |
| break; | |
| case JpxDecodeAction::kUseCmyk: | |
| conversion.override_colorspace_ = | |
| CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceCMYK); | |
| break; | |
| case JpxDecodeAction::kConvertArgbToRgb: | |
| conversion.swap_rgb_ = true; | |
| conversion.convert_argb_to_rgb_ = true; | |
| conversion.override_colorspace_ = nullptr; | |
| break; | |
| } | |
| // If there exists a PDF colorspace, then CPDF_DIB already has the | |
| // components count. | |
| if (!pdf_colorspace) { | |
| conversion.jpx_components_count_ = | |
| GetComponentCountFromJpxImageInfo(jpx_info); | |
| } | |
| std::optional<FXDIB_Format> maybe_format = | |
| GetFormatFromJpxDecodeActionAndImageInfo(maybe_action.value(), | |
| jpx_info.channels); | |
| if (maybe_format.has_value()) { | |
| conversion.format_ = maybe_format.value(); | |
| conversion.width_ = jpx_info.width; | |
| } else { | |
| conversion.format_ = FXDIB_Format::kBgr; | |
| conversion.width_ = (jpx_info.width * jpx_info.channels + 2) / 3; | |
| } | |
| return conversion; | |
| } | |
| JpxDecodeConversion::JpxDecodeConversion() = default; | |
| JpxDecodeConversion::JpxDecodeConversion(JpxDecodeConversion&&) noexcept = | |
| default; | |
| JpxDecodeConversion& JpxDecodeConversion::operator=( | |
| JpxDecodeConversion&&) noexcept = default; | |
| JpxDecodeConversion::~JpxDecodeConversion() = default; |