• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/pdf/SkPDFTag.h"
9 
10 #include "include/core/SkPoint.h"
11 #include "include/core/SkScalar.h"
12 #include "include/private/base/SkAssert.h"
13 #include "include/private/base/SkTo.h"
14 #include "src/pdf/SkPDFDocumentPriv.h"
15 
16 #include <algorithm>
17 #include <memory>
18 #include <utility>
19 
20 using namespace skia_private;
21 
22 // The struct parent tree consists of one entry per page, followed by
23 // entries for individual struct tree nodes corresponding to
24 // annotations.  Each entry is a key/value pair with an integer key
25 // and an indirect reference key.
26 //
27 // The page entries get consecutive keys starting at 0. Since we don't
28 // know the total number of pages in the document at the time we start
29 // processing annotations, start the key for annotations with a large
30 // number, which effectively becomes the maximum number of pages in a
31 // PDF we can handle.
32 const int kFirstAnnotationStructParentKey = 100000;
33 
34 namespace {
35 struct Location {
36     SkPoint fPoint{SK_ScalarNaN, SK_ScalarNaN};
37     unsigned fPageIndex{0};
38 
accumulate__anon1a399b610111::Location39     void accumulate(Location const& child) {
40         if (!child.fPoint.isFinite()) {
41             return;
42         }
43         if (!fPoint.isFinite()) {
44             *this = child;
45             return;
46         }
47         if (child.fPageIndex < fPageIndex) {
48             *this = child;
49             return;
50         }
51         if (child.fPageIndex == fPageIndex) {
52             fPoint.fX = std::min(child.fPoint.fX, fPoint.fX);
53             fPoint.fY = std::max(child.fPoint.fY, fPoint.fY); // PDF y-up
54             return;
55         }
56     }
57 };
58 } // namespace
59 
60 struct SkPDFTagNode {
61     // Structure element nodes need a unique alphanumeric ID,
62     // and we need to be able to output them sorted in lexicographic
63     // order. This helper function takes one of our node IDs and
64     // builds an ID string that zero-pads the digits so that lexicographic
65     // order matches numeric order.
nodeIdToStringSkPDFTagNode66     static SkString nodeIdToString(int nodeId) {
67         SkString idString;
68         idString.printf("node%08d", nodeId);
69         return idString;
70     }
71 
72     SkPDFTagNode* fChildren = nullptr;
73     size_t fChildCount = 0;
74     struct MarkedContentInfo {
75         Location fLocation;
76         int fMarkId;
77     };
78     TArray<MarkedContentInfo> fMarkedContent;
79     int fNodeId;
80     bool fWantTitle;
81     SkString fTypeString;
82     SkString fTitle;
83     SkString fAlt;
84     SkString fLang;
85     SkPDFIndirectReference fRef;
86     enum State {
87         kUnknown,
88         kYes,
89         kNo,
90     } fCanDiscard = kUnknown;
91     std::unique_ptr<SkPDFArray> fAttributes;
92     struct AnnotationInfo {
93         unsigned fPageIndex;
94         SkPDFIndirectReference fAnnotationRef;
95     };
96     std::vector<AnnotationInfo> fAnnotations;
97 };
98 
99 SkPDF::AttributeList::AttributeList() = default;
100 
101 SkPDF::AttributeList::~AttributeList() = default;
102 
appendInt(const char * owner,const char * name,int value)103 void SkPDF::AttributeList::appendInt(
104         const char* owner, const char* name, int value) {
105     if (!fAttrs)
106         fAttrs = SkPDFMakeArray();
107     std::unique_ptr<SkPDFDict> attrDict = SkPDFMakeDict();
108     attrDict->insertName("O", owner);
109     attrDict->insertInt(name, value);
110     fAttrs->appendObject(std::move(attrDict));
111 }
112 
appendFloat(const char * owner,const char * name,float value)113 void SkPDF::AttributeList::appendFloat(
114         const char* owner, const char* name, float value) {
115     if (!fAttrs)
116         fAttrs = SkPDFMakeArray();
117     std::unique_ptr<SkPDFDict> attrDict = SkPDFMakeDict();
118     attrDict->insertName("O", owner);
119     attrDict->insertScalar(name, value);
120     fAttrs->appendObject(std::move(attrDict));
121 }
122 
appendName(const char * owner,const char * name,const char * value)123 void SkPDF::AttributeList::appendName(
124         const char* owner, const char* name, const char* value) {
125     if (!fAttrs)
126         fAttrs = SkPDFMakeArray();
127     std::unique_ptr<SkPDFDict> attrDict = SkPDFMakeDict();
128     attrDict->insertName("O", owner);
129     attrDict->insertName(name, value);
130     fAttrs->appendObject(std::move(attrDict));
131 }
132 
appendFloatArray(const char * owner,const char * name,const std::vector<float> & value)133 void SkPDF::AttributeList::appendFloatArray(
134         const char* owner, const char* name, const std::vector<float>& value) {
135     if (!fAttrs)
136         fAttrs = SkPDFMakeArray();
137     std::unique_ptr<SkPDFDict> attrDict = SkPDFMakeDict();
138     attrDict->insertName("O", owner);
139     std::unique_ptr<SkPDFArray> pdfArray = SkPDFMakeArray();
140     for (float element : value) {
141         pdfArray->appendScalar(element);
142     }
143     attrDict->insertObject(name, std::move(pdfArray));
144     fAttrs->appendObject(std::move(attrDict));
145 }
146 
appendNodeIdArray(const char * owner,const char * name,const std::vector<int> & nodeIds)147 void SkPDF::AttributeList::appendNodeIdArray(
148         const char* owner,
149         const char* name,
150         const std::vector<int>& nodeIds) {
151     if (!fAttrs)
152         fAttrs = SkPDFMakeArray();
153     std::unique_ptr<SkPDFDict> attrDict = SkPDFMakeDict();
154     attrDict->insertName("O", owner);
155     std::unique_ptr<SkPDFArray> pdfArray = SkPDFMakeArray();
156     for (int nodeId : nodeIds) {
157         SkString idString = SkPDFTagNode::nodeIdToString(nodeId);
158         pdfArray->appendByteString(idString);
159     }
160     attrDict->insertObject(name, std::move(pdfArray));
161     fAttrs->appendObject(std::move(attrDict));
162 }
163 
SkPDFTagTree()164 SkPDFTagTree::SkPDFTagTree() : fArena(4 * sizeof(SkPDFTagNode)) {}
165 
166 SkPDFTagTree::~SkPDFTagTree() = default;
167 
168 // static
Copy(SkPDF::StructureElementNode & node,SkPDFTagNode * dst,SkArenaAlloc * arena,THashMap<int,SkPDFTagNode * > * nodeMap,bool wantTitle)169 void SkPDFTagTree::Copy(SkPDF::StructureElementNode& node,
170                         SkPDFTagNode* dst,
171                         SkArenaAlloc* arena,
172                         THashMap<int, SkPDFTagNode*>* nodeMap,
173                         bool wantTitle) {
174     nodeMap->set(node.fNodeId, dst);
175     for (int nodeId : node.fAdditionalNodeIds) {
176         SkASSERT(!nodeMap->find(nodeId));
177         nodeMap->set(nodeId, dst);
178     }
179     dst->fNodeId = node.fNodeId;
180 
181     // Accumulate title text, need to be in sync with create_outline_from_headers
182     const char* type = node.fTypeString.c_str();
183     wantTitle |= fOutline == SkPDF::Metadata::Outline::StructureElementHeaders &&
184                  type[0] == 'H' && '1' <= type[1] && type[1] <= '6';
185     dst->fWantTitle = wantTitle;
186 
187     dst->fTypeString = node.fTypeString;
188     dst->fAlt = node.fAlt;
189     dst->fLang = node.fLang;
190 
191     size_t childCount = node.fChildVector.size();
192     SkPDFTagNode* children = arena->makeArray<SkPDFTagNode>(childCount);
193     dst->fChildCount = childCount;
194     dst->fChildren = children;
195     for (size_t i = 0; i < childCount; ++i) {
196         Copy(*node.fChildVector[i], &children[i], arena, nodeMap, wantTitle);
197     }
198 
199     dst->fAttributes = std::move(node.fAttributes.fAttrs);
200 }
201 
init(SkPDF::StructureElementNode * node,SkPDF::Metadata::Outline outline)202 void SkPDFTagTree::init(SkPDF::StructureElementNode* node, SkPDF::Metadata::Outline outline) {
203     if (node) {
204         fRoot = fArena.make<SkPDFTagNode>();
205         fOutline = outline;
206         Copy(*node, fRoot, &fArena, &fNodeMap, false);
207     }
208 }
209 
id()210 int SkPDFTagTree::Mark::id() {
211     return fNode ? fNode->fMarkedContent[fMarkIndex].fMarkId : -1;
212 }
213 
point()214 SkPoint& SkPDFTagTree::Mark::point() {
215     return fNode->fMarkedContent[fMarkIndex].fLocation.fPoint;
216 }
217 
createMarkIdForNodeId(int nodeId,unsigned pageIndex,SkPoint point)218 auto SkPDFTagTree::createMarkIdForNodeId(int nodeId, unsigned pageIndex, SkPoint point) -> Mark {
219     if (!fRoot) {
220         return Mark();
221     }
222     SkPDFTagNode** tagPtr = fNodeMap.find(nodeId);
223     if (!tagPtr) {
224         return Mark();
225     }
226     SkPDFTagNode* tag = *tagPtr;
227     SkASSERT(tag);
228     while (SkToUInt(fMarksPerPage.size()) < pageIndex + 1) {
229         fMarksPerPage.push_back();
230     }
231     TArray<SkPDFTagNode*>& pageMarks = fMarksPerPage[pageIndex];
232     int markId = pageMarks.size();
233     tag->fMarkedContent.push_back({{point, pageIndex}, markId});
234     pageMarks.push_back(tag);
235     return Mark(tag, tag->fMarkedContent.size() - 1);
236 }
237 
createStructParentKeyForNodeId(int nodeId,unsigned pageIndex)238 int SkPDFTagTree::createStructParentKeyForNodeId(int nodeId, unsigned pageIndex) {
239     if (!fRoot) {
240         return -1;
241     }
242     SkPDFTagNode** tagPtr = fNodeMap.find(nodeId);
243     if (!tagPtr) {
244         return -1;
245     }
246     SkPDFTagNode* tag = *tagPtr;
247     SkASSERT(tag);
248 
249     tag->fCanDiscard = SkPDFTagNode::kNo;
250 
251     int nextStructParentKey = kFirstAnnotationStructParentKey +
252         static_cast<int>(fParentTreeAnnotationNodeIds.size());
253     fParentTreeAnnotationNodeIds.push_back(nodeId);
254     return nextStructParentKey;
255 }
256 
can_discard(SkPDFTagNode * node)257 static bool can_discard(SkPDFTagNode* node) {
258     if (node->fCanDiscard == SkPDFTagNode::kYes) {
259         return true;
260     }
261     if (node->fCanDiscard == SkPDFTagNode::kNo) {
262         return false;
263     }
264     if (!node->fMarkedContent.empty()) {
265         node->fCanDiscard = SkPDFTagNode::kNo;
266         return false;
267     }
268     for (size_t i = 0; i < node->fChildCount; ++i) {
269         if (!can_discard(&node->fChildren[i])) {
270             node->fCanDiscard = SkPDFTagNode::kNo;
271             return false;
272         }
273     }
274     node->fCanDiscard = SkPDFTagNode::kYes;
275     return true;
276 }
277 
PrepareTagTreeToEmit(SkPDFIndirectReference parent,SkPDFTagNode * node,SkPDFDocument * doc)278 SkPDFIndirectReference SkPDFTagTree::PrepareTagTreeToEmit(SkPDFIndirectReference parent,
279                                                           SkPDFTagNode* node,
280                                                           SkPDFDocument* doc) {
281     SkPDFIndirectReference ref = doc->reserveRef();
282     std::unique_ptr<SkPDFArray> kids = SkPDFMakeArray();
283     SkPDFTagNode* children = node->fChildren;
284     size_t childCount = node->fChildCount;
285     for (size_t i = 0; i < childCount; ++i) {
286         SkPDFTagNode* child = &children[i];
287         if (!(can_discard(child))) {
288             kids->appendRef(PrepareTagTreeToEmit(ref, child, doc));
289         }
290     }
291     for (const SkPDFTagNode::MarkedContentInfo& info : node->fMarkedContent) {
292         std::unique_ptr<SkPDFDict> mcr = SkPDFMakeDict("MCR");
293         mcr->insertRef("Pg", doc->getPage(info.fLocation.fPageIndex));
294         mcr->insertInt("MCID", info.fMarkId);
295         kids->appendObject(std::move(mcr));
296     }
297     for (const SkPDFTagNode::AnnotationInfo& annotationInfo : node->fAnnotations) {
298         std::unique_ptr<SkPDFDict> annotationDict = SkPDFMakeDict("OBJR");
299         annotationDict->insertRef("Obj", annotationInfo.fAnnotationRef);
300         annotationDict->insertRef("Pg", doc->getPage(annotationInfo.fPageIndex));
301         kids->appendObject(std::move(annotationDict));
302     }
303     node->fRef = ref;
304     SkPDFDict dict("StructElem");
305     dict.insertName("S", node->fTypeString.isEmpty() ? "NonStruct" : node->fTypeString.c_str());
306     if (!node->fAlt.isEmpty()) {
307         dict.insertTextString("Alt", node->fAlt);
308     }
309     if (!node->fLang.isEmpty()) {
310         dict.insertTextString("Lang", node->fLang);
311     }
312     dict.insertRef("P", parent);
313     dict.insertObject("K", std::move(kids));
314     if (node->fAttributes) {
315         dict.insertObject("A", std::move(node->fAttributes));
316     }
317 
318     // Each node has a unique ID that also needs to be referenced
319     // in a separate IDTree node, along with the lowest and highest
320     // unique ID string.
321     SkString idString = SkPDFTagNode::nodeIdToString(node->fNodeId);
322     dict.insertByteString("ID", idString.c_str());
323     IDTreeEntry idTreeEntry = {node->fNodeId, ref};
324     fIdTreeEntries.push_back(idTreeEntry);
325 
326     return doc->emit(dict, ref);
327 }
328 
addNodeAnnotation(int nodeId,SkPDFIndirectReference annotationRef,unsigned pageIndex)329 void SkPDFTagTree::addNodeAnnotation(int nodeId, SkPDFIndirectReference annotationRef, unsigned pageIndex) {
330     if (!fRoot) {
331         return;
332     }
333     SkPDFTagNode** tagPtr = fNodeMap.find(nodeId);
334     if (!tagPtr) {
335         return;
336     }
337     SkPDFTagNode* tag = *tagPtr;
338     SkASSERT(tag);
339 
340     SkPDFTagNode::AnnotationInfo annotationInfo = {pageIndex, annotationRef};
341     tag->fAnnotations.push_back(annotationInfo);
342 }
343 
addNodeTitle(int nodeId,SkSpan<const char> title)344 void SkPDFTagTree::addNodeTitle(int nodeId, SkSpan<const char> title) {
345     if (!fRoot) {
346         return;
347     }
348     SkPDFTagNode** tagPtr = fNodeMap.find(nodeId);
349     if (!tagPtr) {
350         return;
351     }
352     SkPDFTagNode* tag = *tagPtr;
353     SkASSERT(tag);
354 
355     if (tag->fWantTitle) {
356         tag->fTitle.append(title.data(), title.size());
357         // Arbitrary cutoff for size.
358         if (tag->fTitle.size() > 1023) {
359             tag->fWantTitle = false;
360         }
361     }
362 }
363 
makeStructTreeRoot(SkPDFDocument * doc)364 SkPDFIndirectReference SkPDFTagTree::makeStructTreeRoot(SkPDFDocument* doc) {
365     if (!fRoot || can_discard(fRoot)) {
366         return SkPDFIndirectReference();
367     }
368 
369     SkPDFIndirectReference ref = doc->reserveRef();
370 
371     unsigned pageCount = SkToUInt(doc->pageCount());
372 
373     // Build the StructTreeRoot.
374     SkPDFDict structTreeRoot("StructTreeRoot");
375     structTreeRoot.insertRef("K", PrepareTagTreeToEmit(ref, fRoot, doc));
376     structTreeRoot.insertInt("ParentTreeNextKey", SkToInt(pageCount));
377 
378     // Build the parent tree, which consists of two things:
379     // (1) For each page, a mapping from the marked content IDs on
380     // each page to their corresponding tags
381     // (2) For each annotation, an indirect reference to that
382     // annotation's struct tree element.
383     SkPDFDict parentTree("ParentTree");
384     auto parentTreeNums = SkPDFMakeArray();
385 
386     // First, one entry per page.
387     SkASSERT(SkToUInt(fMarksPerPage.size()) <= pageCount);
388     for (int j = 0; j < fMarksPerPage.size(); ++j) {
389         const TArray<SkPDFTagNode*>& pageMarks = fMarksPerPage[j];
390         SkPDFArray markToTagArray;
391         for (SkPDFTagNode* mark : pageMarks) {
392             SkASSERT(mark->fRef);
393             markToTagArray.appendRef(mark->fRef);
394         }
395         parentTreeNums->appendInt(j);
396         parentTreeNums->appendRef(doc->emit(markToTagArray));
397     }
398 
399     // Then, one entry per annotation.
400     for (size_t j = 0; j < fParentTreeAnnotationNodeIds.size(); ++j) {
401         int nodeId = fParentTreeAnnotationNodeIds[j];
402         int structParentKey = kFirstAnnotationStructParentKey + static_cast<int>(j);
403 
404         SkPDFTagNode** tagPtr = fNodeMap.find(nodeId);
405         if (!tagPtr) {
406             continue;
407         }
408         SkPDFTagNode* tag = *tagPtr;
409         parentTreeNums->appendInt(structParentKey);
410         parentTreeNums->appendRef(tag->fRef);
411     }
412 
413     parentTree.insertObject("Nums", std::move(parentTreeNums));
414     structTreeRoot.insertRef("ParentTree", doc->emit(parentTree));
415 
416     // Build the IDTree, a mapping from every unique ID string to
417     // a reference to its corresponding structure element node.
418     if (!fIdTreeEntries.empty()) {
419         std::sort(fIdTreeEntries.begin(), fIdTreeEntries.end(),
420                   [](const IDTreeEntry& a, const IDTreeEntry& b) {
421                     return a.nodeId < b.nodeId;
422                   });
423 
424         SkPDFDict idTree;
425         SkPDFDict idTreeLeaf;
426         auto limits = SkPDFMakeArray();
427         SkString lowestNodeIdString = SkPDFTagNode::nodeIdToString(
428             fIdTreeEntries.begin()->nodeId);
429         limits->appendByteString(lowestNodeIdString);
430         SkString highestNodeIdString = SkPDFTagNode::nodeIdToString(
431             fIdTreeEntries.rbegin()->nodeId);
432         limits->appendByteString(highestNodeIdString);
433         idTreeLeaf.insertObject("Limits", std::move(limits));
434         auto names = SkPDFMakeArray();
435         for (const IDTreeEntry& entry : fIdTreeEntries) {
436           SkString idString = SkPDFTagNode::nodeIdToString(entry.nodeId);
437             names->appendByteString(idString);
438             names->appendRef(entry.ref);
439         }
440         idTreeLeaf.insertObject("Names", std::move(names));
441         auto idTreeKids = SkPDFMakeArray();
442         idTreeKids->appendRef(doc->emit(idTreeLeaf));
443         idTree.insertObject("Kids", std::move(idTreeKids));
444         structTreeRoot.insertRef("IDTree", doc->emit(idTree));
445     }
446 
447     return doc->emit(structTreeRoot, ref);
448 }
449 
450 namespace {
451 struct OutlineEntry {
452     struct Content {
453         SkString fText;
454         Location fLocation;
accumulate__anon1a399b610311::OutlineEntry::Content455         void accumulate(Content const& child) {
456             fText += child.fText;
457             fLocation.accumulate(child.fLocation);
458         }
459     };
460 
461     Content fContent;
462     int fHeaderLevel;
463     SkPDFIndirectReference fRef;
464     SkPDFIndirectReference fStructureRef;
465     std::vector<OutlineEntry> fChildren = {};
466     size_t fDescendentsEmitted = 0;
467 
emitDescendents__anon1a399b610311::OutlineEntry468     void emitDescendents(SkPDFDocument* const doc) {
469         fDescendentsEmitted = fChildren.size();
470         for (size_t i = 0; i < fChildren.size(); ++i) {
471             auto&& child = fChildren[i];
472             child.emitDescendents(doc);
473             fDescendentsEmitted += child.fDescendentsEmitted;
474 
475             SkPDFDict entry;
476             entry.insertTextString("Title", child.fContent.fText);
477 
478             auto destination = SkPDFMakeArray();
479             destination->appendRef(doc->getPage(child.fContent.fLocation.fPageIndex));
480             destination->appendName("XYZ");
481             destination->appendScalar(child.fContent.fLocation.fPoint.x());
482             destination->appendScalar(child.fContent.fLocation.fPoint.y());
483             destination->appendInt(0);
484             entry.insertObject("Dest", std::move(destination));
485 
486             entry.insertRef("Parent", child.fRef);
487             if (child.fStructureRef) {
488                 entry.insertRef("SE", child.fStructureRef);
489             }
490             if (0 < i) {
491                 entry.insertRef("Prev", fChildren[i-1].fRef);
492             }
493             if (i < fChildren.size()-1) {
494                 entry.insertRef("Next", fChildren[i+1].fRef);
495             }
496             if (!child.fChildren.empty()) {
497                 entry.insertRef("First", child.fChildren.front().fRef);
498                 entry.insertRef("Last", child.fChildren.back().fRef);
499                 entry.insertInt("Count", child.fDescendentsEmitted);
500             }
501             doc->emit(entry, child.fRef);
502         }
503     }
504 };
505 
create_outline_entry_content(SkPDFTagNode * const node)506 OutlineEntry::Content create_outline_entry_content(SkPDFTagNode* const node) {
507     SkString text;
508     if (!node->fTitle.isEmpty()) {
509         text = node->fTitle;
510     } else if (!node->fAlt.isEmpty()) {
511         text = node->fAlt;
512     }
513 
514     // The uppermost/leftmost point on the earliest page of this node's marks.
515     Location markPoint;
516     for (auto&& mark : node->fMarkedContent) {
517         markPoint.accumulate(mark.fLocation);
518     }
519 
520     OutlineEntry::Content content{std::move(text), std::move(markPoint)};
521 
522     // Accumulate children
523     SkSpan<SkPDFTagNode> children(node->fChildren, node->fChildCount);
524     for (auto&& child : children) {
525         if (can_discard(&child)) {
526             continue;
527         }
528         content.accumulate(create_outline_entry_content(&child));
529     }
530     return content;
531 }
create_outline_from_headers(SkPDFDocument * const doc,SkPDFTagNode * const node,STArray<7,OutlineEntry * > & stack)532 void create_outline_from_headers(SkPDFDocument* const doc, SkPDFTagNode* const node,
533                                  STArray<7, OutlineEntry*>& stack) {
534     char const *type = node->fTypeString.c_str();
535     if (type[0] == 'H' && '1' <= type[1] && type[1] <= '6') {
536         int level = type[1] - '0';
537         while (level <= stack.back()->fHeaderLevel) {
538             stack.pop_back();
539         }
540         OutlineEntry::Content content = create_outline_entry_content(node);
541         if (!content.fText.isEmpty()) {
542             OutlineEntry e{std::move(content), level, doc->reserveRef(), node->fRef};
543             stack.push_back(&stack.back()->fChildren.emplace_back(std::move(e)));
544             return;
545         }
546     }
547 
548     SkSpan<SkPDFTagNode> children(node->fChildren, node->fChildCount);
549     for (auto&& child : children) {
550         if (can_discard(&child)) {
551             continue;
552         }
553         create_outline_from_headers(doc, &child, stack);
554     }
555 }
556 
557 } // namespace
558 
makeOutline(SkPDFDocument * doc)559 SkPDFIndirectReference SkPDFTagTree::makeOutline(SkPDFDocument* doc) {
560     if (!fRoot || can_discard(fRoot) ||
561         fOutline != SkPDF::Metadata::Outline::StructureElementHeaders)
562     {
563         return SkPDFIndirectReference();
564     }
565 
566     STArray<7, OutlineEntry*> stack;
567     OutlineEntry top{{SkString(), Location()}, 0, {}, {}};
568     stack.push_back(&top);
569     create_outline_from_headers(doc, fRoot, stack);
570     if (top.fChildren.empty()) {
571         return SkPDFIndirectReference();
572     }
573     top.emitDescendents(doc);
574     SkPDFIndirectReference outlineRef = doc->reserveRef();
575     SkPDFDict outline("Outlines");
576     outline.insertRef("First", top.fChildren.front().fRef);
577     outline.insertRef("Last", top.fChildren.back().fRef);
578     outline.insertInt("Count", top.fDescendentsEmitted);
579 
580     return doc->emit(outline, outlineRef);
581 }
582 
getRootLanguage()583 SkString SkPDFTagTree::getRootLanguage() {
584     return fRoot ? fRoot->fLang : SkString();
585 }
586