Skip to content
Open
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
282 changes: 282 additions & 0 deletions fpdfsdk/epdf_annot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4921,6 +4921,288 @@ EPDFAnnot_GetStructurePosition(FPDF_ANNOTATION annot,
return static_cast<unsigned long>(steps.size());
}

namespace {

// The structure element |annot_dict|'s /StructParent leads to, if it lists the
// annotation, and its object number (0 for a direct one).
struct AltElement {
RetainPtr<const CPDF_Dictionary> element;
uint32_t objnum = 0;
};

std::optional<AltElement> AltElementOf(const CPDF_Document* doc,
const CPDF_Dictionary* annot_dict) {
RetainPtr<const CPDF_Number> key =
ToNumber(annot_dict->GetDirectObjectFor("StructParent"));
const CPDF_Dictionary* catalog = doc->GetRoot();
if (!key || !key->IsInteger() || key->GetInteger() < 0 || !catalog) {
return std::nullopt;
}
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 std::nullopt;
}
RetainPtr<const CPDF_Object> entry =
CPDF_NumberTree(std::move(parent_tree)).LookupValue(key->GetInteger());
if (!entry) {
return std::nullopt;
}
RetainPtr<const CPDF_Dictionary> element = ToDictionary(entry->GetDirect());
if (!element) {
return std::nullopt;
}
const uint32_t annot_obj_num = annot_dict->GetObjNum();
const bool lists_it =
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;
}).has_value();
if (!lists_it) {
return std::nullopt;
}
// The number tree hands back the element itself, not the reference to it.
const uint32_t element_obj_num = element->GetObjNum();
return AltElement{element, element_obj_num};
}

// |holder|'s |key| as a dictionary to write: an indirect one through the
// document (so a layer copies it up first), a direct one in place.
RetainPtr<CPDF_Dictionary> WritableDictFor(CPDF_Document* doc,
CPDF_Dictionary* holder,
ByteStringView key) {
RetainPtr<CPDF_Object> value = holder->GetMutableObjectFor(key);
if (!value) {
return nullptr;
}
if (const CPDF_Reference* ref = value->AsReference()) {
return ToDictionary(doc->GetMutableIndirectObject(ref->GetRefObjNum()));
}
return ToDictionary(std::move(value));
}

// Appends |objnum| to |parent|'s /K, which becomes an array when it isn't.
void AppendStructKid(CPDF_Document* doc,
CPDF_Dictionary* parent,
uint32_t objnum) {
RetainPtr<CPDF_Object> kids = parent->GetMutableObjectFor("K");
RetainPtr<CPDF_Array> array;
if (const CPDF_Reference* ref = kids ? kids->AsReference() : nullptr) {
// An indirect array is written through the document; a reference to
// anything else is the one kid.
if (ToArray(ref->GetDirect())) {
array = ToArray(doc->GetMutableIndirectObject(ref->GetRefObjNum()));
}
} else if (kids) {
array = ToArray(kids);
}
if (kids && !array) {
RetainPtr<CPDF_Object> only = kids->Clone();
array = parent->SetNewFor<CPDF_Array>("K");
array->Append(std::move(only));
} else if (!array) {
array = parent->SetNewFor<CPDF_Array>("K");
}
array->AppendNew<CPDF_Reference>(doc, objnum);
}

} // namespace

FPDF_EXPORT unsigned long FPDF_CALLCONV
EPDFAnnot_GetAltText(FPDF_ANNOTATION annot,
FPDF_WCHAR* buffer,
unsigned long buflen) {
ScopedFPDFAnnotationView annot_view(annot);
CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot);
if (!ctx) {
return 0;
}
CPDF_Document* doc = ctx->GetPage()->GetDocument();
const CPDF_Dictionary* annot_dict = ctx->GetAnnotDict();
if (!doc || !annot_dict) {
return 0;
}
CPDF_DocumentViewScope document_view(doc);
std::optional<AltElement> found = AltElementOf(doc, annot_dict);
if (!found || !found->element->KeyExist("Alt")) {
return 0;
}
// SAFETY: required from caller.
return Utf16EncodeMaybeCopyAndReturnLength(
found->element->GetUnicodeTextFor("Alt"),
UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen)));
}

FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
EPDFAnnot_SetAltText(FPDF_ANNOTATION annot, FPDF_WIDESTRING alt) {
ScopedFPDFAnnotationView annot_view(annot);
CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot);
if (!ctx) {
return false;
}
CPDF_Document* doc = ctx->GetPage()->GetDocument();
if (!doc) {
return false;
}
CPDF_DocumentViewScope document_view(doc);
RetainPtr<CPDF_Dictionary> annot_dict =
GetMutableAnnotDictFromFPDFAnnotation(annot);
if (!annot_dict) {
return false;
}
CPDF_Page* page = ctx->GetPage()->AsPDFPage();
RetainPtr<const CPDF_Dictionary> page_dict = page ? page->GetDict() : nullptr;
const uint32_t page_obj_num = page_dict ? page_dict->GetObjNum() : 0;
// SAFETY: required from caller.
const WideString text =
alt ? UNSAFE_BUFFERS(WideStringFromFPDFWideString(alt)) : WideString();

// An inline annotation is in no tree, and the tree can only list an
// object: it becomes one first.
if (!annot_dict->GetObjNum()) {
if (text.IsEmpty()) {
return true;
}
const int page_index = page_obj_num ? doc->GetPageIndex(page_obj_num) : -1;
if (page_index < 0 || PromoteInlineAnnots(doc, page_index) < 0) {
return false;
}
annot_dict = GetMutableAnnotDictFromFPDFAnnotation(annot);
if (!annot_dict || !annot_dict->GetObjNum()) {
return false;
}
}
const uint32_t annot_obj_num = annot_dict->GetObjNum();

// Already in the tree: the element that lists it takes the text.
if (std::optional<AltElement> found = AltElementOf(doc, annot_dict.Get())) {
if (!found->objnum) {
return false;
}
RetainPtr<CPDF_Dictionary> element =
ToDictionary(doc->GetMutableIndirectObject(found->objnum));
if (!element) {
return false;
}
if (text.IsEmpty()) {
element->RemoveFor("Alt");
} else {
element->SetNewFor<CPDF_String>("Alt", text.AsStringView());
}
return true;
}
if (text.IsEmpty()) {
return true;
}

// Everything is checked before anything is written: a refusal changes
// nothing.
RetainPtr<CPDF_Dictionary> catalog = doc->GetMutableRoot();
if (!catalog) {
return false;
}
RetainPtr<const CPDF_Object> existing_root =
catalog->GetDirectObjectFor("StructTreeRoot");
if (existing_root && !existing_root->IsDictionary()) {
return false;
}
if (existing_root) {
RetainPtr<const CPDF_Object> existing_tree =
existing_root->AsDictionary()->GetDirectObjectFor("ParentTree");
if (existing_tree && (!existing_tree->IsDictionary() ||
existing_tree->AsDictionary()->KeyExist("Kids"))) {
return false;
}
}

RetainPtr<CPDF_Dictionary> tree_root;
if (existing_root) {
tree_root = WritableDictFor(doc, catalog.Get(), "StructTreeRoot");
} else {
tree_root = doc->NewIndirect<CPDF_Dictionary>();
tree_root->SetNewFor<CPDF_Name>("Type", "StructTreeRoot");
catalog->SetNewFor<CPDF_Reference>("StructTreeRoot", doc,
tree_root->GetObjNum());
}
if (!tree_root) {
return false;
}
RetainPtr<CPDF_Dictionary> parent_tree =
WritableDictFor(doc, tree_root.Get(), "ParentTree");
if (!parent_tree) {
parent_tree = doc->NewIndirect<CPDF_Dictionary>();
tree_root->SetNewFor<CPDF_Reference>("ParentTree", doc,
parent_tree->GetObjNum());
}
RetainPtr<CPDF_Array> nums = parent_tree->GetMutableArrayFor("Nums");
if (!nums) {
nums = parent_tree->SetNewFor<CPDF_Array>("Nums");
}

// The key: /ParentTreeNextKey, past every key in use.
int key = 0;
if (RetainPtr<const CPDF_Number> next =
ToNumber(tree_root->GetDirectObjectFor("ParentTreeNextKey"));
next && next->IsInteger() && next->GetInteger() > 0) {
key = next->GetInteger();
}
for (size_t i = 0; i + 1 < nums->size(); i += 2) {
key = std::max(key, nums->GetIntegerAt(i) + 1);
}

