Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions fpdfsdk/epdf_annot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
#include "core/fpdfdoc/cpdf_fontface.h"
#include "core/fpdfdoc/cpdf_formfield.h"
#include "core/fpdfdoc/cpdf_interactiveform.h"
#include "core/fpdfdoc/cpdf_numbertree.h"
#include "core/fpdfdoc/cpdf_richtextjson.h"
#include "core/fpdfdoc/cpdf_richtextparser.h"
#include "core/fpdfdoc/cpdf_richtextwriter.h"
Expand Down Expand Up @@ -4719,6 +4720,124 @@ EPDFAnnot_GetObjectNumber(FPDF_ANNOTATION annot) {
return dict->GetObjNum();
}

namespace {

// How deep EPDFAnnot_GetStructurePosition() climbs before it gives up.
constexpr size_t kMaxStructureDepth = 256;

// The index of the first of |node|'s /K kids that |is_it| accepts, or nullopt.
// /K is one kid or an array of them (ISO 32000-2 Table 355).
template <typename Predicate>
std::optional<unsigned int> KidIndex(const CPDF_Dictionary* node,
Predicate is_it) {
RetainPtr<const CPDF_Object> kids = node->GetDirectObjectFor("K");
if (!kids) {
return std::nullopt;
}
const CPDF_Array* array = kids->AsArray();
if (!array) {
return is_it(kids.Get()) ? std::optional<unsigned int>(0) : std::nullopt;
}
for (size_t i = 0; i < array->size(); ++i) {
RetainPtr<const CPDF_Object> kid = array->GetDirectObjectAt(i);
if (kid && is_it(kid.Get())) {
return static_cast<unsigned int>(i);
}
}
return std::nullopt;
}

// |annot|'s place in the structure tree, the root's index first; empty when it
// isn't in the tree. See EPDFAnnot_GetStructurePosition().
std::vector<unsigned int> StructurePositionOf(const CPDF_Document* doc,
const CPDF_Dictionary* annot) {
RetainPtr<const CPDF_Number> key =
ToNumber(annot->GetDirectObjectFor("StructParent"));
const CPDF_Dictionary* catalog = doc->GetRoot();
if (!key || !key->IsInteger() || key->GetInteger() < 0 || !catalog) {
return {};
}
RetainPtr<const CPDF_Dictionary> tree_root =
catalog->GetDictFor("StructTreeRoot");
RetainPtr<const CPDF_Dictionary> parent_tree =
tree_root ? tree_root->GetDictFor("ParentTree") : nullptr;
if (!parent_tree) {
return {};
}
// An annotation's entry in the parent tree is the element whose /K holds its
// object reference (ISO 32000-2 14.7.5.4).
RetainPtr<const CPDF_Object> entry =
CPDF_NumberTree(std::move(parent_tree)).LookupValue(key->GetInteger());
RetainPtr<const CPDF_Dictionary> element =
ToDictionary(entry ? entry->GetDirect() : nullptr);
if (!element) {
return {};
}
const uint32_t annot_obj_num = annot->GetObjNum();
std::optional<unsigned int> step =
KidIndex(element.Get(), [annot_obj_num](const CPDF_Object* kid) {
const CPDF_Dictionary* objr = kid->AsDictionary();
if (!objr || objr->GetNameFor("Type") != "OBJR") {
return false;
}
RetainPtr<const CPDF_Reference> target =
ToReference(objr->GetObjectFor("Obj"));
return target && annot_obj_num &&
target->GetRefObjNum() == annot_obj_num;
});
if (!step) {
return {};
}

std::vector<unsigned int> steps = {*step};
std::set<const CPDF_Dictionary*> seen = {element.Get()};
RetainPtr<const CPDF_Dictionary> node = element;
while (steps.size() <= kMaxStructureDepth) {
RetainPtr<const CPDF_Dictionary> parent = node->GetDictFor("P");
if (!parent || !seen.insert(parent.Get()).second) {
return {};
}
const CPDF_Dictionary* child = node.Get();
step = KidIndex(parent.Get(), [child](const CPDF_Object* kid) {
return kid->AsDictionary() == child;
});
if (!step) {
return {};
}
steps.push_back(*step);
if (parent == tree_root) {
std::reverse(steps.begin(), steps.end());
return steps;
}
node = std::move(parent);
}
return {};
}

} // namespace

