1 // Copyright 2014 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 "content/renderer/accessibility/blink_ax_tree_source.h"
6 
7 #include <stddef.h>
8 
9 #include <algorithm>
10 #include <set>
11 
12 #include "base/command_line.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/stl_util.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/strings/string_util.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "build/build_config.h"
19 #include "content/common/ax_serialization_utils.h"
20 #include "content/public/common/content_features.h"
21 #include "content/renderer/accessibility/ax_image_annotator.h"
22 #include "content/renderer/accessibility/render_accessibility_impl.h"
23 #include "content/renderer/render_frame_impl.h"
24 #include "content/renderer/render_frame_proxy.h"
25 #include "content/renderer/render_view_impl.h"
26 #include "third_party/blink/public/platform/web_rect.h"
27 #include "third_party/blink/public/platform/web_size.h"
28 #include "third_party/blink/public/platform/web_string.h"
29 #include "third_party/blink/public/platform/web_vector.h"
30 #include "third_party/blink/public/web/web_ax_enums.h"
31 #include "third_party/blink/public/web/web_ax_object.h"
32 #include "third_party/blink/public/web/web_disallow_transition_scope.h"
33 #include "third_party/blink/public/web/web_document.h"
34 #include "third_party/blink/public/web/web_element.h"
35 #include "third_party/blink/public/web/web_form_control_element.h"
36 #include "third_party/blink/public/web/web_frame.h"
37 #include "third_party/blink/public/web/web_local_frame.h"
38 #include "third_party/blink/public/web/web_node.h"
39 #include "third_party/blink/public/web/web_plugin.h"
40 #include "third_party/blink/public/web/web_plugin_container.h"
41 #include "third_party/blink/public/web/web_view.h"
42 #include "ui/accessibility/accessibility_features.h"
43 #include "ui/accessibility/accessibility_switches.h"
44 #include "ui/accessibility/ax_enum_util.h"
45 #include "ui/accessibility/ax_role_properties.h"
46 #include "ui/accessibility/ax_tree_id.h"
47 #include "ui/gfx/geometry/vector2d_f.h"
48 #include "url/gurl.h"
49 #include "url/url_constants.h"
50 
51 using base::ASCIIToUTF16;
52 using base::UTF16ToUTF8;
53 using blink::WebAXObject;
54 using blink::WebAXObjectAttribute;
55 using blink::WebAXObjectVectorAttribute;
56 using blink::WebDocument;
57 using blink::WebElement;
58 using blink::WebFrame;
59 using blink::WebLocalFrame;
60 using blink::WebNode;
61 using blink::WebPlugin;
62 using blink::WebPluginContainer;
63 using blink::WebVector;
64 using blink::WebView;
65 
66 namespace content {
67 
68 namespace {
69 
70 // Images smaller than this number, in CSS pixels, will never get annotated.
71 // Note that OCR works on pretty small images, so this shouldn't be too large.
72 const int kMinImageAnnotationWidth = 16;
73 const int kMinImageAnnotationHeight = 16;
74 
AddIntListAttributeFromWebObjects(ax::mojom::IntListAttribute attr,const WebVector<WebAXObject> & objects,ui::AXNodeData * dst)75 void AddIntListAttributeFromWebObjects(ax::mojom::IntListAttribute attr,
76                                        const WebVector<WebAXObject>& objects,
77                                        ui::AXNodeData* dst) {
78   std::vector<int32_t> ids;
79   for (size_t i = 0; i < objects.size(); i++)
80     ids.push_back(objects[i].AxID());
81   if (!ids.empty())
82     dst->AddIntListAttribute(attr, ids);
83 }
84 
85 class AXNodeDataSparseAttributeAdapter
86     : public blink::WebAXSparseAttributeClient {
87  public:
AXNodeDataSparseAttributeAdapter(ui::AXNodeData * dst)88   explicit AXNodeDataSparseAttributeAdapter(ui::AXNodeData* dst) : dst_(dst) {
89     DCHECK(dst_);
90   }
91   ~AXNodeDataSparseAttributeAdapter() override = default;
92 
93  private:
94   ui::AXNodeData* dst_;
95 
AddObjectAttribute(WebAXObjectAttribute attribute,const WebAXObject & value)96   void AddObjectAttribute(WebAXObjectAttribute attribute,
97                           const WebAXObject& value) override {
98     switch (attribute) {
99       case WebAXObjectAttribute::kAriaActiveDescendant:
100         // TODO(dmazzoni): WebAXObject::ActiveDescendant currently returns
101         // more information than the sparse interface does.
102         // ******** Why is this a TODO? ********
103         break;
104       case WebAXObjectAttribute::kAriaErrorMessage:
105         // Use WebAXObject::ErrorMessage(), which provides both ARIA error
106         // messages as well as built-in HTML form validation messages.
107         break;
108       default:
109         NOTREACHED();
110     }
111   }
112 
AddObjectVectorAttribute(WebAXObjectVectorAttribute attribute,const blink::WebVector<WebAXObject> & value)113   void AddObjectVectorAttribute(
114       WebAXObjectVectorAttribute attribute,
115       const blink::WebVector<WebAXObject>& value) override {
116     switch (attribute) {
117       case WebAXObjectVectorAttribute::kAriaControls:
118         AddIntListAttributeFromWebObjects(
119             ax::mojom::IntListAttribute::kControlsIds, value, dst_);
120         break;
121       case WebAXObjectVectorAttribute::kAriaDetails:
122         AddIntListAttributeFromWebObjects(
123             ax::mojom::IntListAttribute::kDetailsIds, value, dst_);
124         break;
125       case WebAXObjectVectorAttribute::kAriaFlowTo:
126         AddIntListAttributeFromWebObjects(
127             ax::mojom::IntListAttribute::kFlowtoIds, value, dst_);
128         break;
129       default:
130         NOTREACHED();
131     }
132   }
133 };
134 
ParentObjectUnignored(WebAXObject child)135 WebAXObject ParentObjectUnignored(WebAXObject child) {
136   WebAXObject parent = child.ParentObject();
137   while (!parent.IsDetached() && !parent.AccessibilityIsIncludedInTree())
138     parent = parent.ParentObject();
139   return parent;
140 }
141 
142 // Returns true if |ancestor| is the first unignored parent of |child|,
143 // which means that when walking up the parent chain from |child|,
144 // |ancestor| is the *first* ancestor that isn't marked as
145 // accessibilityIsIgnored().
IsParentUnignoredOf(WebAXObject ancestor,WebAXObject child)146 bool IsParentUnignoredOf(WebAXObject ancestor, WebAXObject child) {
147   WebAXObject parent = ParentObjectUnignored(child);
148   return parent.Equals(ancestor);
149 }
150 
151 // Helper function that searches in the subtree of |obj| to a max
152 // depth of |max_depth| for an image.
153 //
154 // Returns true on success, or false if it finds more than one image,
155 // or any node with a name, or anything deeper than |max_depth|.
SearchForExactlyOneInnerImage(WebAXObject obj,WebAXObject * inner_image,int max_depth)156 bool SearchForExactlyOneInnerImage(WebAXObject obj,
157                                    WebAXObject* inner_image,
158                                    int max_depth) {
159   DCHECK(inner_image);
160 
161   // If it's the first image, set |inner_image|. If we already
162   // found an image, fail.
163   if (obj.Role() == ax::mojom::Role::kImage) {
164     if (!inner_image->IsDetached())
165       return false;
166     *inner_image = obj;
167   } else {
168     // If we found something else with a name, fail.
169     if (!ui::IsDocument(obj.Role()) && !ui::IsLink(obj.Role())) {
170       blink::WebString web_name = obj.GetName();
171       if (!base::ContainsOnlyChars(web_name.Utf8(), base::kWhitespaceASCII)) {
172         return false;
173       }
174     }
175   }
176 
177   // Fail if we recursed to |max_depth| and there's more of a subtree.
178   if (max_depth == 0 && obj.ChildCount())
179     return false;
180 
181   // Recurse.
182   for (unsigned int i = 0; i < obj.ChildCount(); i++) {
183     if (!SearchForExactlyOneInnerImage(obj.ChildAt(i), inner_image,
184                                        max_depth - 1))
185       return false;
186   }
187 
188   return !inner_image->IsDetached();
189 }
190 
191 // Return true if the subtree of |obj|, to a max depth of 3, contains
192 // exactly one image. Return that image in |inner_image|.
FindExactlyOneInnerImageInMaxDepthThree(WebAXObject obj,WebAXObject * inner_image)193 bool FindExactlyOneInnerImageInMaxDepthThree(WebAXObject obj,
194                                              WebAXObject* inner_image) {
195   DCHECK(inner_image);
196   return SearchForExactlyOneInnerImage(obj, inner_image, /* max_depth = */ 3);
197 }
198 
GetEquivalentAriaRoleString(const ax::mojom::Role role)199 std::string GetEquivalentAriaRoleString(const ax::mojom::Role role) {
200   switch (role) {
201     case ax::mojom::Role::kArticle:
202       return "article";
203     case ax::mojom::Role::kBanner:
204       return "banner";
205     case ax::mojom::Role::kButton:
206       return "button";
207     case ax::mojom::Role::kComplementary:
208       return "complementary";
209     case ax::mojom::Role::kFigure:
210       return "figure";
211     case ax::mojom::Role::kFooter:
212       return "contentinfo";
213     case ax::mojom::Role::kHeader:
214       return "banner";
215     case ax::mojom::Role::kHeading:
216       return "heading";
217     case ax::mojom::Role::kImage:
218       return "img";
219     case ax::mojom::Role::kMain:
220       return "main";
221     case ax::mojom::Role::kNavigation:
222       return "navigation";
223     case ax::mojom::Role::kRadioButton:
224       return "radio";
225     case ax::mojom::Role::kRegion:
226       return "region";
227     case ax::mojom::Role::kSection:
228       // A <section> element uses the 'region' ARIA role mapping.
229       return "region";
230     case ax::mojom::Role::kSlider:
231       return "slider";
232     case ax::mojom::Role::kTime:
233       return "time";
234     default:
235       break;
236   }
237 
238   return std::string();
239 }
240 
241 }  // namespace
242 
ScopedFreezeBlinkAXTreeSource(BlinkAXTreeSource * tree_source)243 ScopedFreezeBlinkAXTreeSource::ScopedFreezeBlinkAXTreeSource(
244     BlinkAXTreeSource* tree_source)
245     : tree_source_(tree_source) {
246   tree_source_->Freeze();
247 }
248 
~ScopedFreezeBlinkAXTreeSource()249 ScopedFreezeBlinkAXTreeSource::~ScopedFreezeBlinkAXTreeSource() {
250   tree_source_->Thaw();
251 }
252 
BlinkAXTreeSource(RenderFrameImpl * render_frame,ui::AXMode mode)253 BlinkAXTreeSource::BlinkAXTreeSource(RenderFrameImpl* render_frame,
254                                      ui::AXMode mode)
255     : render_frame_(render_frame), accessibility_mode_(mode), frozen_(false) {
256   image_annotation_debugging_ =
257       base::CommandLine::ForCurrentProcess()->HasSwitch(
258           ::switches::kEnableExperimentalAccessibilityLabelsDebugging);
259 }
260 
~BlinkAXTreeSource()261 BlinkAXTreeSource::~BlinkAXTreeSource() {}
262 
Freeze()263 void BlinkAXTreeSource::Freeze() {
264   CHECK(!frozen_);
265   frozen_ = true;
266 
267   if (render_frame_ && render_frame_->GetWebFrame())
268     document_ = render_frame_->GetWebFrame()->GetDocument();
269   else
270     document_ = WebDocument();
271 
272   root_ = ComputeRoot();
273 
274   if (!document_.IsNull())
275     focus_ = WebAXObject::FromWebDocumentFocused(document_);
276   else
277     focus_ = WebAXObject();
278 }
279 
Thaw()280 void BlinkAXTreeSource::Thaw() {
281   CHECK(frozen_);
282   frozen_ = false;
283 }
284 
SetRoot(WebAXObject root)285 void BlinkAXTreeSource::SetRoot(WebAXObject root) {
286   CHECK(!frozen_);
287   explicit_root_ = root;
288 }
289 
IsInTree(WebAXObject node) const290 bool BlinkAXTreeSource::IsInTree(WebAXObject node) const {
291   CHECK(frozen_);
292   while (IsValid(node)) {
293     if (node.Equals(root()))
294       return true;
295     node = GetParent(node);
296   }
297   return false;
298 }
299 
SetAccessibilityMode(ui::AXMode new_mode)300 void BlinkAXTreeSource::SetAccessibilityMode(ui::AXMode new_mode) {
301   if (accessibility_mode_ == new_mode)
302     return;
303   accessibility_mode_ = new_mode;
304 }
305 
ShouldLoadInlineTextBoxes(const blink::WebAXObject & obj) const306 bool BlinkAXTreeSource::ShouldLoadInlineTextBoxes(
307     const blink::WebAXObject& obj) const {
308 #if !defined(OS_ANDROID)
309   // If inline text boxes are enabled globally, no need to explicitly load them.
310   if (accessibility_mode_.has_mode(ui::AXMode::kInlineTextBoxes))
311     return false;
312 #endif
313 
314   // On some platforms, like Android, we only load inline text boxes for
315   // a subset of nodes:
316   //
317   // Within the subtree of a focused editable text area.
318   // When specifically enabled for a subtree via |load_inline_text_boxes_ids_|.
319 
320   int32_t focus_id = focus().AxID();
321   WebAXObject ancestor = obj;
322   while (!ancestor.IsDetached()) {
323     int32_t ancestor_id = ancestor.AxID();
324     if (base::Contains(load_inline_text_boxes_ids_, ancestor_id) ||
325         (ancestor_id == focus_id && ancestor.IsEditable())) {
326       return true;
327     }
328     ancestor = ancestor.ParentObject();
329   }
330 
331   return false;
332 }
333 
SetLoadInlineTextBoxesForId(int32_t id)334 void BlinkAXTreeSource::SetLoadInlineTextBoxesForId(int32_t id) {
335   // Keeping stale IDs in the set is harmless but we don't want it to keep
336   // growing without bound, so clear out any unnecessary IDs whenever this
337   // method is called.
338   for (auto iter = load_inline_text_boxes_ids_.begin();
339        iter != load_inline_text_boxes_ids_.end();) {
340     if (GetFromId(*iter).IsDetached())
341       iter = load_inline_text_boxes_ids_.erase(iter);
342     else
343       ++iter;
344   }
345 
346   load_inline_text_boxes_ids_.insert(id);
347 }
348 
PopulateAXRelativeBounds(WebAXObject obj,ui::AXRelativeBounds * bounds,bool * clips_children) const349 void BlinkAXTreeSource::PopulateAXRelativeBounds(WebAXObject obj,
350                                                  ui::AXRelativeBounds* bounds,
351                                                  bool* clips_children) const {
352   WebAXObject offset_container;
353   gfx::RectF bounds_in_container;
354   SkMatrix44 web_container_transform;
355   obj.GetRelativeBounds(offset_container, bounds_in_container,
356                         web_container_transform, clips_children);
357   bounds->bounds = bounds_in_container;
358   if (!offset_container.IsDetached())
359     bounds->offset_container_id = offset_container.AxID();
360 
361   if (content::AXShouldIncludePageScaleFactorInRoot() && obj.Equals(root())) {
362     const WebView* web_view = render_frame_->GetRenderView()->GetWebView();
363     std::unique_ptr<gfx::Transform> container_transform =
364         std::make_unique<gfx::Transform>(web_container_transform);
365     container_transform->Scale(web_view->PageScaleFactor(),
366                                web_view->PageScaleFactor());
367     container_transform->Translate(
368         -web_view->VisualViewportOffset().OffsetFromOrigin());
369     if (!container_transform->IsIdentity())
370       bounds->transform = std::move(container_transform);
371   } else if (!web_container_transform.isIdentity()) {
372     bounds->transform =
373         base::WrapUnique(new gfx::Transform(web_container_transform));
374   }
375 }
376 
HasCachedBoundingBox(int32_t id) const377 bool BlinkAXTreeSource::HasCachedBoundingBox(int32_t id) const {
378   return base::Contains(cached_bounding_boxes_, id);
379 }
380 
GetCachedBoundingBox(int32_t id) const381 const ui::AXRelativeBounds& BlinkAXTreeSource::GetCachedBoundingBox(
382     int32_t id) const {
383   auto iter = cached_bounding_boxes_.find(id);
384   DCHECK(iter != cached_bounding_boxes_.end());
385   return iter->second;
386 }
387 
SetCachedBoundingBox(int32_t id,const ui::AXRelativeBounds & bounds)388 void BlinkAXTreeSource::SetCachedBoundingBox(
389     int32_t id,
390     const ui::AXRelativeBounds& bounds) {
391   cached_bounding_boxes_[id] = bounds;
392 }
393 
GetCachedBoundingBoxCount() const394 size_t BlinkAXTreeSource::GetCachedBoundingBoxCount() const {
395   return cached_bounding_boxes_.size();
396 }
397 
GetTreeData(ui::AXTreeData * tree_data) const398 bool BlinkAXTreeSource::GetTreeData(ui::AXTreeData* tree_data) const {
399   CHECK(frozen_);
400   tree_data->doctype = "html";
401   tree_data->loaded = root().IsLoaded();
402   tree_data->loading_progress = root().EstimatedLoadingProgress();
403   tree_data->mimetype =
404       document().IsXHTMLDocument() ? "text/xhtml" : "text/html";
405   tree_data->title = document().Title().Utf8();
406   tree_data->url = document().Url().GetString().Utf8();
407 
408   if (!focus().IsNull())
409     tree_data->focus_id = focus().AxID();
410 
411   bool is_selection_backward = false;
412   WebAXObject anchor_object, focus_object;
413   int anchor_offset, focus_offset;
414   ax::mojom::TextAffinity anchor_affinity, focus_affinity;
415   root().Selection(is_selection_backward, anchor_object, anchor_offset,
416                    anchor_affinity, focus_object, focus_offset, focus_affinity);
417   if (!anchor_object.IsNull() && !focus_object.IsNull() && anchor_offset >= 0 &&
418       focus_offset >= 0) {
419     int32_t anchor_id = anchor_object.AxID();
420     int32_t focus_id = focus_object.AxID();
421     tree_data->sel_is_backward = is_selection_backward;
422     tree_data->sel_anchor_object_id = anchor_id;
423     tree_data->sel_anchor_offset = anchor_offset;
424     tree_data->sel_focus_object_id = focus_id;
425     tree_data->sel_focus_offset = focus_offset;
426     tree_data->sel_anchor_affinity = anchor_affinity;
427     tree_data->sel_focus_affinity = focus_affinity;
428   }
429 
430   // Get the tree ID for this frame.
431   if (WebLocalFrame* web_frame = document().GetFrame())
432     tree_data->tree_id = web_frame->GetAXTreeID();
433 
434   tree_data->root_scroller_id = root().RootScroller().AxID();
435 
436   return true;
437 }
438 
GetRoot() const439 WebAXObject BlinkAXTreeSource::GetRoot() const {
440   if (frozen_)
441     return root_;
442   else
443     return ComputeRoot();
444 }
445 
GetFromId(int32_t id) const446 WebAXObject BlinkAXTreeSource::GetFromId(int32_t id) const {
447   return WebAXObject::FromWebDocumentByID(GetMainDocument(), id);
448 }
449 
GetId(WebAXObject node) const450 int32_t BlinkAXTreeSource::GetId(WebAXObject node) const {
451   return node.AxID();
452 }
453 
GetChildren(WebAXObject parent,std::vector<WebAXObject> * out_children) const454 void BlinkAXTreeSource::GetChildren(
455     WebAXObject parent,
456     std::vector<WebAXObject>* out_children) const {
457   CHECK(frozen_);
458 
459   if ((parent.Role() == ax::mojom::Role::kStaticText ||
460        parent.Role() == ax::mojom::Role::kLineBreak) &&
461       ShouldLoadInlineTextBoxes(parent)) {
462     parent.LoadInlineTextBoxes();
463   }
464 
465   bool is_iframe = false;
466   WebNode node = parent.GetNode();
467   if (!node.IsNull() && node.IsElementNode())
468     is_iframe = node.To<WebElement>().HasHTMLTagName("iframe");
469 
470   for (unsigned i = 0; i < parent.ChildCount(); i++) {
471     WebAXObject child = parent.ChildAt(i);
472 
473     // The child may be invalid due to issues in blink accessibility code.
474     if (child.IsDetached())
475       continue;
476 
477     // Skip children whose parent isn't |parent|.
478     // As an exception, include children of an iframe element.
479     if (!is_iframe && !IsParentUnignoredOf(parent, child))
480       continue;
481 
482     // Skip table headers and columns, they're only needed on Mac
483     // and soon we'll get rid of this code entirely.
484     if (child.Role() == ax::mojom::Role::kColumn ||
485         child.Role() == ax::mojom::Role::kTableHeaderContainer)
486       continue;
487 
488     out_children->push_back(child);
489   }
490 }
491 
GetParent(WebAXObject node) const492 WebAXObject BlinkAXTreeSource::GetParent(WebAXObject node) const {
493   CHECK(frozen_);
494 
495   // Blink returns ignored objects when walking up the parent chain,
496   // we have to skip those here. Also, stop when we get to the root
497   // element.
498   do {
499     if (node.Equals(root()))
500       return WebAXObject();
501     node = node.ParentObject();
502   } while (!node.IsDetached() && !node.AccessibilityIsIncludedInTree());
503 
504   return node;
505 }
506 
IsIgnored(WebAXObject node) const507 bool BlinkAXTreeSource::IsIgnored(WebAXObject node) const {
508   return node.AccessibilityIsIgnored();
509 }
510 
IsValid(WebAXObject node) const511 bool BlinkAXTreeSource::IsValid(WebAXObject node) const {
512   return !node.IsDetached();  // This also checks if it's null.
513 }
514 
IsEqual(WebAXObject node1,WebAXObject node2) const515 bool BlinkAXTreeSource::IsEqual(WebAXObject node1, WebAXObject node2) const {
516   return node1.Equals(node2);
517 }
518 
GetNull() const519 WebAXObject BlinkAXTreeSource::GetNull() const {
520   return WebAXObject();
521 }
522 
GetDebugString(blink::WebAXObject node) const523 std::string BlinkAXTreeSource::GetDebugString(blink::WebAXObject node) const {
524   return node.ToString(true).Utf8();
525 }
526 
SerializerClearedNode(int32_t node_id)527 void BlinkAXTreeSource::SerializerClearedNode(int32_t node_id) {
528   cached_bounding_boxes_.erase(node_id);
529 }
530 
SerializeNode(WebAXObject src,ui::AXNodeData * dst) const531 void BlinkAXTreeSource::SerializeNode(WebAXObject src,
532                                       ui::AXNodeData* dst) const {
533 #if DCHECK_IS_ON()
534   // Never causes a document lifecycle change during serialization,
535   // because the assumption is that layout is in a safe, stable state.
536   WebDocument document = GetMainDocument();
537   blink::WebDisallowTransitionScope disallow(&document);
538 #endif
539 
540   // TODO(crbug.com/1068668): AX onion soup - finish migrating the rest of
541   // this function inside of AXObject::Serialize and removing
542   // unneeded WebAXObject interfaces.
543   src.Serialize(dst, accessibility_mode_);
544 
545   dst->role = src.Role();
546   dst->id = src.AxID();
547 
548   TRACE_EVENT2("accessibility", "BlinkAXTreeSource::SerializeNode", "role",
549                ui::ToString(dst->role), "id", dst->id);
550 
551   SerializeNameAndDescriptionAttributes(src, dst);
552 
553   if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader) ||
554       accessibility_mode_.has_mode(ui::AXMode::kPDF)) {
555     // Heading level.
556     if (ui::IsHeading(dst->role) && src.HeadingLevel()) {
557       dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
558                            src.HeadingLevel());
559     }
560 
561     WebAXObject parent = ParentObjectUnignored(src);
562     if (src.Language().length()) {
563       // TODO(chrishall): should we still trim redundant languages off here?
564       if (parent.IsNull() || parent.Language() != src.Language()) {
565         TruncateAndAddStringAttribute(
566             dst, ax::mojom::StringAttribute::kLanguage, src.Language().Utf8());
567       }
568     }
569 
570     SerializeListAttributes(src, dst);
571   }
572 
573   if (accessibility_mode_.has_mode(ui::AXMode::kPDF)) {
574     // Return early. None of the following attributes are needed for PDFs.
575     return;
576   }
577 
578   SerializeBoundingBoxAttributes(src, dst);
579   cached_bounding_boxes_[dst->id] = dst->relative_bounds;
580 
581   SerializeSparseAttributes(src, dst);
582   SerializeChooserPopupAttributes(src, dst);
583 
584   if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) {
585     SerializeMarkerAttributes(src, dst);
586     if (src.IsInLiveRegion())
587       SerializeLiveRegionAttributes(src, dst);
588     SerializeOtherScreenReaderAttributes(src, dst);
589   }
590 
591   WebNode node = src.GetNode();
592   bool is_iframe = false;
593   if (!node.IsNull() && node.IsElementNode()) {
594     WebElement element = node.To<WebElement>();
595     is_iframe = element.HasHTMLTagName("iframe");
596 
597     SerializeElementAttributes(src, element, dst);
598     if (accessibility_mode_.has_mode(ui::AXMode::kHTML)) {
599       SerializeHTMLAttributes(src, element, dst);
600     }
601 
602     // Presence of other ARIA attributes.
603     if (src.HasAriaAttribute())
604       dst->AddBoolAttribute(ax::mojom::BoolAttribute::kHasAriaAttribute, true);
605   }
606 
607   // Add the ids of *indirect* children - those who are children of this node,
608   // but whose parent is *not* this node. One example is a table
609   // cell, which is a child of both a row and a column. Because the cell's
610   // parent is the row, the row adds it as a child, and the column adds it
611   // as an indirect child.
612   int child_count = src.ChildCount();
613   std::vector<int32_t> indirect_child_ids;
614   for (int i = 0; i < child_count; ++i) {
615     WebAXObject child = src.ChildAt(i);
616     if (!is_iframe && !child.IsDetached() && !IsParentUnignoredOf(src, child))
617       indirect_child_ids.push_back(child.AxID());
618   }
619   if (indirect_child_ids.size() > 0) {
620     dst->AddIntListAttribute(ax::mojom::IntListAttribute::kIndirectChildIds,
621                              indirect_child_ids);
622   }
623 
624   if (src.IsScrollableContainer()) {
625     SerializeScrollAttributes(src, dst);
626   }
627 
628   if (dst->id == image_data_node_id_) {
629     // In general, string attributes should be truncated using
630     // TruncateAndAddStringAttribute, but ImageDataUrl contains a data url
631     // representing an image, so add it directly using AddStringAttribute.
632     dst->AddStringAttribute(ax::mojom::StringAttribute::kImageDataUrl,
633                             src.ImageDataUrl(max_image_data_size_).Utf8());
634   }
635 }
636 
SerializeBoundingBoxAttributes(WebAXObject src,ui::AXNodeData * dst) const637 void BlinkAXTreeSource::SerializeBoundingBoxAttributes(
638     WebAXObject src,
639     ui::AXNodeData* dst) const {
640   bool clips_children = false;
641   PopulateAXRelativeBounds(src, &dst->relative_bounds, &clips_children);
642   if (clips_children)
643     dst->AddBoolAttribute(ax::mojom::BoolAttribute::kClipsChildren, true);
644 
645   if (src.IsLineBreakingObject()) {
646     dst->AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
647                           true);
648   }
649 }
650 
SerializeSparseAttributes(WebAXObject src,ui::AXNodeData * dst) const651 void BlinkAXTreeSource::SerializeSparseAttributes(WebAXObject src,
652                                                   ui::AXNodeData* dst) const {
653   AXNodeDataSparseAttributeAdapter sparse_attribute_adapter(dst);
654   src.GetSparseAXAttributes(sparse_attribute_adapter);
655 }
656 
SerializeNameAndDescriptionAttributes(WebAXObject src,ui::AXNodeData * dst) const657 void BlinkAXTreeSource::SerializeNameAndDescriptionAttributes(
658     WebAXObject src,
659     ui::AXNodeData* dst) const {
660   ax::mojom::NameFrom name_from;
661   blink::WebVector<WebAXObject> name_objects;
662   blink::WebString web_name = src.GetName(name_from, name_objects);
663   if ((!web_name.IsEmpty() && !web_name.IsNull()) ||
664       name_from == ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
665     int max_length = dst->role == ax::mojom::Role::kStaticText
666                          ? kMaxStaticTextLength
667                          : kMaxStringAttributeLength;
668     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kName,
669                                   web_name.Utf8(), max_length);
670     dst->SetNameFrom(name_from);
671     AddIntListAttributeFromWebObjects(
672         ax::mojom::IntListAttribute::kLabelledbyIds, name_objects, dst);
673   }
674 
675   ax::mojom::DescriptionFrom description_from;
676   blink::WebVector<WebAXObject> description_objects;
677   blink::WebString web_description =
678       src.Description(name_from, description_from, description_objects);
679   if (!web_description.IsEmpty()) {
680     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDescription,
681                                   web_description.Utf8());
682     dst->SetDescriptionFrom(description_from);
683     AddIntListAttributeFromWebObjects(
684         ax::mojom::IntListAttribute::kDescribedbyIds, description_objects, dst);
685   }
686 
687   blink::WebString web_title = src.Title(name_from);
688   if (!web_title.IsEmpty()) {
689     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kTooltip,
690                                   web_title.Utf8());
691   }
692 
693   if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) {
694     blink::WebString web_placeholder = src.Placeholder(name_from);
695     if (!web_placeholder.IsEmpty())
696       TruncateAndAddStringAttribute(dst,
697                                     ax::mojom::StringAttribute::kPlaceholder,
698                                     web_placeholder.Utf8());
699   }
700 }
701 
SerializeInlineTextBoxAttributes(WebAXObject src,ui::AXNodeData * dst) const702 void BlinkAXTreeSource::SerializeInlineTextBoxAttributes(
703     WebAXObject src,
704     ui::AXNodeData* dst) const {
705   DCHECK_EQ(ax::mojom::Role::kInlineTextBox, dst->role);
706 
707   WebVector<int> src_character_offsets;
708   src.CharacterOffsets(src_character_offsets);
709   dst->AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets,
710                            src_character_offsets.ReleaseVector());
711 
712   WebVector<int> src_word_starts;
713   WebVector<int> src_word_ends;
714   src.GetWordBoundaries(src_word_starts, src_word_ends);
715   dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
716                            src_word_starts.ReleaseVector());
717   dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
718                            src_word_ends.ReleaseVector());
719 }
720 
SerializeMarkerAttributes(WebAXObject src,ui::AXNodeData * dst) const721 void BlinkAXTreeSource::SerializeMarkerAttributes(WebAXObject src,
722                                                   ui::AXNodeData* dst) const {
723   // Spelling, grammar and other document markers.
724   WebVector<ax::mojom::MarkerType> src_marker_types;
725   WebVector<int> src_marker_starts;
726   WebVector<int> src_marker_ends;
727   src.Markers(src_marker_types, src_marker_starts, src_marker_ends);
728   DCHECK_EQ(src_marker_types.size(), src_marker_starts.size());
729   DCHECK_EQ(src_marker_starts.size(), src_marker_ends.size());
730 
731   if (src_marker_types.size()) {
732     std::vector<int32_t> marker_types;
733     std::vector<int32_t> marker_starts;
734     std::vector<int32_t> marker_ends;
735     marker_types.reserve(src_marker_types.size());
736     marker_starts.reserve(src_marker_starts.size());
737     marker_ends.reserve(src_marker_ends.size());
738     for (size_t i = 0; i < src_marker_types.size(); ++i) {
739       marker_types.push_back(static_cast<int32_t>(src_marker_types[i]));
740       marker_starts.push_back(src_marker_starts[i]);
741       marker_ends.push_back(src_marker_ends[i]);
742     }
743     dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes,
744                              marker_types);
745     dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts,
746                              marker_starts);
747     dst->AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds,
748                              marker_ends);
749   }
750 }
751 
SerializeLiveRegionAttributes(WebAXObject src,ui::AXNodeData * dst) const752 void BlinkAXTreeSource::SerializeLiveRegionAttributes(
753     WebAXObject src,
754     ui::AXNodeData* dst) const {
755   DCHECK(src.IsInLiveRegion());
756 
757   dst->AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic,
758                         src.LiveRegionAtomic());
759   if (!src.LiveRegionStatus().IsEmpty()) {
760     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveStatus,
761                                   src.LiveRegionStatus().Utf8());
762   }
763   TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveRelevant,
764                                 src.LiveRegionRelevant().Utf8());
765   // If we are not at the root of an atomic live region.
766   if (src.ContainerLiveRegionAtomic() && !src.LiveRegionRoot().IsDetached() &&
767       !src.LiveRegionAtomic()) {
768     dst->AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId,
769                          src.LiveRegionRoot().AxID());
770   }
771   dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveAtomic,
772                         src.ContainerLiveRegionAtomic());
773   dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveBusy,
774                         src.ContainerLiveRegionBusy());
775   TruncateAndAddStringAttribute(
776       dst, ax::mojom::StringAttribute::kContainerLiveStatus,
777       src.ContainerLiveRegionStatus().Utf8());
778   TruncateAndAddStringAttribute(
779       dst, ax::mojom::StringAttribute::kContainerLiveRelevant,
780       src.ContainerLiveRegionRelevant().Utf8());
781 }
782 
SerializeListAttributes(WebAXObject src,ui::AXNodeData * dst) const783 void BlinkAXTreeSource::SerializeListAttributes(WebAXObject src,
784                                                 ui::AXNodeData* dst) const {
785   if (src.SetSize())
786     dst->AddIntAttribute(ax::mojom::IntAttribute::kSetSize, src.SetSize());
787 
788   if (src.PosInSet())
789     dst->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, src.PosInSet());
790 }
791 
SerializeScrollAttributes(WebAXObject src,ui::AXNodeData * dst) const792 void BlinkAXTreeSource::SerializeScrollAttributes(WebAXObject src,
793                                                   ui::AXNodeData* dst) const {
794   // Only mark as scrollable if user has actual scrollbars to use.
795   dst->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable,
796                         src.IsUserScrollable());
797   // Provide x,y scroll info if scrollable in any way (programmatically or via
798   // user).
799   const gfx::Point& scroll_offset = src.GetScrollOffset();
800   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollX, scroll_offset.x());
801   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollY, scroll_offset.y());
802 
803   const gfx::Point& min_scroll_offset = src.MinimumScrollOffset();
804   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin,
805                        min_scroll_offset.x());
806   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMin,
807                        min_scroll_offset.y());
808 
809   const gfx::Point& max_scroll_offset = src.MaximumScrollOffset();
810   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax,
811                        max_scroll_offset.x());
812   dst->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMax,
813                        max_scroll_offset.y());
814 }
815 
SerializeChooserPopupAttributes(WebAXObject src,ui::AXNodeData * dst) const816 void BlinkAXTreeSource::SerializeChooserPopupAttributes(
817     WebAXObject src,
818     ui::AXNodeData* dst) const {
819   WebAXObject chooser_popup = src.ChooserPopup();
820   if (!chooser_popup.IsNull()) {
821     int32_t chooser_popup_id = chooser_popup.AxID();
822     auto controls_ids =
823         dst->GetIntListAttribute(ax::mojom::IntListAttribute::kControlsIds);
824     controls_ids.push_back(chooser_popup_id);
825     dst->AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds,
826                              controls_ids);
827   }
828 }
829 
SerializeOtherScreenReaderAttributes(WebAXObject src,ui::AXNodeData * dst) const830 void BlinkAXTreeSource::SerializeOtherScreenReaderAttributes(
831     WebAXObject src,
832     ui::AXNodeData* dst) const {
833   if (dst->role == ax::mojom::Role::kColorWell)
834     dst->AddIntAttribute(ax::mojom::IntAttribute::kColorValue,
835                          src.ColorValue());
836 
837   if (dst->role == ax::mojom::Role::kLink) {
838     WebAXObject target = src.InPageLinkTarget();
839     if (!target.IsNull()) {
840       int32_t target_id = target.AxID();
841       dst->AddIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId,
842                            target_id);
843     }
844   }
845 
846   if (dst->role == ax::mojom::Role::kRadioButton) {
847     AddIntListAttributeFromWebObjects(
848         ax::mojom::IntListAttribute::kRadioGroupIds, src.RadioButtonsInGroup(),
849         dst);
850   }
851 
852   if (src.AriaCurrentState() != ax::mojom::AriaCurrentState::kNone) {
853     dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
854                          static_cast<int32_t>(src.AriaCurrentState()));
855   }
856 
857   if (src.InvalidState() != ax::mojom::InvalidState::kNone)
858     dst->SetInvalidState(src.InvalidState());
859   if (src.InvalidState() == ax::mojom::InvalidState::kOther &&
860       src.AriaInvalidValue().length()) {
861     TruncateAndAddStringAttribute(dst,
862                                   ax::mojom::StringAttribute::kAriaInvalidValue,
863                                   src.AriaInvalidValue().Utf8());
864   }
865 
866   if (src.CheckedState() != ax::mojom::CheckedState::kNone) {
867     dst->SetCheckedState(src.CheckedState());
868   }
869 
870   if (dst->role == ax::mojom::Role::kInlineTextBox) {
871     SerializeInlineTextBoxAttributes(src, dst);
872   }
873 
874   if (src.AccessKey().length()) {
875     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kAccessKey,
876                                   src.AccessKey().Utf8());
877   }
878 
879   if (src.AutoComplete().length()) {
880     TruncateAndAddStringAttribute(dst,
881                                   ax::mojom::StringAttribute::kAutoComplete,
882                                   src.AutoComplete().Utf8());
883   }
884 
885   if (src.Action() != ax::mojom::DefaultActionVerb::kNone) {
886     dst->SetDefaultActionVerb(src.Action());
887   }
888 
889   blink::WebString display_style = src.ComputedStyleDisplay();
890   if (!display_style.IsEmpty()) {
891     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDisplay,
892                                   display_style.Utf8());
893   }
894 
895   if (src.KeyboardShortcut().length() &&
896       !dst->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) {
897     TruncateAndAddStringAttribute(dst,
898                                   ax::mojom::StringAttribute::kKeyShortcuts,
899                                   src.KeyboardShortcut().Utf8());
900   }
901 
902   if (!src.NextOnLine().IsDetached()) {
903     dst->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
904                          src.NextOnLine().AxID());
905   }
906 
907   if (!src.PreviousOnLine().IsDetached()) {
908     dst->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
909                          src.PreviousOnLine().AxID());
910   }
911 
912   if (!src.AriaActiveDescendant().IsDetached()) {
913     dst->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
914                          src.AriaActiveDescendant().AxID());
915   }
916 
917   if (!src.ErrorMessage().IsDetached()) {
918     dst->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId,
919                          src.ErrorMessage().AxID());
920   }
921 
922   if (ui::SupportsHierarchicalLevel(dst->role) && src.HierarchicalLevel()) {
923     dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
924                          src.HierarchicalLevel());
925   }
926 
927   if (src.CanvasHasFallbackContent())
928     dst->AddBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback, true);
929 
930   if (dst->role == ax::mojom::Role::kProgressIndicator ||
931       dst->role == ax::mojom::Role::kMeter ||
932       dst->role == ax::mojom::Role::kScrollBar ||
933       dst->role == ax::mojom::Role::kSlider ||
934       dst->role == ax::mojom::Role::kSpinButton ||
935       (dst->role == ax::mojom::Role::kSplitter &&
936        dst->HasState(ax::mojom::State::kFocusable))) {
937     float value;
938     if (src.ValueForRange(&value))
939       dst->AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, value);
940 
941     float max_value;
942     if (src.MaxValueForRange(&max_value)) {
943       dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange,
944                              max_value);
945     }
946 
947     float min_value;
948     if (src.MinValueForRange(&min_value)) {
949       dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange,
950                              min_value);
951     }
952 
953     float step_value;
954     if (src.StepValueForRange(&step_value)) {
955       dst->AddFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange,
956                              step_value);
957     }
958   }
959 
960   if (ui::IsDialog(dst->role)) {
961     dst->AddBoolAttribute(ax::mojom::BoolAttribute::kModal, src.IsModal());
962   }
963 
964   if (dst->role == ax::mojom::Role::kRootWebArea)
965     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kHtmlTag,
966                                   "#document");
967 
968   if (dst->role == ax::mojom::Role::kImage)
969     AddImageAnnotations(src, dst);
970 
971   // If a link or web area isn't otherwise labeled and contains exactly one
972   // image (searching only to a max depth of 2), and the link doesn't have
973   // accessible text from an attribute like aria-label, then annotate the
974   // link/web area with the image's annotation, too.
975   if ((ui::IsLink(dst->role) || ui::IsDocument(dst->role)) &&
976       dst->GetNameFrom() != ax::mojom::NameFrom::kAttribute) {
977     WebAXObject inner_image;
978     if (FindExactlyOneInnerImageInMaxDepthThree(src, &inner_image))
979       AddImageAnnotations(inner_image, dst);
980   }
981 
982   WebNode node = src.GetNode();
983   if (!node.IsNull() && node.IsElementNode()) {
984     WebElement element = node.To<WebElement>();
985     if (element.HasHTMLTagName("input") && element.HasAttribute("type")) {
986       TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kInputType,
987                                     element.GetAttribute("type").Utf8());
988     }
989   }
990 
991   // aria-dropeffect is deprecated in WAI-ARIA 1.1.
992   WebVector<ax::mojom::Dropeffect> src_dropeffects;
993   src.Dropeffects(src_dropeffects);
994   if (!src_dropeffects.empty()) {
995     for (auto&& dropeffect : src_dropeffects) {
996       dst->AddDropeffect(dropeffect);
997     }
998   }
999 }
1000 
SerializeElementAttributes(WebAXObject src,WebElement element,ui::AXNodeData * dst) const1001 void BlinkAXTreeSource::SerializeElementAttributes(WebAXObject src,
1002                                                    WebElement element,
1003                                                    ui::AXNodeData* dst) const {
1004   if (element.HasAttribute("class")) {
1005     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kClassName,
1006                                   element.GetAttribute("class").Utf8());
1007   }
1008 
1009   // ARIA role.
1010   if (element.HasAttribute("role")) {
1011     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole,
1012                                   element.GetAttribute("role").Utf8());
1013   } else {
1014     std::string role = GetEquivalentAriaRoleString(dst->role);
1015     if (!role.empty())
1016       TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kRole,
1017                                     role);
1018   }
1019 }
1020 
SerializeHTMLAttributes(WebAXObject src,WebElement element,ui::AXNodeData * dst) const1021 void BlinkAXTreeSource::SerializeHTMLAttributes(WebAXObject src,
1022                                                 WebElement element,
1023                                                 ui::AXNodeData* dst) const {
1024   // TODO(ctguil): The tagName in WebKit is lower cased but
1025   // HTMLElement::nodeName calls localNameUpper. Consider adding
1026   // a WebElement method that returns the original lower cased tagName.
1027   TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kHtmlTag,
1028                                 base::ToLowerASCII(element.TagName().Utf8()));
1029   for (unsigned i = 0; i < element.AttributeCount(); ++i) {
1030     std::string name = base::ToLowerASCII(element.AttributeLocalName(i).Utf8());
1031     if (name != "class") {  // class already in kClassName.
1032       std::string value = element.AttributeValue(i).Utf8();
1033       dst->html_attributes.push_back(std::make_pair(name, value));
1034     }
1035   }
1036 
1037 // TODO(nektar): Turn off kHTMLAccessibilityMode for automation and Mac
1038 // and remove ifdef.
1039 #if defined(OS_WIN) || defined(OS_CHROMEOS)
1040   if (dst->role == ax::mojom::Role::kMath && element.InnerHTML().length()) {
1041     TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kInnerHtml,
1042                                   element.InnerHTML().Utf8());
1043   }
1044 #endif
1045 }
1046 
GetMainDocument() const1047 blink::WebDocument BlinkAXTreeSource::GetMainDocument() const {
1048   CHECK(frozen_);
1049   return document_;
1050 }
1051 
ComputeRoot() const1052 WebAXObject BlinkAXTreeSource::ComputeRoot() const {
1053   if (!explicit_root_.IsNull())
1054     return explicit_root_;
1055 
1056   if (!render_frame_ || !render_frame_->GetWebFrame())
1057     return WebAXObject();
1058 
1059   WebDocument document = render_frame_->GetWebFrame()->GetDocument();
1060   if (!document.IsNull())
1061     return WebAXObject::FromWebDocument(document);
1062 
1063   return WebAXObject();
1064 }
1065 
TruncateAndAddStringAttribute(ui::AXNodeData * dst,ax::mojom::StringAttribute attribute,const std::string & value,uint32_t max_len) const1066 void BlinkAXTreeSource::TruncateAndAddStringAttribute(
1067     ui::AXNodeData* dst,
1068     ax::mojom::StringAttribute attribute,
1069     const std::string& value,
1070     uint32_t max_len) const {
1071   if (value.size() > max_len) {
1072     std::string truncated;
1073     base::TruncateUTF8ToByteSize(value, max_len, &truncated);
1074     dst->AddStringAttribute(attribute, truncated);
1075   } else {
1076     dst->AddStringAttribute(attribute, value);
1077   }
1078 }
1079 
AddImageAnnotations(blink::WebAXObject & src,ui::AXNodeData * dst) const1080 void BlinkAXTreeSource::AddImageAnnotations(blink::WebAXObject& src,
1081                                             ui::AXNodeData* dst) const {
1082   if (!base::FeatureList::IsEnabled(features::kExperimentalAccessibilityLabels))
1083     return;
1084 
1085   // Reject ignored objects
1086   if (src.AccessibilityIsIgnored()) {
1087     return;
1088   }
1089 
1090   // Reject images that are explicitly empty, or that have a
1091   // meaningful name already.
1092   ax::mojom::NameFrom name_from;
1093   blink::WebVector<WebAXObject> name_objects;
1094   blink::WebString web_name = src.GetName(name_from, name_objects);
1095 
1096   // If an image has a nonempty name, compute whether we should add an
1097   // image annotation or not.
1098   bool should_annotate_image_with_nonempty_name = false;
1099 
1100   // When visual debugging is enabled, the "title" attribute is set to a
1101   // string beginning with a "%". If the name comes from that string we
1102   // can ignore it, and treat the name as empty.
1103   if (image_annotation_debugging_ &&
1104       base::StartsWith(web_name.Utf8(), "%", base::CompareCase::SENSITIVE))
1105     should_annotate_image_with_nonempty_name = true;
1106 
1107   if (features::IsAugmentExistingImageLabelsEnabled()) {
1108     // If the name consists of mostly stopwords, we can add an image
1109     // annotations. See ax_image_stopwords.h for details.
1110     if (image_annotator_->ImageNameHasMostlyStopwords(web_name.Utf8()))
1111       should_annotate_image_with_nonempty_name = true;
1112   }
1113 
1114   // If the image's name is explicitly empty, or if it has a name (and
1115   // we're not treating the name as empty), then it's ineligible for
1116   // an annotation.
1117   if ((name_from == ax::mojom::NameFrom::kAttributeExplicitlyEmpty ||
1118        !web_name.IsEmpty()) &&
1119       !should_annotate_image_with_nonempty_name) {
1120     dst->SetImageAnnotationStatus(
1121         ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
1122     return;
1123   }
1124 
1125   // If the name of a document (root web area) starts with the filename,
1126   // it probably means the user opened an image in a new tab.
1127   // If so, we can treat the name as empty and give it an annotation.
1128   std::string dst_name =
1129       dst->GetStringAttribute(ax::mojom::StringAttribute::kName);
1130   if (dst->role == ax::mojom::Role::kRootWebArea) {
1131     std::string filename = GURL(document().Url()).ExtractFileName();
1132     if (base::StartsWith(dst_name, filename, base::CompareCase::SENSITIVE))
1133       should_annotate_image_with_nonempty_name = true;
1134   }
1135 
1136   // |dst| may be a document or link containing an image. Skip annotating
1137   // it if it already has text other than whitespace.
1138   if (!base::ContainsOnlyChars(dst_name, base::kWhitespaceASCII) &&
1139       !should_annotate_image_with_nonempty_name) {
1140     dst->SetImageAnnotationStatus(
1141         ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
1142     return;
1143   }
1144 
1145   // Skip images that are too small to label. This also catches
1146   // unloaded images where the size is unknown.
1147   WebAXObject offset_container;
1148   gfx::RectF bounds;
1149   SkMatrix44 container_transform;
1150   bool clips_children = false;
1151   src.GetRelativeBounds(offset_container, bounds, container_transform,
1152                         &clips_children);
1153   if (bounds.width() < kMinImageAnnotationWidth ||
1154       bounds.height() < kMinImageAnnotationHeight) {
1155     dst->SetImageAnnotationStatus(
1156         ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
1157     return;
1158   }
1159 
1160   // Skip images in documents which are not http, https, file and data schemes.
1161   GURL gurl = document().Url();
1162   if (!(gurl.SchemeIsHTTPOrHTTPS() || gurl.SchemeIsFile() ||
1163         gurl.SchemeIs(url::kDataScheme))) {
1164     dst->SetImageAnnotationStatus(
1165         ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme);
1166     return;
1167   }
1168 
1169   if (!image_annotator_) {
1170     if (!first_unlabeled_image_id_.has_value() ||
1171         first_unlabeled_image_id_.value() == src.AxID()) {
1172       dst->SetImageAnnotationStatus(
1173           ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
1174       first_unlabeled_image_id_ = src.AxID();
1175     } else {
1176       dst->SetImageAnnotationStatus(
1177           ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation);
1178     }
1179     return;
1180   }
1181 
1182   if (image_annotator_->HasAnnotationInCache(src)) {
1183     dst->AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
1184                             image_annotator_->GetImageAnnotation(src));
1185     dst->SetImageAnnotationStatus(
1186         image_annotator_->GetImageAnnotationStatus(src));
1187   } else if (image_annotator_->HasImageInCache(src)) {
1188     image_annotator_->OnImageUpdated(src);
1189     dst->SetImageAnnotationStatus(
1190         ax::mojom::ImageAnnotationStatus::kAnnotationPending);
1191   } else if (!image_annotator_->HasImageInCache(src)) {
1192     image_annotator_->OnImageAdded(src);
1193     dst->SetImageAnnotationStatus(
1194         ax::mojom::ImageAnnotationStatus::kAnnotationPending);
1195   }
1196 }
1197 
1198 }  // namespace content
1199