// The element goes into the tree's one /Document element, if it has one.
RetainPtr<CPDF_Dictionary> parent = tree_root;
if (RetainPtr<const CPDF_Object> kids = tree_root->GetObjectFor("K");
kids && kids->AsReference()) {
RetainPtr<const CPDF_Dictionary> only = ToDictionary(kids->GetDirect());
if (only && only->GetNameFor("S") == "Document") {
RetainPtr<CPDF_Dictionary> document_element = ToDictionary(
doc->GetMutableIndirectObject(kids->AsReference()->GetRefObjNum()));
if (document_element) {
parent = std::move(document_element);
}
}
}

RetainPtr<CPDF_Dictionary> element = doc->NewIndirect<CPDF_Dictionary>();
element->SetNewFor<CPDF_Name>("Type", "StructElem");
element->SetNewFor<CPDF_Name>("S", "Figure");
element->SetNewFor<CPDF_Reference>("P", doc, parent->GetObjNum());
if (page_obj_num) {
element->SetNewFor<CPDF_Reference>("Pg", doc, page_obj_num);
}
element->SetNewFor<CPDF_String>("Alt", text.AsStringView());
RetainPtr<CPDF_Dictionary> objr = element->SetNewFor<CPDF_Dictionary>("K");
objr->SetNewFor<CPDF_Name>("Type", "OBJR");
objr->SetNewFor<CPDF_Reference>("Obj", doc, annot_obj_num);
if (page_obj_num) {
objr->SetNewFor<CPDF_Reference>("Pg", doc, page_obj_num);
}
AppendStructKid(doc, parent.Get(), element->GetObjNum());

// The parent tree keeps its keys in order.
size_t at = nums->size() - nums->size() % 2;
for (size_t i = 0; i + 1 < nums->size(); i += 2) {
if (nums->GetIntegerAt(i) > key) {
at = i;
break;
}
}
nums->InsertNewAt<CPDF_Number>(at, key);
nums->InsertNewAt<CPDF_Reference>(at + 1, doc, element->GetObjNum());
tree_root->SetNewFor<CPDF_Number>("ParentTreeNextKey", key + 1);
annot_dict->SetNewFor<CPDF_Number>("StructParent", key);
return true;
}

FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV
EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) {
if (!page || obj_num == 0) {
Expand Down
50 changes: 50 additions & 0 deletions fpdfsdk/epdf_annot_embeddertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10410,3 +10410,53 @@ TEST_F(EPDFAnnotStructureEmbedderTest, AnUntaggedDocumentHasNoPositions) {
ASSERT_TRUE(annot);
EXPECT_EQ(0u, EPDFAnnot_GetStructurePosition(annot.get(), nullptr, 0));
}

// Alt text lives in the structure tree, as PDF.js writes a stamp's: a /Figure
// element with the /Alt, listing the annotation, which gets a /StructParent.
TEST_F(EPDFAnnotEmbedderTest, AltTextRoundTripsThroughTheStructureTree) {
embedpdf_test::ScopedTestDocument doc(NewTestDocument());
ASSERT_TRUE(doc);
ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 612, 792));
ASSERT_TRUE(page);
ScopedFPDFAnnotation stamp(
FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_STAMP));
ASSERT_TRUE(stamp);
ScopedFPDFAnnotation square(
FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_SQUARE));
ASSERT_TRUE(square);

auto alt_of = [](FPDF_ANNOTATION annot) {
const unsigned long length = EPDFAnnot_GetAltText(annot, nullptr, 0);
if (length == 0) {
return std::wstring();
}
std::vector<FPDF_WCHAR> buffer = GetFPDFWideStringBuffer(length);
EXPECT_EQ(length, EPDFAnnot_GetAltText(annot, buffer.data(), length));
return GetPlatformWString(buffer.data());
};

