| // Copyright 2016 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/fxge/skia/fx_skia_device.h" |
| |
| #include <memory> |
| #include <set> |
| #include <utility> |
| |
| #include "core/fpdfapi/page/cpdf_page.h" |
| #include "core/fpdfapi/render/cpdf_pagerendercontext.h" |
| #include "core/fxcrt/fx_codepage.h" |
| #include "core/fxge/cfx_defaultrenderdevice.h" |
| #include "core/fxge/cfx_fillrenderoptions.h" |
| #include "core/fxge/cfx_font.h" |
| #include "core/fxge/cfx_graphstatedata.h" |
| #include "core/fxge/cfx_path.h" |
| #include "core/fxge/cfx_renderdevice.h" |
| #include "core/fxge/cfx_textrenderoptions.h" |
| #include "core/fxge/dib/cfx_dibitmap.h" |
| #include "core/fxge/text_char_pos.h" |
| #include "fpdfsdk/cpdfsdk_helpers.h" |
| #include "fpdfsdk/cpdfsdk_renderpage.h" |
| #include "public/cpp/fpdf_scopers.h" |
| #include "public/fpdfview.h" |
| #include "testing/embedder_test.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| #include "third_party/skia/include/core/SkSize.h" |
| #include "third_party/skia/include/utils/SkNoDrawCanvas.h" |
| |
| namespace { |
| |
| using ::testing::NiceMock; |
| using ::testing::SizeIs; |
| using ::testing::WithArg; |
| |
| struct State { |
| enum class Change { kNo, kYes }; |
| enum class Save { kNo, kYes }; |
| enum class Clip { kNo, kSame, kDifferentPath, kDifferentMatrix }; |
| enum class Graphic { kNone, kPath, kText }; |
| |
| Change m_change; |
| Save m_save; |
| Clip m_clip; |
| Graphic m_graphic; |
| uint32_t m_pixel; |
| }; |
| |
| void EmptyTest(CFX_SkiaDeviceDriver* driver, const State&) { |
| driver->SaveState(); |
| driver->RestoreState(true); |
| driver->RestoreState(false); |
| } |
| |
| void CommonTest(CFX_SkiaDeviceDriver* driver, const State& state) { |
| TextCharPos charPos[1]; |
| charPos[0].m_Origin = CFX_PointF(0, 1); |
| charPos[0].m_GlyphIndex = 0; |
| charPos[0].m_FontCharWidth = 4; |
| |
| CFX_Font font; |
| font.LoadSubst("Courier", /*bTrueType=*/true, /*flags=*/0, |
| /*weight=*/400, /*italic_angle=*/0, FX_CodePage::kShiftJIS, |
| /*bVertical=*/false); |
| float fontSize = 20; |
| CFX_Path clipPath; |
| CFX_Path clipPath2; |
| clipPath.AppendRect(0, 0, 3, 1); |
| clipPath2.AppendRect(0, 0, 2, 1); |
| CFX_Matrix clipMatrix; |
| CFX_Matrix clipMatrix2(1, 0, 0, 1, 0, 1); |
| driver->SaveState(); |
| CFX_Path path1; |
| path1.AppendRect(0, 0, 1, 2); |
| |
| CFX_Matrix matrix; |
| CFX_Matrix matrix2; |
| matrix2.Translate(1, 0); |
| CFX_GraphStateData graphState; |
| // Turn off anti-aliasing so that pixels with transitional colors can be |
| // avoided. |
| static constexpr CFX_TextRenderOptions kTextOptions( |
| CFX_TextRenderOptions::kAliasing); |
| if (state.m_save == State::Save::kYes) |
| driver->SaveState(); |
| if (state.m_clip != State::Clip::kNo) |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| if (state.m_graphic == State::Graphic::kPath) { |
| driver->DrawPath(path1, &matrix, &graphState, 0xFF112233, 0, |
| CFX_FillRenderOptions::WindingOptions(), |
| BlendMode::kNormal); |
| } else if (state.m_graphic == State::Graphic::kText) { |
| driver->DrawDeviceText(charPos, &font, matrix, fontSize, 0xFF445566, |
| kTextOptions); |
| } |
| if (state.m_save == State::Save::kYes) |
| driver->RestoreState(true); |
| CFX_Path path2; |
| path2.AppendRect(0, 0, 2, 2); |
| if (state.m_change == State::Change::kYes) { |
| if (state.m_graphic == State::Graphic::kPath) |
| graphState.m_LineCap = CFX_GraphStateData::LineCap::kRound; |
| else if (state.m_graphic == State::Graphic::kText) |
| fontSize = 2; |
| } |
| if (state.m_clip == State::Clip::kSame) |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| else if (state.m_clip == State::Clip::kDifferentPath) |
| driver->SetClip_PathFill(clipPath2, &clipMatrix, CFX_FillRenderOptions()); |
| else if (state.m_clip == State::Clip::kDifferentMatrix) |
| driver->SetClip_PathFill(clipPath, &clipMatrix2, CFX_FillRenderOptions()); |
| if (state.m_graphic == State::Graphic::kPath) { |
| driver->DrawPath(path2, &matrix2, &graphState, 0xFF112233, 0, |
| CFX_FillRenderOptions::WindingOptions(), |
| BlendMode::kNormal); |
| } else if (state.m_graphic == State::Graphic::kText) { |
| driver->DrawDeviceText(charPos, &font, matrix2, fontSize, 0xFF445566, |
| kTextOptions); |
| } |
| if (state.m_save == State::Save::kYes) |
| driver->RestoreState(false); |
| driver->RestoreState(false); |
| } |
| |
| void OutOfSequenceClipTest(CFX_SkiaDeviceDriver* driver, const State&) { |
| CFX_Path clipPath; |
| clipPath.AppendRect(1, 0, 3, 1); |
| CFX_Matrix clipMatrix; |
| driver->SaveState(); |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| driver->RestoreState(true); |
| driver->SaveState(); |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| driver->RestoreState(false); |
| driver->RestoreState(false); |
| |
| driver->SaveState(); |
| driver->SaveState(); |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| driver->RestoreState(true); |
| driver->SetClip_PathFill(clipPath, &clipMatrix, CFX_FillRenderOptions()); |
| driver->RestoreState(false); |
| driver->RestoreState(false); |
| } |
| |
| void Harness(void (*Test)(CFX_SkiaDeviceDriver*, const State&), |
| const State& state) { |
| constexpr int kWidth = 4; |
| constexpr int kHeight = 1; |
| ScopedFPDFBitmap bitmap(FPDFBitmap_Create(kWidth, kHeight, 1)); |
| ASSERT_TRUE(bitmap); |
| FPDFBitmap_FillRect(bitmap.get(), 0, 0, kWidth, kHeight, 0x00000000); |
| RetainPtr<CFX_DIBitmap> pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap.get())); |
| auto driver = CFX_SkiaDeviceDriver::Create(pBitmap, false, nullptr, false); |
| ASSERT_TRUE(driver); |
| (*Test)(driver.get(), state); |
| uint32_t pixel = pBitmap->GetPixel(0, 0); |
| EXPECT_EQ(state.m_pixel, pixel); |
| } |
| |
| void RenderPageToSkCanvas(FPDF_PAGE page, |
| int start_x, |
| int start_y, |
| int size_x, |
| int size_y, |
| SkCanvas* canvas) { |
| CPDF_Page* cpdf_page = CPDFPageFromFPDFPage(page); |
| |
| auto context = std::make_unique<CPDF_PageRenderContext>(); |
| CPDF_PageRenderContext* unowned_context = context.get(); |
| |
| CPDF_Page::RenderContextClearer clearer(cpdf_page); |
| cpdf_page->SetRenderContext(std::move(context)); |
| |
| auto default_device = std::make_unique<CFX_DefaultRenderDevice>(); |
| default_device->AttachCanvas(canvas); |
| unowned_context->m_pDevice = std::move(default_device); |
| |
| CPDFSDK_RenderPageWithContext(unowned_context, cpdf_page, start_x, start_y, |
| size_x, size_y, /*rotate=*/0, /*flags=*/0, |
| /*color_scheme=*/nullptr, |
| /*need_to_restore=*/true, /*pause=*/nullptr); |
| } |
| |
| class MockCanvas : public SkNoDrawCanvas { |
| public: |
| MockCanvas(int width, int height) : SkNoDrawCanvas(width, height) {} |
| |
| MOCK_METHOD(void, |
| onDrawImageRect2, |
| (const SkImage*, |
| const SkRect&, |
| const SkRect&, |
| const SkSamplingOptions&, |
| const SkPaint*, |
| SrcRectConstraint), |
| (override)); |
| }; |
| |
| using FxgeSkiaEmbedderTest = EmbedderTest; |
| |
| } // namespace |
| |
| TEST(fxge, SkiaStateEmpty) { |
| if (!CFX_DefaultRenderDevice::UseSkiaRenderer()) { |
| return; |
| } |
| Harness(&EmptyTest, {}); |
| } |
| |
| TEST(fxge, SkiaStatePath) { |
| if (!CFX_DefaultRenderDevice::UseSkiaRenderer()) { |
| return; |
| } |
| Harness(&CommonTest, {State::Change::kNo, State::Save::kYes, |
| State::Clip::kSame, State::Graphic::kPath, 0xFF112233}); |
| Harness(&CommonTest, |
| {State::Change::kNo, State::Save::kYes, State::Clip::kDifferentPath, |
| State::Graphic::kPath, 0xFF112233}); |
| Harness(&CommonTest, {State::Change::kNo, State::Save::kYes, State::Clip::kNo, |
| State::Graphic::kPath, 0xFF112233}); |
| Harness(&CommonTest, {State::Change::kYes, State::Save::kNo, State::Clip::kNo, |
| State::Graphic::kPath, 0xFF112233}); |
| Harness(&CommonTest, {State::Change::kNo, State::Save::kNo, State::Clip::kNo, |
| State::Graphic::kPath, 0xFF112233}); |
| } |
| |
| TEST(fxge, SkiaStateText) { |
| if (!CFX_DefaultRenderDevice::UseSkiaRenderer()) { |
| return; |
| } |
| |
| Harness(&CommonTest, |
| {State::Change::kNo, State::Save::kYes, State::Clip::kDifferentMatrix, |
| State::Graphic::kText, 0xFF445566}); |
| Harness(&CommonTest, {State::Change::kNo, State::Save::kYes, |
| State::Clip::kSame, State::Graphic::kText, 0xFF445566}); |
| } |
| |
| TEST(fxge, SkiaStateOOSClip) { |
| if (!CFX_DefaultRenderDevice::UseSkiaRenderer()) { |
| return; |
| } |
| Harness(&OutOfSequenceClipTest, {}); |
| } |
| |
| TEST_F(FxgeSkiaEmbedderTest, RenderBigImageTwice) { |
| static constexpr int kImageWidth = 5100; |
| static constexpr int kImageHeight = 6600; |
| |
| // Page size that renders 20 image pixels per output pixel. This value evenly |
| // divides both the image width and half the image height. |
| static constexpr int kPageToImageFactor = 20; |
| static constexpr int kPageWidth = kImageWidth / kPageToImageFactor; |
| static constexpr int kPageHeight = kImageHeight / kPageToImageFactor; |
| |
| if (!CFX_DefaultRenderDevice::UseSkiaRenderer()) { |
| GTEST_SKIP() << "Skia is not the default renderer"; |
| } |
| |
| ASSERT_TRUE(OpenDocument("bug_2034.pdf")); |
| FPDF_PAGE page = LoadPage(0); |
| ASSERT_TRUE(page); |
| |
| std::set<int> image_ids; |
| NiceMock<MockCanvas> canvas(kPageWidth, kPageHeight / 2); |
| EXPECT_CALL(canvas, onDrawImageRect2) |
| .WillRepeatedly(WithArg<0>([&image_ids](const SkImage* image) { |
| ASSERT_TRUE(image); |
| image_ids.insert(image->uniqueID()); |
| |
| // TODO(crbug.com/pdfium/2026): Image dimensions should be clipped to |
| // 5100x3320. The extra `kPageToImageFactor` accounts for anti-aliasing. |
| EXPECT_EQ(SkISize::Make(kImageWidth, kImageHeight), image->dimensions()) |
| << "Actual image dimensions: " << image->width() << "x" |
| << image->height(); |
| })); |
| |
| // Render top half. |
| RenderPageToSkCanvas(page, /*start_x=*/0, /*start_y=*/0, |
| /*size_x=*/kPageWidth, /*size_y=*/kPageHeight, &canvas); |
| |
| // Render bottom half. |
| RenderPageToSkCanvas(page, /*start_x=*/0, /*start_y=*/-kPageHeight / 2, |
| /*size_x=*/kPageWidth, /*size_y=*/kPageHeight, &canvas); |
| |
| EXPECT_THAT(image_ids, SizeIs(1)); |
| |
| UnloadPage(page); |
| } |