| // Copyright 2024 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/fpdfdoc/cpdf_pagelabel.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| |
| #include "core/fpdfapi/page/test_with_page_module.h" |
| #include "core/fpdfapi/parser/cpdf_array.h" |
| #include "core/fpdfapi/parser/cpdf_dictionary.h" |
| #include "core/fpdfapi/parser/cpdf_name.h" |
| #include "core/fpdfapi/parser/cpdf_number.h" |
| #include "core/fpdfapi/parser/cpdf_string.h" |
| #include "core/fpdfapi/parser/cpdf_test_document.h" |
| #include "core/fxcrt/retain_ptr.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::Eq; |
| using testing::Optional; |
| |
| namespace { |
| |
| struct NumValue { |
| const char* style; |
| const char* prefix; |
| std::optional<int> starting_number; |
| }; |
| |
| void AddNumKeyValue(CPDF_Array* nums, int key, const NumValue& value) { |
| nums->AppendNew<CPDF_Number>(key); |
| auto page_label_dict = nums->AppendNew<CPDF_Dictionary>(); |
| page_label_dict->SetNewFor<CPDF_Name>("Type", "PageLabel"); |
| if (value.style) { |
| page_label_dict->SetNewFor<CPDF_Name>("S", value.style); |
| } |
| if (value.prefix) { |
| page_label_dict->SetNewFor<CPDF_String>("P", value.prefix); |
| } |
| if (value.starting_number.has_value()) { |
| page_label_dict->SetNewFor<CPDF_Number>("St", |
| value.starting_number.value()); |
| } |
| } |
| |
| void AddLimitsArray(CPDF_Dictionary* node, int least, int greatest) { |
| auto limits = node->SetNewFor<CPDF_Array>("Limits"); |
| limits->AppendNew<CPDF_Number>(least); |
| limits->AppendNew<CPDF_Number>(greatest); |
| } |
| |
| // Set up a number tree with 3 levels and 5 nodes per diagram below. The number |
| // tree is intended for use as the /PageLabels entry in a Catalog. The keys in |
| // the leaf nodes are page indices, and the values are page label dictionaries. |
| // See ISO 32000-1:2008 spec, table 159. |
| // |
| // The values are in the format: |S,P,St| |
| // |
| // S: Style (name) |
| // P: Prefix (string) |
| // St: Starting number (integer) Must be >= 1 if specified. |
| // |
| // All 3 fields are optional. |
| // |
| // [page_labels_root] |
| // | |
| // | |
| // | |
| // [kid1] |
| // | |
| // +------------+ |
| // | | |
| // [grand_kid2] [grand_kid3] |
| // | {8000: |,"x",|} |
| // | |
| // +--------------------+ |
| // | | |
| // [great_grand_kid4] [great_grand_kid5] |
| // {0: |"R",,|} {3000: |"r",,|} |
| // {100: |"A","abc",5|} {5000: |"a",,|} |
| // {900: |"D",,999|} |
| // |
| void FillPageLabelsTreeDict(CPDF_Dictionary* page_labels_root) { |
| constexpr char kKids[] = "Kids"; |
| constexpr char kNums[] = "Nums"; |
| |
| auto page_labels_root_kids = page_labels_root->SetNewFor<CPDF_Array>(kKids); |
| auto kid1 = page_labels_root_kids->AppendNew<CPDF_Dictionary>(); |
| |
| AddLimitsArray(kid1.Get(), 0, 8000); |
| auto kids1_kids = kid1->SetNewFor<CPDF_Array>(kKids); |
| auto grand_kid2 = kids1_kids->AppendNew<CPDF_Dictionary>(); |
| auto grand_kid3 = kids1_kids->AppendNew<CPDF_Dictionary>(); |
| |
| AddLimitsArray(grand_kid2.Get(), 0, 5000); |
| auto grand_kid2_kids = grand_kid2->SetNewFor<CPDF_Array>(kKids); |
| auto great_grand_kid4 = grand_kid2_kids->AppendNew<CPDF_Dictionary>(); |
| auto great_grand_kid5 = grand_kid2_kids->AppendNew<CPDF_Dictionary>(); |
| |
| AddLimitsArray(grand_kid3.Get(), 8000, 8000); |
| auto nums = grand_kid3->SetNewFor<CPDF_Array>(kNums); |
| AddNumKeyValue( |
| nums.Get(), 8000, |
| {.style = nullptr, .prefix = "x", .starting_number = std::nullopt}); |
| |
| AddLimitsArray(great_grand_kid4.Get(), 0, 900); |
| nums = great_grand_kid4->SetNewFor<CPDF_Array>(kNums); |
| AddNumKeyValue( |
| nums.Get(), 0, |
| {.style = "R", .prefix = nullptr, .starting_number = std::nullopt}); |
| AddNumKeyValue(nums.Get(), 100, |
| {.style = "A", .prefix = "abc", .starting_number = 5}); |
| AddNumKeyValue(nums.Get(), 900, |
| {.style = "D", .prefix = nullptr, .starting_number = 999}); |
| |
| AddLimitsArray(great_grand_kid5.Get(), 3000, 5000); |
| nums = great_grand_kid5->SetNewFor<CPDF_Array>(kNums); |
| AddNumKeyValue( |
| nums.Get(), 3000, |
| {.style = "r", .prefix = nullptr, .starting_number = std::nullopt}); |
| AddNumKeyValue( |
| nums.Get(), 5000, |
| {.style = "a", .prefix = nullptr, .starting_number = std::nullopt}); |
| } |
| |
| } // namespace |
| |
| class PageLabelTest : public TestWithPageModule { |
| public: |
| PageLabelTest() = default; |
| ~PageLabelTest() override = default; |
| |
| void SetUp() override { |
| TestWithPageModule::SetUp(); |
| |
| auto root_dict = pdfium::MakeRetain<CPDF_Dictionary>(); |
| root_dict->SetNewFor<CPDF_Dictionary>("Pages"); |
| auto page_labels_dict = root_dict->SetNewFor<CPDF_Dictionary>("PageLabels"); |
| FillPageLabelsTreeDict(page_labels_dict.Get()); |
| |
| doc_ = std::make_unique<CPDF_TestDocument>(); |
| doc_->SetRoot(std::move(root_dict)); |
| for (int page_index = 0; page_index < 10001; ++page_index) { |
| ASSERT_TRUE(doc_->CreateNewPage(page_index)); |
| } |
| |
| page_label_ = std::make_unique<CPDF_PageLabel>(doc_.get()); |
| } |
| |
| void TearDown() override { |
| page_label_.reset(); |
| doc_.reset(); |
| |
| TestWithPageModule::TearDown(); |
| } |
| |
| const CPDF_PageLabel* page_label() const { return page_label_.get(); } |
| |
| private: |
| std::unique_ptr<CPDF_TestDocument> doc_; |
| std::unique_ptr<CPDF_PageLabel> page_label_; |
| }; |
| |
| TEST_F(PageLabelTest, GetLabel) { |
| EXPECT_THAT(page_label()->GetLabel(-1), Eq(std::nullopt)); |
| EXPECT_THAT(page_label()->GetLabel(0), Optional(WideString(L"I"))); |
| EXPECT_THAT(page_label()->GetLabel(1), Optional(WideString(L"II"))); |
| EXPECT_THAT(page_label()->GetLabel(37), Optional(WideString(L"XXXVIII"))); |
| EXPECT_THAT(page_label()->GetLabel(99), Optional(WideString(L"C"))); |
| EXPECT_THAT(page_label()->GetLabel(100), Optional(WideString(L"abcE"))); |
| EXPECT_THAT(page_label()->GetLabel(101), Optional(WideString(L"abcF"))); |
| EXPECT_THAT(page_label()->GetLabel(525), |
| Optional(WideString(L"abcNNNNNNNNNNNNNNNNN"))); |
| EXPECT_THAT(page_label()->GetLabel(899), |
| Optional(WideString(L"abcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))); |
| EXPECT_THAT(page_label()->GetLabel(900), Optional(WideString(L"999"))); |
| EXPECT_THAT(page_label()->GetLabel(901), Optional(WideString(L"1000"))); |
| EXPECT_THAT(page_label()->GetLabel(1234), Optional(WideString(L"1333"))); |
| EXPECT_THAT(page_label()->GetLabel(2999), Optional(WideString(L"3098"))); |
| EXPECT_THAT(page_label()->GetLabel(3000), Optional(WideString(L"i"))); |
| EXPECT_THAT(page_label()->GetLabel(3001), Optional(WideString(L"ii"))); |
| EXPECT_THAT(page_label()->GetLabel(3579), Optional(WideString(L"dlxxx"))); |
| EXPECT_THAT(page_label()->GetLabel(4999), Optional(WideString(L"mm"))); |
| EXPECT_THAT(page_label()->GetLabel(5000), Optional(WideString(L"a"))); |
| EXPECT_THAT(page_label()->GetLabel(5001), Optional(WideString(L"b"))); |
| EXPECT_THAT(page_label()->GetLabel(7654), |
| Optional(WideString( |
| L"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" |
| L"cccccccccccccccccccccccccccccccccccccccccccc"))); |
| EXPECT_THAT( |
| page_label()->GetLabel(7999), |
| Optional(WideString( |
| L"jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj" |
| L"jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj"))); |
| EXPECT_THAT(page_label()->GetLabel(8000), Optional(WideString(L"x"))); |
| EXPECT_THAT(page_label()->GetLabel(8001), Optional(WideString(L"x"))); |
| EXPECT_THAT(page_label()->GetLabel(10000), Optional(WideString(L"x"))); |
| EXPECT_THAT(page_label()->GetLabel(10001), Eq(std::nullopt)); |
| } |
| |
| TEST_F(PageLabelTest, GetLabelPerf) { |
| for (int i = 0; i < 10001; ++i) { |
| page_label()->GetLabel(i); |
| } |
| } |