EXPECT_EQ(L"", alt_of(stamp.get()));
ScopedFPDFWideString first = GetFPDFWideString(L"A red stamp");
ASSERT_TRUE(EPDFAnnot_SetAltText(stamp.get(), first.get()));
EXPECT_EQ(L"A red stamp", alt_of(stamp.get()));
ScopedFPDFWideString second = GetFPDFWideString(L"A square");
ASSERT_TRUE(EPDFAnnot_SetAltText(square.get(), second.get()));
EXPECT_EQ(L"A square", alt_of(square.get()));
EXPECT_EQ(L"A red stamp", alt_of(stamp.get()));

// Both are /Figure kids of the root, at their own parent tree keys.
std::vector<unsigned int> steps(4);
EXPECT_EQ(2u, EPDFAnnot_GetStructurePosition(stamp.get(), steps.data(), 4));
EXPECT_EQ(0u, steps[0]);
EXPECT_EQ(2u, EPDFAnnot_GetStructurePosition(square.get(), steps.data(), 4));
EXPECT_EQ(1u, steps[0]);

// Setting it again rewrites the element; empty removes the /Alt.
ScopedFPDFWideString changed = GetFPDFWideString(L"A blue stamp");
ASSERT_TRUE(EPDFAnnot_SetAltText(stamp.get(), changed.get()));
EXPECT_EQ(L"A blue stamp", alt_of(stamp.get()));
EXPECT_EQ(2u, EPDFAnnot_GetStructurePosition(stamp.get(), steps.data(), 4));
EXPECT_EQ(0u, steps[0]);
ASSERT_TRUE(EPDFAnnot_SetAltText(stamp.get(), nullptr));
EXPECT_EQ(L"", alt_of(stamp.get()));
}
2 changes: 2 additions & 0 deletions fpdfsdk/epdf_c_api_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ int CheckEmbedPDFCApi(void) {
CHK(EPDFAction_NodeHasJavaScript);
CHK(EPDFAction_SetNext);
CHK(EPDFAnnot_GetActionModel);
CHK(EPDFAnnot_GetAltText);
CHK(EPDFAnnot_SetAltText);
CHK(EPDFAnnot_SetEventAction);
CHK(EPDFDoc_GetAdditionalActionModel);
CHK(EPDFDoc_GetNamedJavaScriptActionModel);
Expand Down
40 changes: 40 additions & 0 deletions public/epdf_annot.h
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,46 @@ EPDFAnnot_GetStructurePosition(FPDF_ANNOTATION annot,
unsigned int* buffer,
unsigned long buflen);

// Experimental EmbedPDF Extension API.
// Get the alternate description of an annotation: the /Alt of the structure
// element that lists it (ISO 32000-2 14.9.3), found through its
// /StructParent and the parent tree.
//
// annot - handle to an annotation.
// buffer - receives the text as UTF-16LE with a terminating NUL; may be
// NULL.
// buflen - size of |buffer| in bytes.
//
// Returns the number of bytes the text needs, including the NUL; 0 when the
// annotation isn't in the structure tree or its element has no /Alt.
FPDF_EXPORT unsigned long FPDF_CALLCONV
EPDFAnnot_GetAltText(FPDF_ANNOTATION annot,
FPDF_WCHAR* buffer,
unsigned long buflen);

// Experimental EmbedPDF Extension API.
// Set the alternate description of an annotation, as PDF.js writes a stamp's
// alt text: in the structure tree, not in the annotation.
//
// An annotation already in the structure tree gets the /Alt on the element
// that lists it (an empty |alt| removes it). Otherwise a non-empty |alt|
// creates a /Figure element with the /Alt and an object reference (/OBJR) to
// the annotation, appended to the tree's /Document element when the root's
// one kid is one, else to the root; the annotation gets a /StructParent and
// the parent tree an entry for it. The catalog's /StructTreeRoot and its
// /ParentTree are created when missing.
//
// annot - handle to an annotation; its dictionary must be an indirect
// object.
// alt - the description, UTF-16LE with a terminating NUL; NULL or empty
// removes it.
//
// Returns TRUE on success; FALSE for a direct annotation dictionary, a parent
// tree with /Kids (only a flat /Nums is written), or a structure tree that
// isn't dictionaries where it needs them.
FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
EPDFAnnot_SetAltText(FPDF_ANNOTATION annot, FPDF_WIDESTRING alt);

// 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