FPDF_EXPORT unsigned long FPDF_CALLCONV
EPDFAnnot_GetStructurePosition(FPDF_ANNOTATION annot,
unsigned int* buffer,
unsigned long buflen) {
ScopedFPDFAnnotationView annot_view(annot);
CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot);
if (!ctx) {
return 0;
}
CPDF_Document* doc = ctx->GetPage()->GetDocument();
CPDF_DocumentViewScope document_view(doc);
const CPDF_Dictionary* annot_dict = ctx->GetAnnotDict();
if (!annot_dict || !doc) {
return 0;
}
const std::vector<unsigned int> steps = StructurePositionOf(doc, annot_dict);
if (buffer) {
std::copy_n(steps.begin(), std::min<size_t>(steps.size(), buflen), buffer);
}
return static_cast<unsigned long>(steps.size());
}

FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV
EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) {
if (!page || obj_num == 0) {
Expand Down
79 changes: 79 additions & 0 deletions fpdfsdk/epdf_annot_embeddertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9594,3 +9594,82 @@ INSTANTIATE_TEST_SUITE_P(AppearanceSharing,
"SharedStates"};
return kNames[info.param];
}));

// Where an annotation sits in its document's structure tree
// (structure_position.in): the page lists its fields a to g, and its
// structure reads c, a, b.
class EPDFAnnotStructureEmbedderTest : public EmbedderTest {
protected:
// The position of the first page's annotation at |index|, read from the
// page's dictionary without loading the page, as the engine reads it.
std::vector<unsigned int> PositionOf(int index) {
ScopedFPDFAnnotation annot(EPDFPage_GetAnnotRaw(document(), 0, index));
EXPECT_TRUE(annot);
const unsigned long count =
EPDFAnnot_GetStructurePosition(annot.get(), nullptr, 0);
std::vector<unsigned int> steps(count);
EXPECT_EQ(count, EPDFAnnot_GetStructurePosition(annot.get(), steps.data(),
steps.size()));
return steps;
}
};

TEST_F(EPDFAnnotStructureEmbedderTest,
FindsEachTaggedAnnotationFromItsElementUp) {
ASSERT_TRUE(OpenDocument("structure_position.pdf"));
// a: the first section's third kid; its reference follows a marked-content
// kid in its element.
EXPECT_THAT(PositionOf(0), ::testing::ElementsAre(0u, 0u, 2u, 1u));
// b: the second section's first kid; its element's /K is the reference
// itself, not an array.
EXPECT_THAT(PositionOf(1), ::testing::ElementsAre(0u, 1u, 0u, 0u));
// c: the first section's first kid.
EXPECT_THAT(PositionOf(2), ::testing::ElementsAre(0u, 0u, 0u, 0u));
}

TEST_F(EPDFAnnotStructureEmbedderTest,
ComparedIndexByIndexPositionsReadInTheTreesOrder) {
ASSERT_TRUE(OpenDocument("structure_position.pdf"));
std::vector<std::pair<std::vector<unsigned int>, char>> tagged = {
{PositionOf(0), 'a'}, {PositionOf(1), 'b'}, {PositionOf(2), 'c'}};
std::sort(tagged.begin(), tagged.end());
std::string order;
for (const auto& [position, name] : tagged) {
order += name;
}
EXPECT_EQ("cab", order);
}

