| // Copyright 2023 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_pageimagecache.h" | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include "core/fpdfapi/page/cpdf_docpagedata.h" | |
| #include "core/fpdfapi/page/cpdf_image.h" | |
| #include "core/fpdfapi/page/cpdf_imageobject.h" | |
| #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_parser.h" | |
| #include "core/fpdfapi/render/cpdf_docrenderdata.h" | |
| #include "core/fxcrt/cfx_fileaccess_stream.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "testing/utils/path_service.h" | |
| namespace pdfium { | |
| using CPDFPageImageCacheTest = TestWithPageModule; | |
| TEST_F(CPDFPageImageCacheTest, RenderBug1924) { | |
| // If you render a page with a JPEG2000 image as a thumbnail (small picture) | |
| // first, the image that gets cached has a low resolution. If you afterwards | |
| // render it full-size, you should get a larger image - the image cache will | |
| // be regenerate. | |
| { | |
| std::string file_path = PathService::GetTestFilePath("jpx_lzw.pdf"); | |
| ASSERT_FALSE(file_path.empty()); | |
| auto document = | |
| std::make_unique<CPDF_Document>(std::make_unique<CPDF_DocRenderData>(), | |
| std::make_unique<CPDF_DocPageData>()); | |
| ASSERT_EQ(document->LoadDoc( | |
| CFX_FileAccessStream::CreateFromFilename(file_path.c_str()), | |
| nullptr), | |
| CPDF_Parser::SUCCESS); | |
| RetainPtr<CPDF_Dictionary> page_dict = | |
| document->GetMutablePageDictionary(0); | |
| ASSERT_TRUE(page_dict); | |
| auto page = | |
| pdfium::MakeRetain<CPDF_Page>(document.get(), std::move(page_dict)); | |
| page->AddPageImageCache(); | |
| page->ParseContent(); | |
| CPDF_PageImageCache* page_image_cache = page->GetPageImageCache(); | |
| ASSERT_TRUE(page_image_cache); | |
| CPDF_PageObject* page_obj = page->GetPageObjectByIndex(0); | |
| ASSERT_TRUE(page_obj); | |
| CPDF_ImageObject* image = page_obj->AsImage(); | |
| ASSERT_TRUE(image); | |
| // Render with small scale. | |
| bool should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kICCBased, false, {50, 50}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap_small = page_image_cache->DetachCurBitmap(); | |
| // And render with large scale. | |
| should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kICCBased, false, {100, 100}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap_large = page_image_cache->DetachCurBitmap(); | |
| ASSERT_GT(bitmap_large->GetWidth(), bitmap_small->GetWidth()); | |
| ASSERT_GT(bitmap_large->GetHeight(), bitmap_small->GetHeight()); | |
| ASSERT_TRUE(page->AsPDFPage()); | |
| page->AsPDFPage()->ClearView(); | |
| } | |
| } | |
| TEST_F(CPDFPageImageCacheTest, RenderReducedDctThenFullSize) { | |
| // A DCTDecode (JPEG) image drawn small is decoded at reduced resolution via | |
| // libjpeg's scale_denom (like the JPEG2000 path above). If it is then drawn | |
| // larger, the cache must re-decode at a higher resolution rather than | |
| // upscaling the cached low-resolution bitmap. The image is 400x400 and a | |
| // multiple of the MCU size, so reduced decoding is allowed. | |
| std::string file_path = PathService::GetTestFilePath("jpeg_reduced_size.pdf"); | |
| ASSERT_FALSE(file_path.empty()); | |
| auto document = | |
| std::make_unique<CPDF_Document>(std::make_unique<CPDF_DocRenderData>(), | |
| std::make_unique<CPDF_DocPageData>()); | |
| ASSERT_EQ( | |
| document->LoadDoc( | |
| CFX_FileAccessStream::CreateFromFilename(file_path.c_str()), nullptr), | |
| CPDF_Parser::SUCCESS); | |
| RetainPtr<CPDF_Dictionary> page_dict = document->GetMutablePageDictionary(0); | |
| ASSERT_TRUE(page_dict); | |
| auto page = | |
| pdfium::MakeRetain<CPDF_Page>(document.get(), std::move(page_dict)); | |
| page->AddPageImageCache(); | |
| page->ParseContent(); | |
| CPDF_PageImageCache* page_image_cache = page->GetPageImageCache(); | |
| ASSERT_TRUE(page_image_cache); | |
| CPDF_PageObject* page_obj = page->GetPageObjectByIndex(0); | |
| ASSERT_TRUE(page_obj); | |
| CPDF_ImageObject* image = page_obj->AsImage(); | |
| ASSERT_TRUE(image); | |
| // Render with small scale: 400/50 -> 1/8, so a 50x50 bitmap. | |
| bool should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kUnknown, false, {50, 50}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap_small = page_image_cache->DetachCurBitmap(); | |
| ASSERT_TRUE(bitmap_small); | |
| EXPECT_EQ(bitmap_small->GetWidth(), 50); | |
| EXPECT_EQ(bitmap_small->GetHeight(), 50); | |
| // Render with large scale: 400/100 -> 1/4, so a 100x100 bitmap. The cached | |
| // 50x50 bitmap is too small, so this forces a higher-resolution re-decode. | |
| should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kUnknown, false, {100, 100}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap_large = page_image_cache->DetachCurBitmap(); | |
| ASSERT_TRUE(bitmap_large); | |
| EXPECT_EQ(bitmap_large->GetWidth(), 100); | |
| EXPECT_EQ(bitmap_large->GetHeight(), 100); | |
| ASSERT_TRUE(page->AsPDFPage()); | |
| page->AsPDFPage()->ClearView(); | |
| } | |
| TEST_F(CPDFPageImageCacheTest, RenderReducedDctWithSMask) { | |
| // The base image (DCTDecode) reduces when drawn small, but its soft mask is | |
| // always loaded at full resolution (StartLoadMaskDIB passes a zero max size). | |
| // The base and mask therefore have different dimensions; this exercises that | |
| // path and confirms both are produced without a dimension-mismatch failure. | |
| std::string file_path = | |
| PathService::GetTestFilePath("jpeg_reduced_size_with_smask.pdf"); | |
| ASSERT_FALSE(file_path.empty()); | |
| auto document = | |
| std::make_unique<CPDF_Document>(std::make_unique<CPDF_DocRenderData>(), | |
| std::make_unique<CPDF_DocPageData>()); | |
| ASSERT_EQ( | |
| document->LoadDoc( | |
| CFX_FileAccessStream::CreateFromFilename(file_path.c_str()), nullptr), | |
| CPDF_Parser::SUCCESS); | |
| RetainPtr<CPDF_Dictionary> page_dict = document->GetMutablePageDictionary(0); | |
| ASSERT_TRUE(page_dict); | |
| auto page = | |
| pdfium::MakeRetain<CPDF_Page>(document.get(), std::move(page_dict)); | |
| page->AddPageImageCache(); | |
| page->ParseContent(); | |
| CPDF_PageImageCache* page_image_cache = page->GetPageImageCache(); | |
| ASSERT_TRUE(page_image_cache); | |
| CPDF_PageObject* page_obj = page->GetPageObjectByIndex(0); | |
| ASSERT_TRUE(page_obj); | |
| CPDF_ImageObject* image = page_obj->AsImage(); | |
| ASSERT_TRUE(image); | |
| // Draw small so the 400x400 base is decoded at reduced resolution. | |
| bool should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kUnknown, /*bLoadMask=*/true, {50, 50}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap = page_image_cache->DetachCurBitmap(); | |
| RetainPtr<CFX_DIBBase> mask = page_image_cache->DetachCurMask(); | |
| ASSERT_TRUE(bitmap); | |
| ASSERT_TRUE(mask); | |
| // The base is reduced; the mask is loaded at full resolution. The compositor | |
| // scales each to the destination independently, so the differing sizes are | |
| // expected and handled. | |
| EXPECT_EQ(bitmap->GetWidth(), 50); | |
| EXPECT_EQ(bitmap->GetHeight(), 50); | |
| EXPECT_EQ(mask->GetWidth(), 400); | |
| EXPECT_EQ(mask->GetHeight(), 400); | |
| ASSERT_TRUE(page->AsPDFPage()); | |
| page->AsPDFPage()->ClearView(); | |
| } | |
| TEST_F(CPDFPageImageCacheTest, NonMcuAlignedDctIsNotReduced) { | |
| // Reduced-size decoding is only safe when the image dimensions are a multiple | |
| // of the MCU size (otherwise padded edge blocks would contaminate the | |
| // result). This 408x408 image is 4:2:0, so its MCU is 16x16 and 408 is not a | |
| // multiple of it (408 % 16 == 8), even though 408 % 8 == 0. Drawing it small | |
| // must therefore still decode it at full resolution rather than reducing it. | |
| std::string file_path = | |
| PathService::GetTestFilePath("jpeg_unaligned_no_reduce.pdf"); | |
| ASSERT_FALSE(file_path.empty()); | |
| auto document = | |
| std::make_unique<CPDF_Document>(std::make_unique<CPDF_DocRenderData>(), | |
| std::make_unique<CPDF_DocPageData>()); | |
| ASSERT_EQ( | |
| document->LoadDoc( | |
| CFX_FileAccessStream::CreateFromFilename(file_path.c_str()), nullptr), | |
| CPDF_Parser::SUCCESS); | |
| RetainPtr<CPDF_Dictionary> page_dict = document->GetMutablePageDictionary(0); | |
| ASSERT_TRUE(page_dict); | |
| auto page = | |
| pdfium::MakeRetain<CPDF_Page>(document.get(), std::move(page_dict)); | |
| page->AddPageImageCache(); | |
| page->ParseContent(); | |
| CPDF_PageImageCache* page_image_cache = page->GetPageImageCache(); | |
| ASSERT_TRUE(page_image_cache); | |
| CPDF_PageObject* page_obj = page->GetPageObjectByIndex(0); | |
| ASSERT_TRUE(page_obj); | |
| CPDF_ImageObject* image = page_obj->AsImage(); | |
| ASSERT_TRUE(image); | |
| // Draw small: an MCU-aligned image would be reduced here, but this one must | |
| // not be, so the decoded bitmap stays at the full 408x408. | |
| bool should_continue = page_image_cache->StartGetCachedBitmap( | |
| image->GetImage(), nullptr, page->GetMutablePageResources(), true, | |
| CPDF_ColorSpace::Family::kUnknown, false, {50, 50}); | |
| while (should_continue) { | |
| should_continue = page_image_cache->Continue(nullptr); | |
| } | |
| RetainPtr<CFX_DIBBase> bitmap = page_image_cache->DetachCurBitmap(); | |
| ASSERT_TRUE(bitmap); | |
| EXPECT_EQ(bitmap->GetWidth(), 408); | |
| EXPECT_EQ(bitmap->GetHeight(), 408); | |
| ASSERT_TRUE(page->AsPDFPage()); | |
| page->AsPDFPage()->ClearView(); | |
| } | |
| } // namespace pdfium |