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