TEST_F(EPDFAnnotStructureEmbedderTest,
AnAnnotationTheTreeDoesNotReachHasNoPosition) {
ASSERT_TRUE(OpenDocument("structure_position.pdf"));
// d: no /StructParent.
EXPECT_TRUE(PositionOf(3).empty());
// e: the parent tree names an element that doesn't list it.
EXPECT_TRUE(PositionOf(4).empty());
// f: its element's way up is a cycle that never reaches the root.
EXPECT_TRUE(PositionOf(5).empty());
// g: a /StructParent the parent tree doesn't have.
EXPECT_TRUE(PositionOf(6).empty());
}

TEST_F(EPDFAnnotStructureEmbedderTest,
ReadsTheSameOnALoadedPageAndCountsPastTheBuffer) {
ASSERT_TRUE(OpenDocument("structure_position.pdf"));
ScopedPage page = LoadScopedPage(0);
ASSERT_TRUE(page);
ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0));
ASSERT_TRUE(annot);
std::array<unsigned int, 2> first_two = {99u, 99u};
EXPECT_EQ(4u, EPDFAnnot_GetStructurePosition(annot.get(), first_two.data(),
first_two.size()));
EXPECT_THAT(first_two, ::testing::ElementsAre(0u, 0u));
EXPECT_EQ(0u, EPDFAnnot_GetStructurePosition(nullptr, nullptr, 0));
}

TEST_F(EPDFAnnotStructureEmbedderTest, AnUntaggedDocumentHasNoPositions) {
ASSERT_TRUE(OpenDocument("annotation_highlight_long_content.pdf"));
ScopedFPDFAnnotation annot(EPDFPage_GetAnnotRaw(document(), 0, 0));
ASSERT_TRUE(annot);
EXPECT_EQ(0u, EPDFAnnot_GetStructurePosition(annot.get(), nullptr, 0));
}
1 change: 1 addition & 0 deletions fpdfsdk/epdf_c_api_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ int CheckEmbedPDFCApi(void) {
CHK(EPDFAnnot_GetRichTextJSON);
CHK(EPDFAnnot_GetRotate);
CHK(EPDFAnnot_GetStampDrawing);
CHK(EPDFAnnot_GetStructurePosition);
CHK(EPDFAnnot_GetTextAlignment);
CHK(EPDFAnnot_HasAppearanceStream);
CHK(EPDFAnnot_HasEmbedMetadata);
Expand Down
23 changes: 23 additions & 0 deletions public/epdf_annot.h
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,29 @@ EPDFAnnot_SetCalloutLine(FPDF_ANNOTATION annot,
FPDF_EXPORT unsigned int FPDF_CALLCONV
EPDFAnnot_GetObjectNumber(FPDF_ANNOTATION annot);

// Experimental EmbedPDF Extension API.
// Where an annotation sits in its document's logical structure (the tags,
// ISO 32000-2 14.7): from the structure tree root down to the annotation's
// object reference (/OBJR), the index of each step among its parent's /K
// kids. Compared index by index, two positions give the structure's reading
// order, the order a page's /Tabs /S gives its annotations focus in. It is
// found from the annotation up: its /StructParent, the parent tree, then each
// element's /P. No page is loaded, and nothing else in the tree is read.
//
// annot - handle to an annotation.
// buffer - receives up to |buflen| indexes, the root's first; may be NULL.
// buflen - capacity of |buffer|, in entries.
//
// Returns the number of indexes, which may exceed |buflen|; 0 when the
// annotation isn't in the structure tree: it has no /StructParent, the parent
// tree doesn't lead to an element that lists it, or the way up is broken (a
// missing /P, a parent that doesn't list its kid, a cycle, or more than 256
// levels).
FPDF_EXPORT unsigned long FPDF_CALLCONV
EPDFAnnot_GetStructurePosition(FPDF_ANNOTATION annot,
unsigned int* buffer,
unsigned long buflen);

// Experimental EmbedPDF Extension API.
// Get an annotation on a page by its PDF indirect object number.
// Similar to EPDFPage_GetAnnotByName() but keyed by object number.
Expand Down
Loading