| // 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; |