| // 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/fpdfapi/parser/cpdf_document.h" | |
| #include <memory> | |
| #include <utility> | |
| #include "constants/catalog.h" | |
| #include "core/fpdfapi/page/test_with_page_module.h" | |
| #include "core/fpdfapi/parser/cpdf_array.h" | |
| #include "core/fpdfapi/parser/cpdf_boolean.h" | |
| #include "core/fpdfapi/parser/cpdf_dictionary.h" | |
| #include "core/fpdfapi/parser/cpdf_linearized_header.h" | |
| #include "core/fpdfapi/parser/cpdf_name.h" | |
| #include "core/fpdfapi/parser/cpdf_number.h" | |
| #include "core/fpdfapi/parser/cpdf_parser.h" | |
| #include "core/fpdfapi/parser/cpdf_reference.h" | |
| #include "core/fpdfapi/parser/cpdf_string.h" | |
| #include "core/fpdfapi/parser/cpdf_test_document.h" | |
| #include "core/fxcrt/check.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| namespace { | |
| const int kNumTestPages = 7; | |
| RetainPtr<CPDF_Dictionary> CreatePageTreeNode(RetainPtr<CPDF_Array> kids, | |
| CPDF_Document* doc, | |
| int count) { | |
| uint32_t new_objnum = doc->AddIndirectObject(kids); | |
| auto pageNode = doc->NewIndirect<CPDF_Dictionary>(); | |
| pageNode->SetNewFor<CPDF_Name>("Type", "Pages"); | |
| pageNode->SetNewFor<CPDF_Reference>("Kids", doc, new_objnum); | |
| pageNode->SetNewFor<CPDF_Number>("Count", count); | |
| for (size_t i = 0; i < kids->size(); i++) { | |
| kids->GetMutableDictAt(i)->SetNewFor<CPDF_Reference>("Parent", doc, | |
| pageNode->GetObjNum()); | |
| } | |
| return pageNode; | |
| } | |
| RetainPtr<CPDF_Dictionary> CreateNumberedPage(size_t number) { | |
| auto page = pdfium::MakeRetain<CPDF_Dictionary>(); | |
| page->SetNewFor<CPDF_Name>("Type", "Page"); | |
| page->SetNewFor<CPDF_Number>("PageNumbering", static_cast<int>(number)); | |
| return page; | |
| } | |
| class CPDF_TestDocumentForPages final : public CPDF_TestDocument { | |
| public: | |
| CPDF_TestDocumentForPages() { | |
| // Set up test | |
| auto zeroToTwo = pdfium::MakeRetain<CPDF_Array>(); | |
| zeroToTwo->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(0))); | |
| zeroToTwo->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(1))); | |
| zeroToTwo->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(2))); | |
| RetainPtr<CPDF_Dictionary> branch1 = | |
| CreatePageTreeNode(std::move(zeroToTwo), this, 3); | |
| auto zeroToThree = pdfium::MakeRetain<CPDF_Array>(); | |
| zeroToThree->AppendNew<CPDF_Reference>(this, branch1->GetObjNum()); | |
| zeroToThree->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(3))); | |
| RetainPtr<CPDF_Dictionary> branch2 = | |
| CreatePageTreeNode(std::move(zeroToThree), this, 4); | |
| auto fourFive = pdfium::MakeRetain<CPDF_Array>(); | |
| fourFive->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(4))); | |
| fourFive->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(5))); | |
| RetainPtr<CPDF_Dictionary> branch3 = | |
| CreatePageTreeNode(std::move(fourFive), this, 2); | |
| auto justSix = pdfium::MakeRetain<CPDF_Array>(); | |
| justSix->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(6))); | |
| RetainPtr<CPDF_Dictionary> branch4 = | |
| CreatePageTreeNode(std::move(justSix), this, 1); | |
| auto allPages = pdfium::MakeRetain<CPDF_Array>(); | |
| allPages->AppendNew<CPDF_Reference>(this, branch2->GetObjNum()); | |
| allPages->AppendNew<CPDF_Reference>(this, branch3->GetObjNum()); | |
| allPages->AppendNew<CPDF_Reference>(this, branch4->GetObjNum()); | |
| RetainPtr<CPDF_Dictionary> pagesDict = | |
| CreatePageTreeNode(std::move(allPages), this, kNumTestPages); | |
| SetRootForTesting(NewIndirect<CPDF_Dictionary>()); | |
| GetMutableRoot()->SetNewFor<CPDF_Reference>(pdfium::catalog::kPages, this, | |
| pagesDict->GetObjNum()); | |
| ResizePageListForTesting(kNumTestPages); | |
| } | |
| void SetTreeSize(int size) { | |
| GetMutableRoot()->SetNewFor<CPDF_Number>("Count", size); | |
| ResizePageListForTesting(size); | |
| } | |
| }; | |
| class CPDF_TestDocumentWithPageWithoutPageNum final : public CPDF_TestDocument { | |
| public: | |
| CPDF_TestDocumentWithPageWithoutPageNum() { | |
| // Set up test | |
| auto allPages = pdfium::MakeRetain<CPDF_Array>(); | |
| allPages->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(0))); | |
| allPages->AppendNew<CPDF_Reference>( | |
| this, AddIndirectObject(CreateNumberedPage(1))); | |
| // Page without pageNum. | |
| inlined_page_ = CreateNumberedPage(2); | |
| allPages->Append(inlined_page_); | |
| RetainPtr<CPDF_Dictionary> pagesDict = | |
| CreatePageTreeNode(std::move(allPages), this, 3); | |
| SetRootForTesting(NewIndirect<CPDF_Dictionary>()); | |
| GetMutableRoot()->SetNewFor<CPDF_Reference>(pdfium::catalog::kPages, this, | |
| pagesDict->GetObjNum()); | |
| ResizePageListForTesting(3); | |
| } | |
| const CPDF_Object* inlined_page() const { return inlined_page_.Get(); } | |
| private: | |
| RetainPtr<CPDF_Object> inlined_page_; | |
| }; | |
| class TestLinearized final : public CPDF_LinearizedHeader { | |
| public: | |
| explicit TestLinearized(CPDF_Dictionary* dict) | |
| : CPDF_LinearizedHeader(dict, 0) {} | |
| }; | |
| class CPDF_TestDocPagesWithoutKids final : public CPDF_TestDocument { | |
| public: | |
| CPDF_TestDocPagesWithoutKids() { | |
| auto pagesDict = NewIndirect<CPDF_Dictionary>(); | |
| pagesDict->SetNewFor<CPDF_Name>("Type", "Pages"); | |
| pagesDict->SetNewFor<CPDF_Number>("Count", 3); | |
| ResizePageListForTesting(10); | |
| SetRootForTesting(NewIndirect<CPDF_Dictionary>()); | |
| GetMutableRoot()->SetNewFor<CPDF_Reference>(pdfium::catalog::kPages, this, | |
| pagesDict->GetObjNum()); | |
| } | |
| }; | |
| class CPDF_TestDocumentAllowSetParser final : public CPDF_TestDocument { | |
| public: | |
| CPDF_TestDocumentAllowSetParser() = default; | |
| using CPDF_Document::SetParser; | |
| }; | |
| } // namespace | |
| using DocumentTest = TestWithPageModule; | |
| TEST_F(DocumentTest, GetPages) { | |
| std::unique_ptr<CPDF_TestDocumentForPages> document = | |
| std::make_unique<CPDF_TestDocumentForPages>(); | |
| for (int i = 0; i < kNumTestPages; i++) { | |
| RetainPtr<const CPDF_Dictionary> page = document->GetPageDictionary(i); | |
| ASSERT_TRUE(page); | |
| ASSERT_TRUE(page->KeyExist("PageNumbering")); | |
| EXPECT_EQ(i, page->GetIntegerFor("PageNumbering")); | |
| } | |
| RetainPtr<const CPDF_Dictionary> page = | |
| document->GetPageDictionary(kNumTestPages); | |
| EXPECT_FALSE(page); | |
| } | |
| TEST_F(DocumentTest, GetPageWithoutObjNumTwice) { | |
| auto document = std::make_unique<CPDF_TestDocumentWithPageWithoutPageNum>(); | |
| RetainPtr<const CPDF_Dictionary> page = document->GetPageDictionary(2); | |
| ASSERT_TRUE(page); | |
| ASSERT_EQ(document->inlined_page(), page); | |
| RetainPtr<const CPDF_Dictionary> second_call_page = | |
| document->GetPageDictionary(2); | |
| EXPECT_TRUE(second_call_page); | |
| EXPECT_EQ(page, second_call_page); | |
| } | |
| TEST_F(DocumentTest, GetPagesReverseOrder) { | |
| std::unique_ptr<CPDF_TestDocumentForPages> document = | |
| std::make_unique<CPDF_TestDocumentForPages>(); | |
| for (int i = 6; i >= 0; i--) { | |
| RetainPtr<const CPDF_Dictionary> page = document->GetPageDictionary(i); | |
| ASSERT_TRUE(page); | |
| ASSERT_TRUE(page->KeyExist("PageNumbering")); | |
| EXPECT_EQ(i, page->GetIntegerFor("PageNumbering")); | |
| } | |
| RetainPtr<const CPDF_Dictionary> page = | |
| document->GetPageDictionary(kNumTestPages); | |
| EXPECT_FALSE(page); | |
| } | |
| TEST_F(DocumentTest, GetPagesInDisorder) { | |
| std::unique_ptr<CPDF_TestDocumentForPages> document = | |
| std::make_unique<CPDF_TestDocumentForPages>(); | |
| RetainPtr<const CPDF_Dictionary> page = document->GetPageDictionary(1); | |
| ASSERT_TRUE(page); | |
| ASSERT_TRUE(page->KeyExist("PageNumbering")); | |
| EXPECT_EQ(1, page->GetIntegerFor("PageNumbering")); | |
| page = document->GetPageDictionary(3); | |
| ASSERT_TRUE(page); | |
| ASSERT_TRUE(page->KeyExist("PageNumbering")); | |
| EXPECT_EQ(3, page->GetIntegerFor("PageNumbering")); | |
| page = document->GetPageDictionary(kNumTestPages); | |
| EXPECT_FALSE(page); | |
| page = document->GetPageDictionary(6); | |
| ASSERT_TRUE(page); | |
| ASSERT_TRUE(page->KeyExist("PageNumbering")); | |
| EXPECT_EQ(6, page->GetIntegerFor("PageNumbering")); | |
| } | |
| TEST_F(DocumentTest, IsValidPageObject) { | |
| CPDF_TestDocumentForPages document; | |
| auto dict_type_name_page = pdfium::MakeRetain<CPDF_Dictionary>(); | |
| dict_type_name_page->SetNewFor<CPDF_Name>("Type", "Page"); | |
| document.AddIndirectObject(dict_type_name_page); | |
| EXPECT_TRUE(CPDF_Document::IsValidPageObject(dict_type_name_page.Get())); | |
| auto dict_type_string_page = pdfium::MakeRetain<CPDF_Dictionary>(); | |
| dict_type_string_page->SetNewFor<CPDF_String>("Type", "Page"); | |
| document.AddIndirectObject(dict_type_string_page); | |
| EXPECT_FALSE(CPDF_Document::IsValidPageObject(dict_type_string_page.Get())); | |
| auto dict_type_name_font = pdfium::MakeRetain<CPDF_Dictionary>(); | |
| dict_type_name_font->SetNewFor<CPDF_Name>("Type", "Font"); | |
| document.AddIndirectObject(dict_type_name_font); | |
| EXPECT_FALSE(CPDF_Document::IsValidPageObject(dict_type_name_font.Get())); | |
| auto obj_no_type = document.NewIndirect<CPDF_Dictionary>(); | |
| EXPECT_FALSE(CPDF_Document::IsValidPageObject(obj_no_type.Get())); | |
| } | |
| TEST_F(DocumentTest, UseCachedPageObjNumIfHaveNotPagesDict) { | |
| // ObjNum can be added in CPDF_DataAvail::IsPageAvail(), and PagesDict may not | |
| // exist in this case, e.g. when hint table is used to page check in | |
| // CPDF_DataAvail. | |
| static constexpr int kPageCount = 100; | |
| static constexpr int kTestPageNum = 33; | |
| auto linearization_dict = pdfium::MakeRetain<CPDF_Dictionary>(); | |
| CPDF_TestDocumentAllowSetParser document; | |
| { | |
| auto first_page = CreateNumberedPage(0); | |
| ASSERT_TRUE(first_page); | |
| int first_page_obj_num = document.AddIndirectObject(first_page); | |
| ASSERT_NE(kTestPageNum, first_page_obj_num); | |
| linearization_dict->SetNewFor<CPDF_Boolean>("Linearized", true); | |
| linearization_dict->SetNewFor<CPDF_Number>("N", kPageCount); | |
| linearization_dict->SetNewFor<CPDF_Number>("O", first_page_obj_num); | |
| auto parser = std::make_unique<CPDF_Parser>(); | |
| parser->SetLinearizedHeaderForTesting( | |
| std::make_unique<TestLinearized>(linearization_dict.Get())); | |
| document.SetParser(std::move(parser)); | |
| } | |
| document.LoadPages(); | |
| ASSERT_EQ(kPageCount, document.GetPageCount()); | |
| auto page_stub = document.NewIndirect<CPDF_Dictionary>(); | |
| const uint32_t obj_num = page_stub->GetObjNum(); | |
| EXPECT_FALSE(document.IsPageLoaded(kTestPageNum)); | |
| EXPECT_FALSE(document.GetPageDictionary(kTestPageNum)); | |
| document.SetPageObjNum(kTestPageNum, obj_num); | |
| EXPECT_TRUE(document.IsPageLoaded(kTestPageNum)); | |
| EXPECT_EQ(page_stub, document.GetPageDictionary(kTestPageNum)); | |
| } | |
| TEST_F(DocumentTest, CountGreaterThanPageTree) { | |
| std::unique_ptr<CPDF_TestDocumentForPages> document = | |
| std::make_unique<CPDF_TestDocumentForPages>(); | |
| document->SetTreeSize(kNumTestPages + 3); | |
| for (int i = 0; i < kNumTestPages; i++) { | |
| EXPECT_TRUE(document->GetPageDictionary(i)); | |
| } | |
| for (int i = kNumTestPages; i < kNumTestPages + 4; i++) { | |
| EXPECT_FALSE(document->GetPageDictionary(i)); | |
| } | |
| EXPECT_TRUE(document->GetPageDictionary(kNumTestPages - 1)); | |
| } | |
| TEST_F(DocumentTest, PagesWithoutKids) { | |
| // Set up a document with Pages dict without kids, and Count = 3 | |
| auto doc = std::make_unique<CPDF_TestDocPagesWithoutKids>(); | |
| // Since there are no Page nodes in the Page Tree, GetPageDictionary() will | |
| // never return non-null. | |
| for (int i = 0; i < 5; i++) { | |
| EXPECT_FALSE(doc->GetPageDictionary(i)); | |
| } | |
| EXPECT_FALSE(doc->GetPageDictionary(0)); | |
| } |