1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/pdf/renderer/pdf_accessibility_tree.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/i18n/break_iterator.h"
11 #include "base/memory/ptr_util.h"
12 #include "base/notreached.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/strings/utf_string_conversion_utils.h"
15 #include "components/pdf/renderer/pdf_ax_action_target.h"
16 #include "components/strings/grit/components_strings.h"
17 #include "content/public/renderer/pepper_plugin_instance.h"
18 #include "content/public/renderer/render_accessibility.h"
19 #include "content/public/renderer/render_frame.h"
20 #include "content/public/renderer/render_thread.h"
21 #include "content/public/renderer/render_view.h"
22 #include "content/public/renderer/renderer_ppapi_host.h"
23 #include "pdf/pdf_features.h"
24 #include "third_party/blink/public/strings/grit/blink_strings.h"
25 #include "ui/accessibility/ax_enums.mojom.h"
26 #include "ui/accessibility/null_ax_action_target.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/gfx/geometry/rect_conversions.h"
29 #include "ui/gfx/transform.h"
30
31 namespace pdf {
32
33 namespace {
34
35 // Don't try to apply font size thresholds to automatically identify headings
36 // if the median font size is not at least this many points.
37 const float kMinimumFontSize = 5.0f;
38
39 // Don't try to apply paragraph break thresholds to automatically identify
40 // paragraph breaks if the median line break is not at least this many points.
41 const float kMinimumLineSpacing = 5.0f;
42
43 // Ratio between the font size of one text run and the median on the page
44 // for that text run to be considered to be a heading instead of normal text.
45 const float kHeadingFontSizeRatio = 1.2f;
46
47 // Ratio between the line spacing between two lines and the median on the
48 // page for that line spacing to be considered a paragraph break.
49 const float kParagraphLineSpacingRatio = 1.2f;
50
PpFloatRectToGfxRectF(const PP_FloatRect & r)51 gfx::RectF PpFloatRectToGfxRectF(const PP_FloatRect& r) {
52 return gfx::RectF(r.point.x, r.point.y, r.size.width, r.size.height);
53 }
54
PPRectToGfxRectF(const PP_Rect & r)55 gfx::RectF PPRectToGfxRectF(const PP_Rect& r) {
56 return gfx::RectF(r.point.x, r.point.y, r.size.width, r.size.height);
57 }
58
PpPointToVector2dF(const PP_Point & p)59 gfx::Vector2dF PpPointToVector2dF(const PP_Point& p) {
60 return gfx::Vector2dF(p.x, p.y);
61 }
62
63 // This class is used as part of our heuristic to determine which text runs live
64 // on the same "line". As we process runs, we keep a weighted average of the
65 // top and bottom coordinates of the line, and if a new run falls within that
66 // range (within a threshold) it is considered part of the line.
67 class LineHelper {
68 public:
LineHelper(const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs)69 explicit LineHelper(
70 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs)
71 : text_runs_(text_runs) {
72 StartNewLine(0);
73 }
74
StartNewLine(size_t current_index)75 void StartNewLine(size_t current_index) {
76 DCHECK(current_index == 0 || current_index < text_runs_.size());
77 start_index_ = current_index;
78 accumulated_weight_top_ = 0.0f;
79 accumulated_weight_bottom_ = 0.0f;
80 accumulated_width_ = 0.0f;
81 }
82
ProcessNextRun(size_t run_index)83 void ProcessNextRun(size_t run_index) {
84 DCHECK_LT(run_index, text_runs_.size());
85 RemoveOldRunsUpTo(run_index);
86 AddRun(text_runs_[run_index].bounds);
87 }
88
IsRunOnSameLine(size_t run_index) const89 bool IsRunOnSameLine(size_t run_index) const {
90 DCHECK_LT(run_index, text_runs_.size());
91
92 // Calculate new top/bottom bounds for our line.
93 if (accumulated_width_ == 0.0f)
94 return false;
95
96 float line_top = accumulated_weight_top_ / accumulated_width_;
97 float line_bottom = accumulated_weight_bottom_ / accumulated_width_;
98
99 // Look at the next run, and determine how much it overlaps the line.
100 const auto& run_bounds = text_runs_[run_index].bounds;
101 if (run_bounds.size.height == 0.0f)
102 return false;
103
104 float clamped_top = std::max(line_top, run_bounds.point.y);
105 float clamped_bottom =
106 std::min(line_bottom, run_bounds.point.y + run_bounds.size.height);
107 if (clamped_bottom < clamped_top)
108 return false;
109
110 float coverage = (clamped_bottom - clamped_top) / (run_bounds.size.height);
111
112 // See if it falls within the line (within our threshold).
113 constexpr float kLineCoverageThreshold = 0.25f;
114 return coverage > kLineCoverageThreshold;
115 }
116
117 private:
AddRun(const PP_FloatRect & run_bounds)118 void AddRun(const PP_FloatRect& run_bounds) {
119 float run_width = fabsf(run_bounds.size.width);
120 accumulated_width_ += run_width;
121 accumulated_weight_top_ += run_bounds.point.y * run_width;
122 accumulated_weight_bottom_ +=
123 (run_bounds.point.y + run_bounds.size.height) * run_width;
124 }
125
RemoveRun(const PP_FloatRect & run_bounds)126 void RemoveRun(const PP_FloatRect& run_bounds) {
127 float run_width = fabsf(run_bounds.size.width);
128 accumulated_width_ -= run_width;
129 accumulated_weight_top_ -= run_bounds.point.y * run_width;
130 accumulated_weight_bottom_ -=
131 (run_bounds.point.y + run_bounds.size.height) * run_width;
132 }
133
RemoveOldRunsUpTo(size_t stop_index)134 void RemoveOldRunsUpTo(size_t stop_index) {
135 // Remove older runs from the weighted average if we've exceeded the
136 // threshold distance from them. We remove them to prevent e.g. drop-caps
137 // from unduly influencing future lines.
138 constexpr float kBoxRemoveWidthThreshold = 3.0f;
139 while (start_index_ < stop_index &&
140 accumulated_width_ > text_runs_[start_index_].bounds.size.width *
141 kBoxRemoveWidthThreshold) {
142 const auto& old_bounds = text_runs_[start_index_].bounds;
143 RemoveRun(old_bounds);
144 start_index_++;
145 }
146 }
147
148 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs_;
149 size_t start_index_;
150 float accumulated_weight_top_;
151 float accumulated_weight_bottom_;
152 float accumulated_width_;
153
154 DISALLOW_COPY_AND_ASSIGN(LineHelper);
155 };
156
ComputeParagraphAndHeadingThresholds(const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,float * out_heading_font_size_threshold,float * out_paragraph_spacing_threshold)157 void ComputeParagraphAndHeadingThresholds(
158 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
159 float* out_heading_font_size_threshold,
160 float* out_paragraph_spacing_threshold) {
161 // Scan over the font sizes and line spacing within this page and
162 // set heuristic thresholds so that text larger than the median font
163 // size can be marked as a heading, and spacing larger than the median
164 // line spacing can be a paragraph break.
165 std::vector<float> font_sizes;
166 std::vector<float> line_spacings;
167 for (size_t i = 0; i < text_runs.size(); ++i) {
168 font_sizes.push_back(text_runs[i].style.font_size);
169 if (i > 0) {
170 const auto& cur = text_runs[i].bounds;
171 const auto& prev = text_runs[i - 1].bounds;
172 if (cur.point.y > prev.point.y + prev.size.height / 2)
173 line_spacings.push_back(cur.point.y - prev.point.y);
174 }
175 }
176 if (font_sizes.size() > 2) {
177 std::sort(font_sizes.begin(), font_sizes.end());
178 float median_font_size = font_sizes[font_sizes.size() / 2];
179 if (median_font_size > kMinimumFontSize) {
180 *out_heading_font_size_threshold =
181 median_font_size * kHeadingFontSizeRatio;
182 }
183 }
184 if (line_spacings.size() > 4) {
185 std::sort(line_spacings.begin(), line_spacings.end());
186 float median_line_spacing = line_spacings[line_spacings.size() / 2];
187 if (median_line_spacing > kMinimumLineSpacing) {
188 *out_paragraph_spacing_threshold =
189 median_line_spacing * kParagraphLineSpacingRatio;
190 }
191 }
192 }
193
FinishStaticNode(ui::AXNodeData ** static_text_node,std::string * static_text)194 void FinishStaticNode(ui::AXNodeData** static_text_node,
195 std::string* static_text) {
196 // If we're in the middle of building a static text node, finish it before
197 // moving on to the next object.
198 if (*static_text_node) {
199 (*static_text_node)
200 ->AddStringAttribute(ax::mojom::StringAttribute::kName, (*static_text));
201 static_text->clear();
202 }
203 *static_text_node = nullptr;
204 }
205
ConnectPreviousAndNextOnLine(ui::AXNodeData * previous_on_line_node,ui::AXNodeData * next_on_line_node)206 void ConnectPreviousAndNextOnLine(ui::AXNodeData* previous_on_line_node,
207 ui::AXNodeData* next_on_line_node) {
208 previous_on_line_node->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
209 next_on_line_node->id);
210 next_on_line_node->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
211 previous_on_line_node->id);
212 }
213
BreakParagraph(const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,uint32_t text_run_index,float paragraph_spacing_threshold)214 bool BreakParagraph(
215 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
216 uint32_t text_run_index,
217 float paragraph_spacing_threshold) {
218 // Check to see if its also a new paragraph, i.e., if the distance between
219 // lines is greater than the threshold. If there's no threshold, that
220 // means there weren't enough lines to compute an accurate median, so
221 // we compare against the line size instead.
222 float line_spacing = fabsf(text_runs[text_run_index + 1].bounds.point.y -
223 text_runs[text_run_index].bounds.point.y);
224 return ((paragraph_spacing_threshold > 0 &&
225 line_spacing > paragraph_spacing_threshold) ||
226 (paragraph_spacing_threshold == 0 &&
227 line_spacing > kParagraphLineSpacingRatio *
228 text_runs[text_run_index].bounds.size.height));
229 }
230
GetStaticTextNodeFromNode(ui::AXNode * node)231 ui::AXNode* GetStaticTextNodeFromNode(ui::AXNode* node) {
232 // Returns the appropriate static text node given |node|'s type.
233 // Returns nullptr if there is no appropriate static text node.
234 if (!node)
235 return nullptr;
236 ui::AXNode* static_node = node;
237 // Get the static text from the link node.
238 if (node->data().role == ax::mojom::Role::kLink &&
239 node->children().size() == 1) {
240 static_node = node->children()[0];
241 }
242 // Get the static text from the highlight node.
243 if (node->data().role == ax::mojom::Role::kPdfActionableHighlight &&
244 !node->children().empty()) {
245 static_node = node->children()[0];
246 }
247 // If it's static text node, then it holds text.
248 if (static_node && static_node->data().role == ax::mojom::Role::kStaticText)
249 return static_node;
250 return nullptr;
251 }
252
GetTextRunCharsAsUTF8(const ppapi::PdfAccessibilityTextRunInfo & text_run,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,int char_index)253 std::string GetTextRunCharsAsUTF8(
254 const ppapi::PdfAccessibilityTextRunInfo& text_run,
255 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
256 int char_index) {
257 std::string chars_utf8;
258 for (uint32_t i = 0; i < text_run.len; ++i) {
259 base::WriteUnicodeCharacter(chars[char_index + i].unicode_character,
260 &chars_utf8);
261 }
262 return chars_utf8;
263 }
264
GetTextRunCharOffsets(const ppapi::PdfAccessibilityTextRunInfo & text_run,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,int char_index)265 std::vector<int32_t> GetTextRunCharOffsets(
266 const ppapi::PdfAccessibilityTextRunInfo& text_run,
267 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
268 int char_index) {
269 std::vector<int32_t> char_offsets(text_run.len);
270 double offset = 0.0;
271 for (uint32_t i = 0; i < text_run.len; ++i) {
272 offset += chars[char_index + i].char_width;
273 char_offsets[i] = floor(offset);
274 }
275 return char_offsets;
276 }
277
278 template <typename T>
CompareTextRuns(const T & a,const T & b)279 bool CompareTextRuns(const T& a, const T& b) {
280 return a.text_run_index < b.text_run_index;
281 }
282
283 template <typename T>
IsObjectInTextRun(const std::vector<T> & objects,uint32_t object_index,size_t text_run_index)284 bool IsObjectInTextRun(const std::vector<T>& objects,
285 uint32_t object_index,
286 size_t text_run_index) {
287 return (object_index < objects.size() &&
288 objects[object_index].text_run_index <= text_run_index);
289 }
290
NormalizeTextRunIndex(uint32_t object_end_text_run_index,size_t current_text_run_index)291 size_t NormalizeTextRunIndex(uint32_t object_end_text_run_index,
292 size_t current_text_run_index) {
293 return std::max<size_t>(
294 object_end_text_run_index,
295 current_text_run_index ? current_text_run_index - 1 : 0);
296 }
297
IsTextRenderModeFill(const PP_TextRenderingMode & mode)298 bool IsTextRenderModeFill(const PP_TextRenderingMode& mode) {
299 switch (mode) {
300 case PP_TEXTRENDERINGMODE_FILL:
301 case PP_TEXTRENDERINGMODE_FILLSTROKE:
302 case PP_TEXTRENDERINGMODE_FILLCLIP:
303 case PP_TEXTRENDERINGMODE_FILLSTROKECLIP:
304 return true;
305 default:
306 return false;
307 }
308 }
309
IsTextRenderModeStroke(const PP_TextRenderingMode & mode)310 bool IsTextRenderModeStroke(const PP_TextRenderingMode& mode) {
311 switch (mode) {
312 case PP_TEXTRENDERINGMODE_STROKE:
313 case PP_TEXTRENDERINGMODE_FILLSTROKE:
314 case PP_TEXTRENDERINGMODE_STROKECLIP:
315 case PP_TEXTRENDERINGMODE_FILLSTROKECLIP:
316 return true;
317 default:
318 return false;
319 }
320 }
321
CreateNode(ax::mojom::Role role,ax::mojom::Restriction restriction,content::RenderAccessibility * render_accessibility,std::vector<std::unique_ptr<ui::AXNodeData>> * nodes)322 ui::AXNodeData* CreateNode(
323 ax::mojom::Role role,
324 ax::mojom::Restriction restriction,
325 content::RenderAccessibility* render_accessibility,
326 std::vector<std::unique_ptr<ui::AXNodeData>>* nodes) {
327 DCHECK(render_accessibility);
328
329 auto node = std::make_unique<ui::AXNodeData>();
330 node->id = render_accessibility->GenerateAXID();
331 node->role = role;
332 node->SetRestriction(restriction);
333
334 // All nodes other than the first one have coordinates relative to
335 // the first node.
336 if (!nodes->empty())
337 node->relative_bounds.offset_container_id = (*nodes)[0]->id;
338
339 ui::AXNodeData* node_ptr = node.get();
340 nodes->push_back(std::move(node));
341
342 return node_ptr;
343 }
344
GetRoleForButtonType(PP_PrivateButtonType button_type)345 ax::mojom::Role GetRoleForButtonType(PP_PrivateButtonType button_type) {
346 switch (button_type) {
347 case PP_PrivateButtonType::PP_PRIVATEBUTTON_RADIOBUTTON:
348 return ax::mojom::Role::kRadioButton;
349 case PP_PrivateButtonType::PP_PRIVATEBUTTON_CHECKBOX:
350 return ax::mojom::Role::kCheckBox;
351 case PP_PrivateButtonType::PP_PRIVATEBUTTON_PUSHBUTTON:
352 return ax::mojom::Role::kButton;
353 default:
354 NOTREACHED();
355 return ax::mojom::Role::kNone;
356 }
357 }
358
359 class PdfAccessibilityTreeBuilder {
360 public:
PdfAccessibilityTreeBuilder(const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,const ppapi::PdfAccessibilityPageObjects & page_objects,const gfx::RectF & page_bounds,uint32_t page_index,ui::AXNodeData * page_node,content::RenderAccessibility * render_accessibility,std::vector<std::unique_ptr<ui::AXNodeData>> * nodes,std::map<int32_t,PP_PdfPageCharacterIndex> * node_id_to_page_char_index,std::map<int32_t,PdfAccessibilityTree::AnnotationInfo> * node_id_to_annotation_info)361 explicit PdfAccessibilityTreeBuilder(
362 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
363 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
364 const ppapi::PdfAccessibilityPageObjects& page_objects,
365 const gfx::RectF& page_bounds,
366 uint32_t page_index,
367 ui::AXNodeData* page_node,
368 content::RenderAccessibility* render_accessibility,
369 std::vector<std::unique_ptr<ui::AXNodeData>>* nodes,
370 std::map<int32_t, PP_PdfPageCharacterIndex>* node_id_to_page_char_index,
371 std::map<int32_t, PdfAccessibilityTree::AnnotationInfo>*
372 node_id_to_annotation_info)
373 : text_runs_(text_runs),
374 chars_(chars),
375 links_(page_objects.links),
376 images_(page_objects.images),
377 highlights_(page_objects.highlights),
378 text_fields_(page_objects.form_fields.text_fields),
379 buttons_(page_objects.form_fields.buttons),
380 choice_fields_(page_objects.form_fields.choice_fields),
381 page_bounds_(page_bounds),
382 page_index_(page_index),
383 page_node_(page_node),
384 render_accessibility_(render_accessibility),
385 nodes_(nodes),
386 node_id_to_page_char_index_(node_id_to_page_char_index),
387 node_id_to_annotation_info_(node_id_to_annotation_info) {
388 if (!text_runs.empty()) {
389 text_run_start_indices_.reserve(text_runs.size());
390 text_run_start_indices_.push_back(0);
391 for (size_t i = 0; i < text_runs.size() - 1; ++i) {
392 text_run_start_indices_.push_back(text_run_start_indices_[i] +
393 text_runs[i].len);
394 }
395 }
396 }
397
BuildPageTree()398 void BuildPageTree() {
399 ComputeParagraphAndHeadingThresholds(text_runs_,
400 &heading_font_size_threshold_,
401 ¶graph_spacing_threshold_);
402
403 ui::AXNodeData* para_node = nullptr;
404 ui::AXNodeData* static_text_node = nullptr;
405 ui::AXNodeData* previous_on_line_node = nullptr;
406 std::string static_text;
407 LineHelper line_helper(text_runs_);
408 bool pdf_forms_enabled =
409 base::FeatureList::IsEnabled(chrome_pdf::features::kAccessiblePDFForm);
410
411 for (size_t text_run_index = 0; text_run_index < text_runs_.size();
412 ++text_run_index) {
413 // If we don't have a paragraph, create one.
414 if (!para_node) {
415 para_node =
416 CreateParagraphNode(text_runs_[text_run_index].style.font_size);
417 page_node_->child_ids.push_back(para_node->id);
418 }
419
420 // If the |text_run_index| is less than or equal to the link's
421 // text_run_index, then push the link node in the paragraph.
422 if (IsObjectInTextRun(links_, current_link_index_, text_run_index)) {
423 FinishStaticNode(&static_text_node, &static_text);
424 const ppapi::PdfAccessibilityLinkInfo& link =
425 links_[current_link_index_++];
426 AddLinkToParaNode(link, para_node, &previous_on_line_node,
427 &text_run_index);
428
429 if (link.text_run_count == 0)
430 continue;
431
432 } else if (IsObjectInTextRun(images_, current_image_index_,
433 text_run_index)) {
434 FinishStaticNode(&static_text_node, &static_text);
435 AddImageToParaNode(images_[current_image_index_++], para_node,
436 &text_run_index);
437 continue;
438 } else if (IsObjectInTextRun(highlights_, current_highlight_index_,
439 text_run_index)) {
440 FinishStaticNode(&static_text_node, &static_text);
441 AddHighlightToParaNode(highlights_[current_highlight_index_++],
442 para_node, &previous_on_line_node,
443 &text_run_index);
444 } else if (IsObjectInTextRun(text_fields_, current_text_field_index_,
445 text_run_index) &&
446 pdf_forms_enabled) {
447 FinishStaticNode(&static_text_node, &static_text);
448 AddTextFieldToParaNode(text_fields_[current_text_field_index_++],
449 para_node, &text_run_index);
450 continue;
451 } else if (IsObjectInTextRun(buttons_, current_button_index_,
452 text_run_index) &&
453 pdf_forms_enabled) {
454 FinishStaticNode(&static_text_node, &static_text);
455 AddButtonToParaNode(buttons_[current_button_index_++], para_node,
456 &text_run_index);
457 continue;
458 } else if (IsObjectInTextRun(choice_fields_, current_choice_field_index_,
459 text_run_index) &&
460 pdf_forms_enabled) {
461 FinishStaticNode(&static_text_node, &static_text);
462 AddChoiceFieldToParaNode(choice_fields_[current_choice_field_index_++],
463 para_node, &text_run_index);
464 continue;
465 } else {
466 PP_PdfPageCharacterIndex page_char_index = {
467 page_index_, text_run_start_indices_[text_run_index]};
468
469 // This node is for the text inside the paragraph, it includes
470 // the text of all of the text runs.
471 if (!static_text_node) {
472 static_text_node = CreateStaticTextNode(page_char_index);
473 para_node->child_ids.push_back(static_text_node->id);
474 }
475
476 const ppapi::PdfAccessibilityTextRunInfo& text_run =
477 text_runs_[text_run_index];
478 // Add this text run to the current static text node.
479 ui::AXNodeData* inline_text_box_node =
480 CreateInlineTextBoxNode(text_run, page_char_index);
481 static_text_node->child_ids.push_back(inline_text_box_node->id);
482
483 static_text += inline_text_box_node->GetStringAttribute(
484 ax::mojom::StringAttribute::kName);
485
486 para_node->relative_bounds.bounds.Union(
487 inline_text_box_node->relative_bounds.bounds);
488 static_text_node->relative_bounds.bounds.Union(
489 inline_text_box_node->relative_bounds.bounds);
490
491 if (previous_on_line_node) {
492 ConnectPreviousAndNextOnLine(previous_on_line_node,
493 inline_text_box_node);
494 } else {
495 line_helper.StartNewLine(text_run_index);
496 }
497 line_helper.ProcessNextRun(text_run_index);
498
499 if (text_run_index < text_runs_.size() - 1) {
500 if (line_helper.IsRunOnSameLine(text_run_index + 1)) {
501 // The next run is on the same line.
502 previous_on_line_node = inline_text_box_node;
503 } else {
504 // The next run is on a new line.
505 previous_on_line_node = nullptr;
506 }
507 }
508 }
509
510 if (text_run_index == text_runs_.size() - 1) {
511 FinishStaticNode(&static_text_node, &static_text);
512 break;
513 }
514
515 if (!previous_on_line_node) {
516 if (BreakParagraph(text_runs_, text_run_index,
517 paragraph_spacing_threshold_)) {
518 FinishStaticNode(&static_text_node, &static_text);
519 para_node = nullptr;
520 }
521 }
522 }
523
524 AddRemainingAnnotations(para_node);
525 }
526
527 private:
AddWordStartsAndEnds(ui::AXNodeData * inline_text_box)528 void AddWordStartsAndEnds(ui::AXNodeData* inline_text_box) {
529 base::string16 text = inline_text_box->GetString16Attribute(
530 ax::mojom::StringAttribute::kName);
531 base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD);
532 if (!iter.Init())
533 return;
534
535 std::vector<int32_t> word_starts;
536 std::vector<int32_t> word_ends;
537 while (iter.Advance()) {
538 if (iter.IsWord()) {
539 word_starts.push_back(iter.prev());
540 word_ends.push_back(iter.pos());
541 }
542 }
543 inline_text_box->AddIntListAttribute(
544 ax::mojom::IntListAttribute::kWordStarts, word_starts);
545 inline_text_box->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
546 word_ends);
547 }
548
CreateParagraphNode(float font_size)549 ui::AXNodeData* CreateParagraphNode(float font_size) {
550 ui::AXNodeData* para_node = CreateNode(ax::mojom::Role::kParagraph,
551 ax::mojom::Restriction::kReadOnly,
552 render_accessibility_, nodes_);
553 para_node->AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
554 true);
555
556 // If font size exceeds the |heading_font_size_threshold_|, then classify
557 // it as a Heading.
558 if (heading_font_size_threshold_ > 0 &&
559 font_size > heading_font_size_threshold_) {
560 para_node->role = ax::mojom::Role::kHeading;
561 para_node->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
562 2);
563 para_node->AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h2");
564 }
565
566 return para_node;
567 }
568
CreateStaticTextNode(const PP_PdfPageCharacterIndex & page_char_index)569 ui::AXNodeData* CreateStaticTextNode(
570 const PP_PdfPageCharacterIndex& page_char_index) {
571 ui::AXNodeData* static_text_node = CreateNode(
572 ax::mojom::Role::kStaticText, ax::mojom::Restriction::kReadOnly,
573 render_accessibility_, nodes_);
574 static_text_node->SetNameFrom(ax::mojom::NameFrom::kContents);
575 node_id_to_page_char_index_->emplace(static_text_node->id, page_char_index);
576 return static_text_node;
577 }
578
CreateInlineTextBoxNode(const ppapi::PdfAccessibilityTextRunInfo & text_run,const PP_PdfPageCharacterIndex & page_char_index)579 ui::AXNodeData* CreateInlineTextBoxNode(
580 const ppapi::PdfAccessibilityTextRunInfo& text_run,
581 const PP_PdfPageCharacterIndex& page_char_index) {
582 ui::AXNodeData* inline_text_box_node = CreateNode(
583 ax::mojom::Role::kInlineTextBox, ax::mojom::Restriction::kReadOnly,
584 render_accessibility_, nodes_);
585 inline_text_box_node->SetNameFrom(ax::mojom::NameFrom::kContents);
586
587 std::string chars__utf8 =
588 GetTextRunCharsAsUTF8(text_run, chars_, page_char_index.char_index);
589 inline_text_box_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
590 chars__utf8);
591 inline_text_box_node->AddIntAttribute(
592 ax::mojom::IntAttribute::kTextDirection, text_run.direction);
593 inline_text_box_node->AddStringAttribute(
594 ax::mojom::StringAttribute::kFontFamily, text_run.style.font_name);
595 inline_text_box_node->AddFloatAttribute(
596 ax::mojom::FloatAttribute::kFontSize, text_run.style.font_size);
597 inline_text_box_node->AddFloatAttribute(
598 ax::mojom::FloatAttribute::kFontWeight, text_run.style.font_weight);
599 if (text_run.style.is_italic)
600 inline_text_box_node->AddTextStyle(ax::mojom::TextStyle::kItalic);
601 if (text_run.style.is_bold)
602 inline_text_box_node->AddTextStyle(ax::mojom::TextStyle::kBold);
603 if (IsTextRenderModeFill(text_run.style.render_mode)) {
604 inline_text_box_node->AddIntAttribute(ax::mojom::IntAttribute::kColor,
605 text_run.style.fill_color);
606 } else if (IsTextRenderModeStroke(text_run.style.render_mode)) {
607 inline_text_box_node->AddIntAttribute(ax::mojom::IntAttribute::kColor,
608 text_run.style.stroke_color);
609 }
610
611 inline_text_box_node->relative_bounds.bounds =
612 PpFloatRectToGfxRectF(text_run.bounds) +
613 page_bounds_.OffsetFromOrigin();
614 std::vector<int32_t> char_offsets =
615 GetTextRunCharOffsets(text_run, chars_, page_char_index.char_index);
616 inline_text_box_node->AddIntListAttribute(
617 ax::mojom::IntListAttribute::kCharacterOffsets, char_offsets);
618 AddWordStartsAndEnds(inline_text_box_node);
619 node_id_to_page_char_index_->emplace(inline_text_box_node->id,
620 page_char_index);
621 return inline_text_box_node;
622 }
623
CreateLinkNode(const ppapi::PdfAccessibilityLinkInfo & link)624 ui::AXNodeData* CreateLinkNode(const ppapi::PdfAccessibilityLinkInfo& link) {
625 ui::AXNodeData* link_node =
626 CreateNode(ax::mojom::Role::kLink, ax::mojom::Restriction::kReadOnly,
627 render_accessibility_, nodes_);
628
629 link_node->AddStringAttribute(ax::mojom::StringAttribute::kUrl, link.url);
630 link_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
631 std::string());
632 link_node->relative_bounds.bounds = PpFloatRectToGfxRectF(link.bounds);
633 node_id_to_annotation_info_->emplace(
634 link_node->id,
635 PdfAccessibilityTree::AnnotationInfo(page_index_, link.index_in_page));
636
637 return link_node;
638 }
639
CreateImageNode(const ppapi::PdfAccessibilityImageInfo & image)640 ui::AXNodeData* CreateImageNode(
641 const ppapi::PdfAccessibilityImageInfo& image) {
642 ui::AXNodeData* image_node =
643 CreateNode(ax::mojom::Role::kImage, ax::mojom::Restriction::kReadOnly,
644 render_accessibility_, nodes_);
645
646 if (image.alt_text.empty()) {
647 image_node->AddStringAttribute(
648 ax::mojom::StringAttribute::kName,
649 l10n_util::GetStringUTF8(IDS_AX_UNLABELED_IMAGE_ROLE_DESCRIPTION));
650 } else {
651 image_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
652 image.alt_text);
653 }
654 image_node->relative_bounds.bounds = PpFloatRectToGfxRectF(image.bounds);
655 return image_node;
656 }
657
CreateHighlightNode(const ppapi::PdfAccessibilityHighlightInfo & highlight)658 ui::AXNodeData* CreateHighlightNode(
659 const ppapi::PdfAccessibilityHighlightInfo& highlight) {
660 ui::AXNodeData* highlight_node = CreateNode(
661 ax::mojom::Role::kPdfActionableHighlight,
662 ax::mojom::Restriction::kReadOnly, render_accessibility_, nodes_);
663
664 highlight_node->AddStringAttribute(
665 ax::mojom::StringAttribute::kRoleDescription,
666 l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_HIGHLIGHT));
667 highlight_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
668 std::string());
669 highlight_node->relative_bounds.bounds =
670 PpFloatRectToGfxRectF(highlight.bounds);
671 highlight_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor,
672 highlight.color);
673
674 return highlight_node;
675 }
676
CreatePopupNoteNode(const ppapi::PdfAccessibilityHighlightInfo & highlight)677 ui::AXNodeData* CreatePopupNoteNode(
678 const ppapi::PdfAccessibilityHighlightInfo& highlight) {
679 ui::AXNodeData* popup_note_node =
680 CreateNode(ax::mojom::Role::kNote, ax::mojom::Restriction::kReadOnly,
681 render_accessibility_, nodes_);
682
683 popup_note_node->AddStringAttribute(
684 ax::mojom::StringAttribute::kRoleDescription,
685 l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_POPUP_NOTE));
686 popup_note_node->relative_bounds.bounds =
687 PpFloatRectToGfxRectF(highlight.bounds);
688
689 ui::AXNodeData* static_popup_note_text_node = CreateNode(
690 ax::mojom::Role::kStaticText, ax::mojom::Restriction::kReadOnly,
691 render_accessibility_, nodes_);
692
693 static_popup_note_text_node->SetNameFrom(ax::mojom::NameFrom::kContents);
694 static_popup_note_text_node->AddStringAttribute(
695 ax::mojom::StringAttribute::kName, highlight.note_text);
696 static_popup_note_text_node->relative_bounds.bounds =
697 PpFloatRectToGfxRectF(highlight.bounds);
698
699 popup_note_node->child_ids.push_back(static_popup_note_text_node->id);
700
701 return popup_note_node;
702 }
703
CreateTextFieldNode(const ppapi::PdfAccessibilityTextFieldInfo & text_field)704 ui::AXNodeData* CreateTextFieldNode(
705 const ppapi::PdfAccessibilityTextFieldInfo& text_field) {
706 ax::mojom::Restriction restriction = text_field.is_read_only
707 ? ax::mojom::Restriction::kReadOnly
708 : ax::mojom::Restriction::kNone;
709 ui::AXNodeData* text_field_node =
710 CreateNode(ax::mojom::Role::kTextField, restriction,
711 render_accessibility_, nodes_);
712
713 text_field_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
714 text_field.name);
715 text_field_node->AddStringAttribute(ax::mojom::StringAttribute::kValue,
716 text_field.value);
717 text_field_node->AddState(ax::mojom::State::kFocusable);
718 if (text_field.is_required)
719 text_field_node->AddState(ax::mojom::State::kRequired);
720 if (text_field.is_password)
721 text_field_node->AddState(ax::mojom::State::kProtected);
722 text_field_node->relative_bounds.bounds =
723 PpFloatRectToGfxRectF(text_field.bounds);
724 return text_field_node;
725 }
726
CreateButtonNode(const ppapi::PdfAccessibilityButtonInfo & button)727 ui::AXNodeData* CreateButtonNode(
728 const ppapi::PdfAccessibilityButtonInfo& button) {
729 ax::mojom::Restriction restriction = button.is_read_only
730 ? ax::mojom::Restriction::kReadOnly
731 : ax::mojom::Restriction::kNone;
732 ui::AXNodeData* button_node =
733 CreateNode(GetRoleForButtonType(button.type), restriction,
734 render_accessibility_, nodes_);
735 button_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
736 button.name);
737 button_node->AddState(ax::mojom::State::kFocusable);
738
739 if (button.type == PP_PRIVATEBUTTON_RADIOBUTTON ||
740 button.type == PP_PRIVATEBUTTON_CHECKBOX) {
741 ax::mojom::CheckedState checkedState =
742 button.is_checked ? ax::mojom::CheckedState::kTrue
743 : ax::mojom::CheckedState::kNone;
744 button_node->SetCheckedState(checkedState);
745 button_node->AddStringAttribute(ax::mojom::StringAttribute::kValue,
746 button.value);
747 button_node->AddIntAttribute(ax::mojom::IntAttribute::kSetSize,
748 button.control_count);
749 button_node->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet,
750 button.control_index + 1);
751 }
752
753 button_node->relative_bounds.bounds = PpFloatRectToGfxRectF(button.bounds);
754 return button_node;
755 }
756
CreateListboxOptionNode(const ppapi::PdfAccessibilityChoiceFieldOptionInfo & choice_field_option,ax::mojom::Restriction restriction)757 ui::AXNodeData* CreateListboxOptionNode(
758 const ppapi::PdfAccessibilityChoiceFieldOptionInfo& choice_field_option,
759 ax::mojom::Restriction restriction) {
760 ui::AXNodeData* listbox_option_node =
761 CreateNode(ax::mojom::Role::kListBoxOption, restriction,
762 render_accessibility_, nodes_);
763
764 listbox_option_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
765 choice_field_option.name);
766 listbox_option_node->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected,
767 choice_field_option.is_selected);
768 listbox_option_node->AddState(ax::mojom::State::kFocusable);
769 return listbox_option_node;
770 }
771
CreateListboxNode(const ppapi::PdfAccessibilityChoiceFieldInfo & choice_field,ui::AXNodeData * control_node)772 ui::AXNodeData* CreateListboxNode(
773 const ppapi::PdfAccessibilityChoiceFieldInfo& choice_field,
774 ui::AXNodeData* control_node) {
775 ax::mojom::Restriction restriction = choice_field.is_read_only
776 ? ax::mojom::Restriction::kReadOnly
777 : ax::mojom::Restriction::kNone;
778 ui::AXNodeData* listbox_node = CreateNode(
779 ax::mojom::Role::kListBox, restriction, render_accessibility_, nodes_);
780
781 if (choice_field.type != PP_PRIVATECHOICEFIELD_COMBOBOX) {
782 listbox_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
783 choice_field.name);
784 }
785
786 ui::AXNodeData* first_selected_option = nullptr;
787 for (const ppapi::PdfAccessibilityChoiceFieldOptionInfo& option :
788 choice_field.options) {
789 ui::AXNodeData* listbox_option_node =
790 CreateListboxOptionNode(option, restriction);
791 if (!first_selected_option && listbox_option_node->GetBoolAttribute(
792 ax::mojom::BoolAttribute::kSelected)) {
793 first_selected_option = listbox_option_node;
794 }
795 // TODO(bug 1030242): add |listbox_option_node| specific bounds here.
796 listbox_option_node->relative_bounds.bounds =
797 PpFloatRectToGfxRectF(choice_field.bounds);
798 listbox_node->child_ids.push_back(listbox_option_node->id);
799 }
800
801 if (control_node && first_selected_option) {
802 control_node->AddIntAttribute(
803 ax::mojom::IntAttribute::kActivedescendantId,
804 first_selected_option->id);
805 }
806
807 if (choice_field.is_multi_select)
808 listbox_node->AddState(ax::mojom::State::kMultiselectable);
809 listbox_node->AddState(ax::mojom::State::kFocusable);
810 listbox_node->relative_bounds.bounds =
811 PpFloatRectToGfxRectF(choice_field.bounds);
812 return listbox_node;
813 }
814
CreateComboboxInputNode(const ppapi::PdfAccessibilityChoiceFieldInfo & choice_field,ax::mojom::Restriction restriction)815 ui::AXNodeData* CreateComboboxInputNode(
816 const ppapi::PdfAccessibilityChoiceFieldInfo& choice_field,
817 ax::mojom::Restriction restriction) {
818 ax::mojom::Role input_role = choice_field.has_editable_text_box
819 ? ax::mojom::Role::kTextFieldWithComboBox
820 : ax::mojom::Role::kComboBoxMenuButton;
821 ui::AXNodeData* combobox_input_node =
822 CreateNode(input_role, restriction, render_accessibility_, nodes_);
823 combobox_input_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
824 choice_field.name);
825 for (const ppapi::PdfAccessibilityChoiceFieldOptionInfo& option :
826 choice_field.options) {
827 if (option.is_selected) {
828 combobox_input_node->AddStringAttribute(
829 ax::mojom::StringAttribute::kValue, option.name);
830 break;
831 }
832 }
833
834 combobox_input_node->AddState(ax::mojom::State::kFocusable);
835 combobox_input_node->relative_bounds.bounds =
836 PpFloatRectToGfxRectF(choice_field.bounds);
837 return combobox_input_node;
838 }
839
CreateComboboxNode(const ppapi::PdfAccessibilityChoiceFieldInfo & choice_field)840 ui::AXNodeData* CreateComboboxNode(
841 const ppapi::PdfAccessibilityChoiceFieldInfo& choice_field) {
842 ax::mojom::Restriction restriction = choice_field.is_read_only
843 ? ax::mojom::Restriction::kReadOnly
844 : ax::mojom::Restriction::kNone;
845 ui::AXNodeData* combobox_node =
846 CreateNode(ax::mojom::Role::kComboBoxGrouping, restriction,
847 render_accessibility_, nodes_);
848 ui::AXNodeData* input_element =
849 CreateComboboxInputNode(choice_field, restriction);
850 ui::AXNodeData* list_element =
851 CreateListboxNode(choice_field, input_element);
852 input_element->AddIntListAttribute(
853 ax::mojom::IntListAttribute::kControlsIds,
854 std::vector<int32_t>{list_element->id});
855 combobox_node->child_ids.push_back(input_element->id);
856 combobox_node->child_ids.push_back(list_element->id);
857 combobox_node->AddState(ax::mojom::State::kFocusable);
858 combobox_node->relative_bounds.bounds =
859 PpFloatRectToGfxRectF(choice_field.bounds);
860 return combobox_node;
861 }
862
CreateChoiceFieldNode(const ppapi::PdfAccessibilityChoiceFieldInfo & choice_field)863 ui::AXNodeData* CreateChoiceFieldNode(
864 const ppapi::PdfAccessibilityChoiceFieldInfo& choice_field) {
865 if (choice_field.type == PP_PRIVATECHOICEFIELD_LISTBOX)
866 return CreateListboxNode(choice_field, nullptr);
867
868 return CreateComboboxNode(choice_field);
869 }
870
AddTextToAXNode(uint32_t start_text_run_index,uint32_t end_text_run_index,ui::AXNodeData * ax_node,ui::AXNodeData ** previous_on_line_node)871 void AddTextToAXNode(uint32_t start_text_run_index,
872 uint32_t end_text_run_index,
873 ui::AXNodeData* ax_node,
874 ui::AXNodeData** previous_on_line_node) {
875 PP_PdfPageCharacterIndex page_char_index = {
876 page_index_, text_run_start_indices_[start_text_run_index]};
877 ui::AXNodeData* ax_static_text_node = CreateStaticTextNode(page_char_index);
878 ax_node->child_ids.push_back(ax_static_text_node->id);
879 // Accumulate the text of the node.
880 std::string ax_name;
881 LineHelper line_helper(text_runs_);
882
883 for (size_t text_run_index = start_text_run_index;
884 text_run_index <= end_text_run_index; ++text_run_index) {
885 const ppapi::PdfAccessibilityTextRunInfo& text_run =
886 text_runs_[text_run_index];
887 page_char_index.char_index = text_run_start_indices_[text_run_index];
888 // Add this text run to the current static text node.
889 ui::AXNodeData* inline_text_box_node =
890 CreateInlineTextBoxNode(text_run, page_char_index);
891 ax_static_text_node->child_ids.push_back(inline_text_box_node->id);
892
893 ax_static_text_node->relative_bounds.bounds.Union(
894 inline_text_box_node->relative_bounds.bounds);
895 ax_name += inline_text_box_node->GetStringAttribute(
896 ax::mojom::StringAttribute::kName);
897
898 if (*previous_on_line_node) {
899 ConnectPreviousAndNextOnLine(*previous_on_line_node,
900 inline_text_box_node);
901 } else {
902 line_helper.StartNewLine(text_run_index);
903 }
904 line_helper.ProcessNextRun(text_run_index);
905
906 if (text_run_index < text_runs_.size() - 1) {
907 if (line_helper.IsRunOnSameLine(text_run_index + 1)) {
908 // The next run is on the same line.
909 *previous_on_line_node = inline_text_box_node;
910 } else {
911 // The next run is on a new line.
912 *previous_on_line_node = nullptr;
913 }
914 }
915 }
916
917 ax_node->AddStringAttribute(ax::mojom::StringAttribute::kName, ax_name);
918 ax_static_text_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
919 ax_name);
920 }
921
AddTextToObjectNode(uint32_t object_text_run_index,uint32_t object_text_run_count,ui::AXNodeData * object_node,ui::AXNodeData * para_node,ui::AXNodeData ** previous_on_line_node,size_t * text_run_index)922 void AddTextToObjectNode(uint32_t object_text_run_index,
923 uint32_t object_text_run_count,
924 ui::AXNodeData* object_node,
925 ui::AXNodeData* para_node,
926 ui::AXNodeData** previous_on_line_node,
927 size_t* text_run_index) {
928 // Annotation objects can overlap in PDF. There can be two overlapping
929 // scenarios: Partial overlap and Complete overlap.
930 // Partial overlap
931 //
932 // Link A starts Link B starts Link A ends Link B ends
933 // |a1 |b1 |a2 |b2
934 // -----------------------------------------------------------------------
935 // Text
936 //
937 // Complete overlap
938 // Link A starts Link B starts Link B ends Link A ends
939 // |a1 |b1 |b2 |a2
940 // -----------------------------------------------------------------------
941 // Text
942 //
943 // For overlapping annotations, both annotations would store the full
944 // text data and nothing will get truncated. For partial overlap, link `A`
945 // would contain text between a1 and a2 while link `B` would contain text
946 // between b1 and b2. For complete overlap as well, link `A` would contain
947 // text between a1 and a2 and link `B` would contain text between b1 and
948 // b2. The links would appear in the tree in the order of which they are
949 // present. In the tree for both overlapping scenarios, link `A` would
950 // appear first in the tree and link `B` after it.
951
952 // If |object_text_run_count| > 0, then the object is part of the page text.
953 // Make the text runs contained by the object children of the object node.
954 size_t end_text_run_index = object_text_run_index + object_text_run_count;
955 uint32_t object_end_text_run_index =
956 std::min(end_text_run_index, text_runs_.size()) - 1;
957 AddTextToAXNode(object_text_run_index, object_end_text_run_index,
958 object_node, previous_on_line_node);
959
960 para_node->relative_bounds.bounds.Union(
961 object_node->relative_bounds.bounds);
962
963 *text_run_index =
964 NormalizeTextRunIndex(object_end_text_run_index, *text_run_index);
965 }
966
AddLinkToParaNode(const ppapi::PdfAccessibilityLinkInfo & link,ui::AXNodeData * para_node,ui::AXNodeData ** previous_on_line_node,size_t * text_run_index)967 void AddLinkToParaNode(const ppapi::PdfAccessibilityLinkInfo& link,
968 ui::AXNodeData* para_node,
969 ui::AXNodeData** previous_on_line_node,
970 size_t* text_run_index) {
971 ui::AXNodeData* link_node = CreateLinkNode(link);
972 para_node->child_ids.push_back(link_node->id);
973
974 // If |link.text_run_count| == 0, then the link is not part of the page
975 // text. Push it ahead of the current text run.
976 if (link.text_run_count == 0) {
977 --(*text_run_index);
978 return;
979 }
980
981 // Make the text runs contained by the link children of
982 // the link node.
983 AddTextToObjectNode(link.text_run_index, link.text_run_count, link_node,
984 para_node, previous_on_line_node, text_run_index);
985 }
986
AddImageToParaNode(const ppapi::PdfAccessibilityImageInfo & image,ui::AXNodeData * para_node,size_t * text_run_index)987 void AddImageToParaNode(const ppapi::PdfAccessibilityImageInfo& image,
988 ui::AXNodeData* para_node,
989 size_t* text_run_index) {
990 // If the |text_run_index| is less than or equal to the image's text run
991 // index, then push the image ahead of the current text run.
992 ui::AXNodeData* image_node = CreateImageNode(image);
993 para_node->child_ids.push_back(image_node->id);
994 --(*text_run_index);
995 }
996
AddHighlightToParaNode(const ppapi::PdfAccessibilityHighlightInfo & highlight,ui::AXNodeData * para_node,ui::AXNodeData ** previous_on_line_node,size_t * text_run_index)997 void AddHighlightToParaNode(
998 const ppapi::PdfAccessibilityHighlightInfo& highlight,
999 ui::AXNodeData* para_node,
1000 ui::AXNodeData** previous_on_line_node,
1001 size_t* text_run_index) {
1002 ui::AXNodeData* highlight_node = CreateHighlightNode(highlight);
1003 para_node->child_ids.push_back(highlight_node->id);
1004
1005 // Make the text runs contained by the highlight children of
1006 // the highlight node.
1007 AddTextToObjectNode(highlight.text_run_index, highlight.text_run_count,
1008 highlight_node, para_node, previous_on_line_node,
1009 text_run_index);
1010
1011 if (!highlight.note_text.empty()) {
1012 ui::AXNodeData* popup_note_node = CreatePopupNoteNode(highlight);
1013 highlight_node->child_ids.push_back(popup_note_node->id);
1014 }
1015 }
1016
AddTextFieldToParaNode(const ppapi::PdfAccessibilityTextFieldInfo & text_field,ui::AXNodeData * para_node,size_t * text_run_index)1017 void AddTextFieldToParaNode(
1018 const ppapi::PdfAccessibilityTextFieldInfo& text_field,
1019 ui::AXNodeData* para_node,
1020 size_t* text_run_index) {
1021 // If the |text_run_index| is less than or equal to the text_field's text
1022 // run index, then push the text_field ahead of the current text run.
1023 ui::AXNodeData* text_field_node = CreateTextFieldNode(text_field);
1024 para_node->child_ids.push_back(text_field_node->id);
1025 --(*text_run_index);
1026 }
1027
AddButtonToParaNode(const ppapi::PdfAccessibilityButtonInfo & button,ui::AXNodeData * para_node,size_t * text_run_index)1028 void AddButtonToParaNode(const ppapi::PdfAccessibilityButtonInfo& button,
1029 ui::AXNodeData* para_node,
1030 size_t* text_run_index) {
1031 // If the |text_run_index| is less than or equal to the button's text
1032 // run index, then push the button ahead of the current text run.
1033 ui::AXNodeData* button_node = CreateButtonNode(button);
1034 para_node->child_ids.push_back(button_node->id);
1035 --(*text_run_index);
1036 }
1037
AddChoiceFieldToParaNode(const ppapi::PdfAccessibilityChoiceFieldInfo & choice_field,ui::AXNodeData * para_node,size_t * text_run_index)1038 void AddChoiceFieldToParaNode(
1039 const ppapi::PdfAccessibilityChoiceFieldInfo& choice_field,
1040 ui::AXNodeData* para_node,
1041 size_t* text_run_index) {
1042 // If the |text_run_index| is less than or equal to the choice_field's text
1043 // run index, then push the choice_field ahead of the current text run.
1044 ui::AXNodeData* choice_field_node = CreateChoiceFieldNode(choice_field);
1045 para_node->child_ids.push_back(choice_field_node->id);
1046 --(*text_run_index);
1047 }
1048
AddRemainingAnnotations(ui::AXNodeData * para_node)1049 void AddRemainingAnnotations(ui::AXNodeData* para_node) {
1050 // If we don't have additional links, images or form fields to insert in the
1051 // tree, then return.
1052 if (current_link_index_ >= links_.size() &&
1053 current_image_index_ >= images_.size() &&
1054 current_text_field_index_ >= text_fields_.size() &&
1055 current_button_index_ >= buttons_.size() &&
1056 current_choice_field_index_ >= choice_fields_.size()) {
1057 return;
1058 }
1059
1060 // If we don't have a paragraph node, create a new one.
1061 if (!para_node) {
1062 para_node = CreateNode(ax::mojom::Role::kParagraph,
1063 ax::mojom::Restriction::kReadOnly,
1064 render_accessibility_, nodes_);
1065 page_node_->child_ids.push_back(para_node->id);
1066 }
1067 // Push all the links not anchored to any text run to the last paragraph.
1068 for (size_t i = current_link_index_; i < links_.size(); i++) {
1069 ui::AXNodeData* link_node = CreateLinkNode(links_[i]);
1070 para_node->child_ids.push_back(link_node->id);
1071 }
1072 // Push all the images not anchored to any text run to the last paragraph.
1073 for (size_t i = current_image_index_; i < images_.size(); i++) {
1074 ui::AXNodeData* image_node = CreateImageNode(images_[i]);
1075 para_node->child_ids.push_back(image_node->id);
1076 }
1077
1078 if (base::FeatureList::IsEnabled(
1079 chrome_pdf::features::kAccessiblePDFForm)) {
1080 // Push all the text fields not anchored to any text run to the last
1081 // paragraph.
1082 for (size_t i = current_text_field_index_; i < text_fields_.size(); i++) {
1083 ui::AXNodeData* text_field_node = CreateTextFieldNode(text_fields_[i]);
1084 para_node->child_ids.push_back(text_field_node->id);
1085 }
1086
1087 // Push all the buttons not anchored to any text run to the last
1088 // paragraph.
1089 for (size_t i = current_button_index_; i < buttons_.size(); i++) {
1090 ui::AXNodeData* button_node = CreateButtonNode(buttons_[i]);
1091 para_node->child_ids.push_back(button_node->id);
1092 }
1093
1094 // Push all the choice fields not anchored to any text run to the last
1095 // paragraph.
1096 for (size_t i = current_choice_field_index_; i < choice_fields_.size();
1097 i++) {
1098 ui::AXNodeData* choice_field_node =
1099 CreateChoiceFieldNode(choice_fields_[i]);
1100 para_node->child_ids.push_back(choice_field_node->id);
1101 }
1102 }
1103 }
1104
1105 std::vector<uint32_t> text_run_start_indices_;
1106 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs_;
1107 const std::vector<PP_PrivateAccessibilityCharInfo>& chars_;
1108 const std::vector<ppapi::PdfAccessibilityLinkInfo>& links_;
1109 uint32_t current_link_index_ = 0;
1110 const std::vector<ppapi::PdfAccessibilityImageInfo>& images_;
1111 uint32_t current_image_index_ = 0;
1112 const std::vector<ppapi::PdfAccessibilityHighlightInfo>& highlights_;
1113 uint32_t current_highlight_index_ = 0;
1114 const std::vector<ppapi::PdfAccessibilityTextFieldInfo>& text_fields_;
1115 uint32_t current_text_field_index_ = 0;
1116 const std::vector<ppapi::PdfAccessibilityButtonInfo>& buttons_;
1117 uint32_t current_button_index_ = 0;
1118 const std::vector<ppapi::PdfAccessibilityChoiceFieldInfo>& choice_fields_;
1119 uint32_t current_choice_field_index_ = 0;
1120 const gfx::RectF& page_bounds_;
1121 uint32_t page_index_;
1122 ui::AXNodeData* page_node_;
1123 content::RenderAccessibility* render_accessibility_;
1124 std::vector<std::unique_ptr<ui::AXNodeData>>* nodes_;
1125 std::map<int32_t, PP_PdfPageCharacterIndex>* node_id_to_page_char_index_;
1126 std::map<int32_t, PdfAccessibilityTree::AnnotationInfo>*
1127 node_id_to_annotation_info_;
1128 float heading_font_size_threshold_ = 0;
1129 float paragraph_spacing_threshold_ = 0;
1130 };
1131
1132 } // namespace
1133
PdfAccessibilityTree(content::RendererPpapiHost * host,PP_Instance instance)1134 PdfAccessibilityTree::PdfAccessibilityTree(content::RendererPpapiHost* host,
1135 PP_Instance instance)
1136 : host_(host), instance_(instance) {}
1137
~PdfAccessibilityTree()1138 PdfAccessibilityTree::~PdfAccessibilityTree() {
1139 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1140 if (render_accessibility)
1141 render_accessibility->SetPluginTreeSource(nullptr);
1142 }
1143
1144 // static
IsDataFromPluginValid(const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,const ppapi::PdfAccessibilityPageObjects & page_objects)1145 bool PdfAccessibilityTree::IsDataFromPluginValid(
1146 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
1147 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
1148 const ppapi::PdfAccessibilityPageObjects& page_objects) {
1149 base::CheckedNumeric<uint32_t> char_length = 0;
1150 for (const ppapi::PdfAccessibilityTextRunInfo& text_run : text_runs)
1151 char_length += text_run.len;
1152
1153 if (!char_length.IsValid() || char_length.ValueOrDie() != chars.size())
1154 return false;
1155
1156 const std::vector<ppapi::PdfAccessibilityLinkInfo>& links =
1157 page_objects.links;
1158 if (!std::is_sorted(links.begin(), links.end(),
1159 CompareTextRuns<ppapi::PdfAccessibilityLinkInfo>)) {
1160 return false;
1161 }
1162 // Text run index of a |link| is out of bounds if it exceeds the size of
1163 // |text_runs|. The index denotes the position of the link relative to the
1164 // text runs. The index value equal to the size of |text_runs| indicates that
1165 // the link should be after the last text run.
1166 // |index_in_page| of every |link| should be with in the range of total number
1167 // of links, which is size of |links|.
1168 for (const ppapi::PdfAccessibilityLinkInfo& link : links) {
1169 base::CheckedNumeric<uint32_t> index = link.text_run_index;
1170 index += link.text_run_count;
1171 if (!index.IsValid() || index.ValueOrDie() > text_runs.size() ||
1172 link.index_in_page >= links.size()) {
1173 return false;
1174 }
1175 }
1176
1177 const std::vector<ppapi::PdfAccessibilityImageInfo>& images =
1178 page_objects.images;
1179 if (!std::is_sorted(images.begin(), images.end(),
1180 CompareTextRuns<ppapi::PdfAccessibilityImageInfo>)) {
1181 return false;
1182 }
1183 // Text run index of an |image| works on the same logic as the text run index
1184 // of a |link| as mentioned above.
1185 for (const ppapi::PdfAccessibilityImageInfo& image : images) {
1186 if (image.text_run_index > text_runs.size())
1187 return false;
1188 }
1189
1190 const std::vector<ppapi::PdfAccessibilityHighlightInfo>& highlights =
1191 page_objects.highlights;
1192 if (!std::is_sorted(highlights.begin(), highlights.end(),
1193 CompareTextRuns<ppapi::PdfAccessibilityHighlightInfo>)) {
1194 return false;
1195 }
1196
1197 // Since highlights also span across text runs similar to links, the
1198 // validation method is the same.
1199 // |index_in_page| of a |highlight| follows the same index validation rules
1200 // as of links.
1201 for (const auto& highlight : highlights) {
1202 base::CheckedNumeric<uint32_t> index = highlight.text_run_index;
1203 index += highlight.text_run_count;
1204 if (!index.IsValid() || index.ValueOrDie() > text_runs.size() ||
1205 highlight.index_in_page >= highlights.size()) {
1206 return false;
1207 }
1208 }
1209
1210 const std::vector<ppapi::PdfAccessibilityTextFieldInfo>& text_fields =
1211 page_objects.form_fields.text_fields;
1212 if (!std::is_sorted(text_fields.begin(), text_fields.end(),
1213 CompareTextRuns<ppapi::PdfAccessibilityTextFieldInfo>)) {
1214 return false;
1215 }
1216 // Text run index of an |text_field| works on the same logic as the text run
1217 // index of a |link| as mentioned above.
1218 // |index_in_page| of a |text_field| follows the same index validation rules
1219 // as of links.
1220 for (const ppapi::PdfAccessibilityTextFieldInfo& text_field : text_fields) {
1221 if (text_field.text_run_index > text_runs.size() ||
1222 text_field.index_in_page >= text_fields.size()) {
1223 return false;
1224 }
1225 }
1226
1227 const std::vector<ppapi::PdfAccessibilityChoiceFieldInfo>& choice_fields =
1228 page_objects.form_fields.choice_fields;
1229 if (!std::is_sorted(
1230 choice_fields.begin(), choice_fields.end(),
1231 CompareTextRuns<ppapi::PdfAccessibilityChoiceFieldInfo>)) {
1232 return false;
1233 }
1234 // Text run index of an |choice_field| works on the same logic as the text run
1235 // index of a |link| as mentioned above.
1236 // |index_in_page| of a |choice_field| follows the same index validation rules
1237 // as of links.
1238 for (const auto& choice_field : choice_fields) {
1239 if (choice_field.text_run_index > text_runs.size() ||
1240 choice_field.index_in_page >= choice_fields.size()) {
1241 return false;
1242 }
1243 }
1244
1245 const std::vector<ppapi::PdfAccessibilityButtonInfo>& buttons =
1246 page_objects.form_fields.buttons;
1247 if (!std::is_sorted(
1248 buttons.begin(), buttons.end(),
1249 CompareTextRuns<ppapi::PdfAccessibilityButtonInfo>)) {
1250 return false;
1251 }
1252 for (const ppapi::PdfAccessibilityButtonInfo& button :
1253 buttons) {
1254 // Text run index of an |button| works on the same logic as the text run
1255 // index of a |link| as mentioned above.
1256 // |index_in_page| of a |button| follows the same index validation rules as
1257 // of links.
1258 if (button.text_run_index > text_runs.size() ||
1259 button.index_in_page >= buttons.size()) {
1260 return false;
1261 }
1262 // For radio button or checkbox, value of |button.control_index| should
1263 // always be less than |button.control_count|.
1264 if ((button.type == PP_PrivateButtonType::PP_PRIVATEBUTTON_CHECKBOX ||
1265 button.type == PP_PrivateButtonType::PP_PRIVATEBUTTON_RADIOBUTTON) &&
1266 (button.control_index >= button.control_count)) {
1267 return false;
1268 }
1269 }
1270
1271 return true;
1272 }
1273
SetAccessibilityViewportInfo(const PP_PrivateAccessibilityViewportInfo & viewport_info)1274 void PdfAccessibilityTree::SetAccessibilityViewportInfo(
1275 const PP_PrivateAccessibilityViewportInfo& viewport_info) {
1276 zoom_ = viewport_info.zoom;
1277 scale_ = viewport_info.scale;
1278 CHECK_GT(zoom_, 0);
1279 CHECK_GT(scale_, 0);
1280 scroll_ = PpPointToVector2dF(viewport_info.scroll);
1281 offset_ = PpPointToVector2dF(viewport_info.offset);
1282
1283 selection_start_page_index_ = viewport_info.selection_start_page_index;
1284 selection_start_char_index_ = viewport_info.selection_start_char_index;
1285 selection_end_page_index_ = viewport_info.selection_end_page_index;
1286 selection_end_char_index_ = viewport_info.selection_end_char_index;
1287
1288 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1289 if (render_accessibility && tree_.size() > 1) {
1290 ui::AXNode* root = tree_.root();
1291 ui::AXNodeData root_data = root->data();
1292 root_data.relative_bounds.transform = MakeTransformFromViewInfo();
1293 root->SetData(root_data);
1294 UpdateAXTreeDataFromSelection();
1295 render_accessibility->OnPluginRootNodeUpdated();
1296 }
1297 }
1298
SetAccessibilityDocInfo(const PP_PrivateAccessibilityDocInfo & doc_info)1299 void PdfAccessibilityTree::SetAccessibilityDocInfo(
1300 const PP_PrivateAccessibilityDocInfo& doc_info) {
1301 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1302 if (!render_accessibility)
1303 return;
1304
1305 doc_info_ = doc_info;
1306 doc_node_ =
1307 CreateNode(ax::mojom::Role::kDocument, ax::mojom::Restriction::kReadOnly,
1308 render_accessibility, &nodes_);
1309 doc_node_->AddStringAttribute(
1310 ax::mojom::StringAttribute::kName,
1311 l10n_util::GetPluralStringFUTF8(IDS_PDF_DOCUMENT_PAGE_COUNT,
1312 doc_info.page_count));
1313
1314 // Because all of the coordinates are expressed relative to the
1315 // doc's coordinates, the origin of the doc must be (0, 0). Its
1316 // width and height will be updated as we add each page so that the
1317 // doc's bounding box surrounds all pages.
1318 doc_node_->relative_bounds.bounds = gfx::RectF(0, 0, 1, 1);
1319 }
1320
SetAccessibilityPageInfo(const PP_PrivateAccessibilityPageInfo & page_info,const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,const ppapi::PdfAccessibilityPageObjects & page_objects)1321 void PdfAccessibilityTree::SetAccessibilityPageInfo(
1322 const PP_PrivateAccessibilityPageInfo& page_info,
1323 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
1324 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
1325 const ppapi::PdfAccessibilityPageObjects& page_objects) {
1326 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1327 if (!render_accessibility)
1328 return;
1329
1330 // If unsanitized data is found, don't trust the PPAPI process sending it and
1331 // stop creation of the accessibility tree.
1332 if (!invalid_plugin_message_received_) {
1333 invalid_plugin_message_received_ =
1334 !IsDataFromPluginValid(text_runs, chars, page_objects);
1335 }
1336 if (invalid_plugin_message_received_)
1337 return;
1338
1339 uint32_t page_index = page_info.page_index;
1340 CHECK_GE(page_index, 0U);
1341 CHECK_LT(page_index, doc_info_.page_count);
1342
1343 ui::AXNodeData* page_node =
1344 CreateNode(ax::mojom::Role::kRegion, ax::mojom::Restriction::kReadOnly,
1345 render_accessibility, &nodes_);
1346 page_node->AddStringAttribute(
1347 ax::mojom::StringAttribute::kName,
1348 l10n_util::GetPluralStringFUTF8(IDS_PDF_PAGE_INDEX, page_index + 1));
1349 page_node->AddBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject,
1350 true);
1351
1352 gfx::RectF page_bounds = PPRectToGfxRectF(page_info.bounds);
1353 page_node->relative_bounds.bounds = page_bounds;
1354 doc_node_->relative_bounds.bounds.Union(page_node->relative_bounds.bounds);
1355 doc_node_->child_ids.push_back(page_node->id);
1356
1357 AddPageContent(page_node, page_bounds, page_index, text_runs, chars,
1358 page_objects, render_accessibility);
1359
1360 if (page_index == doc_info_.page_count - 1)
1361 Finish();
1362 }
1363
AddPageContent(ui::AXNodeData * page_node,const gfx::RectF & page_bounds,uint32_t page_index,const std::vector<ppapi::PdfAccessibilityTextRunInfo> & text_runs,const std::vector<PP_PrivateAccessibilityCharInfo> & chars,const ppapi::PdfAccessibilityPageObjects & page_objects,content::RenderAccessibility * render_accessibility)1364 void PdfAccessibilityTree::AddPageContent(
1365 ui::AXNodeData* page_node,
1366 const gfx::RectF& page_bounds,
1367 uint32_t page_index,
1368 const std::vector<ppapi::PdfAccessibilityTextRunInfo>& text_runs,
1369 const std::vector<PP_PrivateAccessibilityCharInfo>& chars,
1370 const ppapi::PdfAccessibilityPageObjects& page_objects,
1371 content::RenderAccessibility* render_accessibility) {
1372 DCHECK(page_node);
1373 PdfAccessibilityTreeBuilder tree_builder(
1374 text_runs, chars, page_objects, page_bounds, page_index, page_node,
1375 render_accessibility, &nodes_, &node_id_to_page_char_index_,
1376 &node_id_to_annotation_info_);
1377 tree_builder.BuildPageTree();
1378 }
1379
Finish()1380 void PdfAccessibilityTree::Finish() {
1381 doc_node_->relative_bounds.transform = MakeTransformFromViewInfo();
1382
1383 ui::AXTreeUpdate update;
1384 update.root_id = doc_node_->id;
1385 for (const auto& node : nodes_)
1386 update.nodes.push_back(*node);
1387
1388 if (!tree_.Unserialize(update))
1389 LOG(FATAL) << tree_.error();
1390
1391 UpdateAXTreeDataFromSelection();
1392
1393 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1394 if (render_accessibility)
1395 render_accessibility->SetPluginTreeSource(this);
1396 }
1397
UpdateAXTreeDataFromSelection()1398 void PdfAccessibilityTree::UpdateAXTreeDataFromSelection() {
1399 tree_data_.sel_is_backward = false;
1400 if (selection_start_page_index_ > selection_end_page_index_) {
1401 tree_data_.sel_is_backward = true;
1402 } else if (selection_start_page_index_ == selection_end_page_index_ &&
1403 selection_start_char_index_ > selection_end_char_index_) {
1404 tree_data_.sel_is_backward = true;
1405 }
1406
1407 FindNodeOffset(selection_start_page_index_, selection_start_char_index_,
1408 &tree_data_.sel_anchor_object_id,
1409 &tree_data_.sel_anchor_offset);
1410 FindNodeOffset(selection_end_page_index_, selection_end_char_index_,
1411 &tree_data_.sel_focus_object_id, &tree_data_.sel_focus_offset);
1412 }
1413
FindNodeOffset(uint32_t page_index,uint32_t page_char_index,int32_t * out_node_id,int32_t * out_node_char_index) const1414 void PdfAccessibilityTree::FindNodeOffset(uint32_t page_index,
1415 uint32_t page_char_index,
1416 int32_t* out_node_id,
1417 int32_t* out_node_char_index) const {
1418 *out_node_id = -1;
1419 *out_node_char_index = 0;
1420 ui::AXNode* root = tree_.root();
1421 if (page_index >= root->children().size())
1422 return;
1423 ui::AXNode* page = root->children()[page_index];
1424
1425 // Iterate over all paragraphs within this given page, and static text nodes
1426 // within each paragraph.
1427 for (ui::AXNode* para : page->children()) {
1428 for (ui::AXNode* child_node : para->children()) {
1429 ui::AXNode* static_text = GetStaticTextNodeFromNode(child_node);
1430 if (!static_text)
1431 continue;
1432 // Look up the page-relative character index for static nodes from a map
1433 // we built while the document was initially built.
1434 auto iter = node_id_to_page_char_index_.find(static_text->id());
1435 uint32_t char_index = iter->second.char_index;
1436 uint32_t len = static_text->data()
1437 .GetStringAttribute(ax::mojom::StringAttribute::kName)
1438 .size();
1439
1440 // If the character index we're looking for falls within the range
1441 // of this node, return the node ID and index within this node's text.
1442 if (page_char_index <= char_index + len) {
1443 *out_node_id = static_text->id();
1444 *out_node_char_index = page_char_index - char_index;
1445 return;
1446 }
1447 }
1448 }
1449 }
1450
FindCharacterOffset(const ui::AXNode & node,uint32_t char_offset_in_node,PP_PdfPageCharacterIndex * page_char_index) const1451 bool PdfAccessibilityTree::FindCharacterOffset(
1452 const ui::AXNode& node,
1453 uint32_t char_offset_in_node,
1454 PP_PdfPageCharacterIndex* page_char_index) const {
1455 auto iter = node_id_to_page_char_index_.find(GetId(&node));
1456 if (iter == node_id_to_page_char_index_.end())
1457 return false;
1458 page_char_index->char_index = iter->second.char_index + char_offset_in_node;
1459 page_char_index->page_index = iter->second.page_index;
1460 return true;
1461 }
1462
GetRenderAccessibility()1463 content::RenderAccessibility* PdfAccessibilityTree::GetRenderAccessibility() {
1464 content::RenderFrame* render_frame =
1465 host_->GetRenderFrameForInstance(instance_);
1466 if (!render_frame)
1467 return nullptr;
1468 content::RenderAccessibility* render_accessibility =
1469 render_frame->GetRenderAccessibility();
1470 if (!render_accessibility)
1471 return nullptr;
1472
1473 // If RenderAccessibility is unable to generate valid positive IDs,
1474 // we shouldn't use it. This can happen if Blink accessibility is disabled
1475 // after we started generating the accessible PDF.
1476 if (render_accessibility->GenerateAXID() <= 0)
1477 return nullptr;
1478
1479 return render_accessibility;
1480 }
1481
1482 std::unique_ptr<gfx::Transform>
MakeTransformFromViewInfo() const1483 PdfAccessibilityTree::MakeTransformFromViewInfo() const {
1484 double applicable_scale_factor =
1485 content::RenderThread::Get()->IsUseZoomForDSF() ? scale_ : 1;
1486 auto transform = std::make_unique<gfx::Transform>();
1487 // |scroll_| represents the offset from which PDF content starts. It is the
1488 // height of the PDF toolbar and the width of sidenav in pixels if it is open.
1489 // Sizes of PDF toolbar and sidenav do not change with zoom.
1490 transform->Scale(applicable_scale_factor, applicable_scale_factor);
1491 transform->Translate(-scroll_);
1492 transform->Scale(zoom_, zoom_);
1493 transform->Translate(offset_);
1494 return transform;
1495 }
1496
AnnotationInfo(uint32_t page_index,uint32_t annotation_index)1497 PdfAccessibilityTree::AnnotationInfo::AnnotationInfo(uint32_t page_index,
1498 uint32_t annotation_index)
1499 : page_index(page_index), annotation_index(annotation_index) {}
1500
1501 PdfAccessibilityTree::AnnotationInfo::AnnotationInfo(
1502 const AnnotationInfo& other) = default;
1503
1504 PdfAccessibilityTree::AnnotationInfo::~AnnotationInfo() = default;
1505
1506 //
1507 // AXTreeSource implementation.
1508 //
1509
GetTreeData(ui::AXTreeData * tree_data) const1510 bool PdfAccessibilityTree::GetTreeData(ui::AXTreeData* tree_data) const {
1511 tree_data->sel_is_backward = tree_data_.sel_is_backward;
1512 tree_data->sel_anchor_object_id = tree_data_.sel_anchor_object_id;
1513 tree_data->sel_anchor_offset = tree_data_.sel_anchor_offset;
1514 tree_data->sel_focus_object_id = tree_data_.sel_focus_object_id;
1515 tree_data->sel_focus_offset = tree_data_.sel_focus_offset;
1516 return true;
1517 }
1518
GetRoot() const1519 ui::AXNode* PdfAccessibilityTree::GetRoot() const {
1520 return tree_.root();
1521 }
1522
GetFromId(int32_t id) const1523 ui::AXNode* PdfAccessibilityTree::GetFromId(int32_t id) const {
1524 return tree_.GetFromId(id);
1525 }
1526
GetId(const ui::AXNode * node) const1527 int32_t PdfAccessibilityTree::GetId(const ui::AXNode* node) const {
1528 return node->id();
1529 }
1530
GetChildren(const ui::AXNode * node,std::vector<const ui::AXNode * > * out_children) const1531 void PdfAccessibilityTree::GetChildren(
1532 const ui::AXNode* node,
1533 std::vector<const ui::AXNode*>* out_children) const {
1534 *out_children = std::vector<const ui::AXNode*>(node->children().cbegin(),
1535 node->children().cend());
1536 }
1537
GetParent(const ui::AXNode * node) const1538 ui::AXNode* PdfAccessibilityTree::GetParent(const ui::AXNode* node) const {
1539 return node->parent();
1540 }
1541
IsIgnored(const ui::AXNode * node) const1542 bool PdfAccessibilityTree::IsIgnored(const ui::AXNode* node) const {
1543 return node->IsIgnored();
1544 }
1545
IsValid(const ui::AXNode * node) const1546 bool PdfAccessibilityTree::IsValid(const ui::AXNode* node) const {
1547 return node != nullptr;
1548 }
1549
IsEqual(const ui::AXNode * node1,const ui::AXNode * node2) const1550 bool PdfAccessibilityTree::IsEqual(const ui::AXNode* node1,
1551 const ui::AXNode* node2) const {
1552 return node1 == node2;
1553 }
1554
GetNull() const1555 const ui::AXNode* PdfAccessibilityTree::GetNull() const {
1556 return nullptr;
1557 }
1558
SerializeNode(const ui::AXNode * node,ui::AXNodeData * out_data) const1559 void PdfAccessibilityTree::SerializeNode(
1560 const ui::AXNode* node, ui::AXNodeData* out_data) const {
1561 *out_data = node->data();
1562 }
1563
CreateActionTarget(const ui::AXNode & target_node)1564 std::unique_ptr<ui::AXActionTarget> PdfAccessibilityTree::CreateActionTarget(
1565 const ui::AXNode& target_node) {
1566 return std::make_unique<PdfAXActionTarget>(target_node, this);
1567 }
1568
ShowContextMenu()1569 bool PdfAccessibilityTree::ShowContextMenu() {
1570 content::RenderAccessibility* render_accessibility = GetRenderAccessibility();
1571 if (!render_accessibility)
1572 return false;
1573
1574 render_accessibility->ShowPluginContextMenu();
1575 return true;
1576 }
1577
HandleAction(const PP_PdfAccessibilityActionData & action_data)1578 void PdfAccessibilityTree::HandleAction(
1579 const PP_PdfAccessibilityActionData& action_data) {
1580 content::PepperPluginInstance* plugin_instance =
1581 host_->GetPluginInstance(instance_);
1582 if (plugin_instance) {
1583 plugin_instance->HandleAccessibilityAction(action_data);
1584 }
1585 }
1586
1587 base::Optional<PdfAccessibilityTree::AnnotationInfo>
GetPdfAnnotationInfoFromAXNode(int32_t ax_node_id) const1588 PdfAccessibilityTree::GetPdfAnnotationInfoFromAXNode(int32_t ax_node_id) const {
1589 auto iter = node_id_to_annotation_info_.find(ax_node_id);
1590 if (iter == node_id_to_annotation_info_.end())
1591 return base::nullopt;
1592
1593 return AnnotationInfo(iter->second.page_index, iter->second.annotation_index);
1594 }
1595
1596 } // namespace pdf
1597