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