| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/pdf/renderer/pdf_accessibility_tree_builder.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/i18n/break_iterator.h" |
| #include "base/logging.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversion_utils.h" |
| #include "components/pdf/renderer/pdf_accessibility_tree_builder_heuristic.h" |
| #include "components/pdf/renderer/pdf_accessibility_tree_builder_structure.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "pdf/accessibility_structs.h" |
| #include "pdf/page_character_index.h" |
| #include "services/strings/grit/services_strings.h" |
| #include "third_party/blink/public/web/web_ax_object.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/accessibility/ax_enums.mojom-shared.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/strings/grit/auto_image_annotation_strings.h" |
| |
| namespace { |
| |
| ax::mojom::Role GetRoleForButtonType(chrome_pdf::ButtonType button_type) { |
| switch (button_type) { |
| case chrome_pdf::ButtonType::kRadioButton: |
| return ax::mojom::Role::kRadioButton; |
| case chrome_pdf::ButtonType::kCheckBox: |
| return ax::mojom::Role::kCheckBox; |
| case chrome_pdf::ButtonType::kPushButton: |
| return ax::mojom::Role::kButton; |
| } |
| } |
| |
| std::string GetTextRunCharsAsUTF8( |
| const chrome_pdf::AccessibilityTextRunInfo& text_run, |
| const std::vector<chrome_pdf::AccessibilityCharInfo>& chars, |
| int char_index) { |
| std::string chars_utf8; |
| for (uint32_t i = 0; i < text_run.len; ++i) { |
| base::WriteUnicodeCharacter( |
| static_cast<base_icu::UChar32>(chars[char_index + i].unicode_character), |
| &chars_utf8); |
| } |
| return chars_utf8; |
| } |
| |
| std::vector<int32_t> GetTextRunCharOffsets( |
| const chrome_pdf::AccessibilityTextRunInfo& text_run, |
| const std::vector<chrome_pdf::AccessibilityCharInfo>& chars, |
| int char_index) { |
| std::vector<int32_t> char_offsets(text_run.len); |
| double offset = 0.0; |
| for (uint32_t i = 0; i < text_run.len; ++i) { |
| offset += chars[char_index + i].char_width; |
| char_offsets[i] = floor(offset); |
| } |
| return char_offsets; |
| } |
| |
| bool IsTextRenderModeFill(const chrome_pdf::AccessibilityTextRenderMode& mode) { |
| switch (mode) { |
| case chrome_pdf::AccessibilityTextRenderMode::kFill: |
| case chrome_pdf::AccessibilityTextRenderMode::kFillStroke: |
| case chrome_pdf::AccessibilityTextRenderMode::kFillClip: |
| case chrome_pdf::AccessibilityTextRenderMode::kFillStrokeClip: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool IsTextRenderModeStroke( |
| const chrome_pdf::AccessibilityTextRenderMode& mode) { |
| switch (mode) { |
| case chrome_pdf::AccessibilityTextRenderMode::kStroke: |
| case chrome_pdf::AccessibilityTextRenderMode::kFillStroke: |
| case chrome_pdf::AccessibilityTextRenderMode::kStrokeClip: |
| case chrome_pdf::AccessibilityTextRenderMode::kFillStrokeClip: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| constexpr int kStandardBoldValue = 700; |
| constexpr int kMaxValidBoldValue = 900; |
| bool IsValidFontWeight(float font_weight) { |
| if (!features::IsPdfAccessibilityHeuristicEnhancementsEnabled()) { |
| return true; |
| } |
| return font_weight <= kMaxValidBoldValue && font_weight >= 0; |
| } |
| |
| } // namespace |
| |
| namespace pdf { |
| |
| PdfAccessibilityTreeBuilder::PdfAccessibilityTreeBuilder( |
| bool mark_headings_using_heuristic, |
| const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs, |
| const std::vector<chrome_pdf::AccessibilityCharInfo>& chars, |
| const chrome_pdf::AccessibilityPageObjects& page_objects, |
| const chrome_pdf::AccessibilityPageInfo& page_info, |
| uint32_t page_index, |
| const chrome_pdf::AccessibilityStructureElement* page_structure_tree, |
| ui::AXNodeData* root_node, |
| blink::WebAXObject* container_obj, |
| std::vector<std::unique_ptr<ui::AXNodeData>>* nodes, |
| std::map<int32_t, chrome_pdf::PageCharacterIndex>* |
| node_id_to_page_char_index, |
| std::map<int32_t, PdfAccessibilityTree::AnnotationInfo>* |
| node_id_to_annotation_info) |
| : mark_headings_using_heuristic_(mark_headings_using_heuristic), |
| text_runs_(text_runs), |
| chars_(chars), |
| links_(page_objects.links), |
| images_(page_objects.images), |
| highlights_(page_objects.highlights), |
| text_fields_(page_objects.form_fields.text_fields), |
| buttons_(page_objects.form_fields.buttons), |
| choice_fields_(page_objects.form_fields.choice_fields), |
| page_structure_tree_(page_structure_tree), |
| page_index_(page_index), |
| root_node_(root_node), |
| container_obj_(container_obj), |
| nodes_(nodes), |
| node_id_to_page_char_index_(node_id_to_page_char_index), |
| node_id_to_annotation_info_(node_id_to_annotation_info) { |
| page_node_ = CreateAndAppendNode(ax::mojom::Role::kRegion, |
| ax::mojom::Restriction::kReadOnly); |
| page_node_->AddStringAttribute( |
| ax::mojom::StringAttribute::kName, |
| l10n_util::GetPluralStringFUTF8(IDS_PDF_PAGE_INDEX, page_index + 1)); |
| page_node_->AddBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject, |
| true); |
| page_node_->relative_bounds.bounds = gfx::RectF(page_info.bounds); |
| root_node_->relative_bounds.bounds.Union(page_node_->relative_bounds.bounds); |
| root_node_->child_ids.push_back(page_node_->id); |
| |
| if (!text_runs.empty()) { |
| text_run_start_indices_.reserve(text_runs.size()); |
| text_run_start_indices_.push_back(0); |
| for (size_t i = 0; i < text_runs.size() - 1; ++i) { |
| text_run_start_indices_.push_back(text_run_start_indices_[i] + |
| text_runs[i].len); |
| } |
| } |
| } |
| |
| PdfAccessibilityTreeBuilder::~PdfAccessibilityTreeBuilder() = default; |
| |
| void PdfAccessibilityTreeBuilder::BuildPageTree() { |
| // Use structure tree mode if a structure tree exists. Structure tree mode |
| // preserves semantic structure from the PDF's tags and inserts any |
| // unassociated text runs as additional content. Fall back to heuristic mode |
| // only when no structure tree is available. |
| bool use_structure_mode = |
| page_structure_tree_ && |
| PdfAccessibilityTreeBuilderStructure::StructureTreeHasContent( |
| page_structure_tree_); |
| |
| VLOG(1) << "PDF page " << page_index_ << ": " |
| << (use_structure_mode ? "structure tree" : "heuristic") << " mode"; |
| |
| if (use_structure_mode) { |
| PdfAccessibilityTreeBuilderStructure(*this, page_structure_tree_) |
| .BuildPageTree(); |
| } else { |
| PdfAccessibilityTreeBuilderHeuristic(*this).BuildPageTree(); |
| } |
| } |
| |
| void PdfAccessibilityTreeBuilder::AddWordStartsAndEnds( |
| ui::AXNodeData* inline_text_box) { |
| std::u16string text = |
| inline_text_box->GetString16Attribute(ax::mojom::StringAttribute::kName); |
| base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); |
| if (!iter.Init()) { |
| return; |
| } |
| |
| std::vector<int32_t> word_starts; |
| std::vector<int32_t> word_ends; |
| while (iter.Advance()) { |
| if (iter.IsWord()) { |
| word_starts.push_back(iter.prev()); |
| word_ends.push_back(iter.pos()); |
| } |
| } |
| inline_text_box->AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, |
| word_starts); |
| inline_text_box->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, |
| word_ends); |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateAndAppendNode( |
| ax::mojom::Role role, |
| ax::mojom::Restriction restriction) { |
| std::unique_ptr<ui::AXNodeData> node = std::make_unique<ui::AXNodeData>(); |
| node->id = container_obj_->GenerateAXID(); |
| node->role = role; |
| node->SetRestriction(restriction); |
| |
| // All nodes have coordinates relative to the root node. |
| if (root_node_) { |
| node->relative_bounds.offset_container_id = root_node_->id; |
| } |
| |
| ui::AXNodeData* node_ptr = node.get(); |
| nodes_->push_back(std::move(node)); |
| |
| return node_ptr; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateStaticTextNode() { |
| ui::AXNodeData* static_text_node = CreateAndAppendNode( |
| ax::mojom::Role::kStaticText, ax::mojom::Restriction::kReadOnly); |
| static_text_node->SetNameFrom(ax::mojom::NameFrom::kContents); |
| return static_text_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateStaticTextNode( |
| const chrome_pdf::PageCharacterIndex& page_char_index) { |
| ui::AXNodeData* static_text_node = CreateStaticTextNode(); |
| node_id_to_page_char_index_->emplace(static_text_node->id, page_char_index); |
| return static_text_node; |
| } |
| |
| // static |
| bool PdfAccessibilityTreeBuilder::AreStylesEquivalent( |
| const chrome_pdf::AccessibilityTextStyleInfo& style1, |
| const chrome_pdf::AccessibilityTextStyleInfo& style2) { |
| return style1.is_italic == style2.is_italic && |
| style1.font_weight == style2.font_weight; |
| } |
| |
| // static |
| bool PdfAccessibilityTreeBuilder::IsBoldStyle( |
| const chrome_pdf::AccessibilityTextStyleInfo& style) { |
| return IsValidFontWeight(style.font_weight) && |
| style.font_weight >= kStandardBoldValue; |
| } |
| |
| // static |
| float PdfAccessibilityTreeBuilder::GetFontWeight( |
| const chrome_pdf::AccessibilityTextStyleInfo& style) { |
| return IsValidFontWeight(style.font_weight) ? style.font_weight : 0.0f; |
| } |
| |
| void PdfAccessibilityTreeBuilder::AddFontWeightAttributes( |
| const chrome_pdf::AccessibilityTextStyleInfo& style, |
| ui::AXNodeData* ax_node_data) { |
| if (IsBoldStyle(style)) { |
| ax_node_data->AddTextStyle(ax::mojom::TextStyle::kBold); |
| } |
| ax_node_data->AddFloatAttribute(ax::mojom::FloatAttribute::kFontWeight, |
| GetFontWeight(style)); |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateStaticTextNodeWithStyle( |
| const chrome_pdf::PageCharacterIndex& page_char_index, |
| const chrome_pdf::AccessibilityTextStyleInfo& style) { |
| ui::AXNodeData* static_text_node = CreateStaticTextNode(page_char_index); |
| if (style.is_italic) { |
| static_text_node->AddTextStyle(ax::mojom::TextStyle::kItalic); |
| } |
| AddFontWeightAttributes(style, static_text_node); |
| |
| return static_text_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateInlineTextBoxNode( |
| const chrome_pdf::AccessibilityTextRunInfo& text_run, |
| const chrome_pdf::PageCharacterIndex& page_char_index) { |
| ui::AXNodeData* inline_text_box_node = CreateAndAppendNode( |
| ax::mojom::Role::kInlineTextBox, ax::mojom::Restriction::kReadOnly); |
| inline_text_box_node->SetNameFrom(ax::mojom::NameFrom::kContents); |
| |
| std::string chars__utf8 = |
| GetTextRunCharsAsUTF8(text_run, *chars_, page_char_index.char_index); |
| inline_text_box_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| chars__utf8); |
| inline_text_box_node->AddIntAttribute( |
| ax::mojom::IntAttribute::kTextDirection, |
| static_cast<uint32_t>(text_run.direction)); |
| inline_text_box_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kFontFamily, text_run.style.font_name); |
| inline_text_box_node->AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, |
| text_run.style.font_size); |
| AddFontWeightAttributes(text_run.style, inline_text_box_node); |
| if (text_run.style.is_italic) { |
| inline_text_box_node->AddTextStyle(ax::mojom::TextStyle::kItalic); |
| } |
| if (IsTextRenderModeFill(text_run.style.render_mode)) { |
| inline_text_box_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, |
| text_run.style.fill_color); |
| } else if (IsTextRenderModeStroke(text_run.style.render_mode)) { |
| inline_text_box_node->AddIntAttribute(ax::mojom::IntAttribute::kColor, |
| text_run.style.stroke_color); |
| } |
| |
| inline_text_box_node->relative_bounds.bounds = |
| text_run.bounds + page_node_->relative_bounds.bounds.OffsetFromOrigin(); |
| std::vector<int32_t> char_offsets = |
| GetTextRunCharOffsets(text_run, *chars_, page_char_index.char_index); |
| inline_text_box_node->AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCharacterOffsets, char_offsets); |
| AddWordStartsAndEnds(inline_text_box_node); |
| node_id_to_page_char_index_->emplace(inline_text_box_node->id, |
| page_char_index); |
| return inline_text_box_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateLinkNode( |
| const chrome_pdf::AccessibilityLinkInfo& link) { |
| ui::AXNodeData* link_node = CreateAndAppendNode( |
| ax::mojom::Role::kLink, ax::mojom::Restriction::kReadOnly); |
| |
| link_node->AddStringAttribute(ax::mojom::StringAttribute::kUrl, link.url); |
| link_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| std::string()); |
| link_node->relative_bounds.bounds = link.bounds; |
| link_node->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kJump); |
| node_id_to_annotation_info_->emplace( |
| link_node->id, |
| PdfAccessibilityTree::AnnotationInfo(page_index_, link.index_in_page)); |
| |
| return link_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateImageNode( |
| const chrome_pdf::AccessibilityImageInfo& image) { |
| ui::AXNodeData* image_node = CreateAndAppendNode( |
| ax::mojom::Role::kImage, ax::mojom::Restriction::kReadOnly); |
| |
| if (image.alt_text.empty()) { |
| image_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kName, |
| l10n_util::GetStringUTF8(IDS_AX_UNLABELED_IMAGE_ROLE_DESCRIPTION)); |
| } else { |
| image_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| image.alt_text); |
| } |
| image_node->relative_bounds.bounds = image.bounds; |
| return image_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateHighlightNode( |
| const chrome_pdf::AccessibilityHighlightInfo& highlight) { |
| ui::AXNodeData* highlight_node = |
| CreateAndAppendNode(ax::mojom::Role::kPdfActionableHighlight, |
| ax::mojom::Restriction::kReadOnly); |
| |
| highlight_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kRoleDescription, |
| l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_HIGHLIGHT)); |
| highlight_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| std::string()); |
| highlight_node->relative_bounds.bounds = highlight.bounds; |
| highlight_node->AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, |
| highlight.color); |
| |
| return highlight_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreatePopupNoteNode( |
| const chrome_pdf::AccessibilityHighlightInfo& highlight) { |
| ui::AXNodeData* popup_note_node = CreateAndAppendNode( |
| ax::mojom::Role::kNote, ax::mojom::Restriction::kReadOnly); |
| |
| popup_note_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kRoleDescription, |
| l10n_util::GetStringUTF8(IDS_AX_ROLE_DESCRIPTION_PDF_POPUP_NOTE)); |
| popup_note_node->relative_bounds.bounds = highlight.bounds; |
| |
| ui::AXNodeData* static_popup_note_text_node = CreateAndAppendNode( |
| ax::mojom::Role::kStaticText, ax::mojom::Restriction::kReadOnly); |
| |
| static_popup_note_text_node->SetNameFrom(ax::mojom::NameFrom::kContents); |
| static_popup_note_text_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kName, highlight.note_text); |
| static_popup_note_text_node->relative_bounds.bounds = highlight.bounds; |
| |
| popup_note_node->child_ids.push_back(static_popup_note_text_node->id); |
| |
| return popup_note_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateTextFieldNode( |
| const chrome_pdf::AccessibilityTextFieldInfo& text_field) { |
| ax::mojom::Restriction restriction = text_field.is_read_only |
| ? ax::mojom::Restriction::kReadOnly |
| : ax::mojom::Restriction::kNone; |
| ui::AXNodeData* text_field_node = |
| CreateAndAppendNode(ax::mojom::Role::kTextField, restriction); |
| |
| text_field_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| text_field.name); |
| text_field_node->AddStringAttribute(ax::mojom::StringAttribute::kValue, |
| text_field.value); |
| text_field_node->AddState(ax::mojom::State::kFocusable); |
| if (text_field.is_required) { |
| text_field_node->AddState(ax::mojom::State::kRequired); |
| } |
| if (text_field.is_password) { |
| text_field_node->AddState(ax::mojom::State::kProtected); |
| } |
| text_field_node->relative_bounds.bounds = text_field.bounds; |
| return text_field_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateButtonNode( |
| const chrome_pdf::AccessibilityButtonInfo& button) { |
| ax::mojom::Restriction restriction = button.is_read_only |
| ? ax::mojom::Restriction::kReadOnly |
| : ax::mojom::Restriction::kNone; |
| ui::AXNodeData* button_node = |
| CreateAndAppendNode(GetRoleForButtonType(button.type), restriction); |
| button_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| button.name); |
| button_node->AddState(ax::mojom::State::kFocusable); |
| |
| if (button.type == chrome_pdf::ButtonType::kRadioButton || |
| button.type == chrome_pdf::ButtonType::kCheckBox) { |
| ax::mojom::CheckedState checkedState = button.is_checked |
| ? ax::mojom::CheckedState::kTrue |
| : ax::mojom::CheckedState::kNone; |
| button_node->SetCheckedState(checkedState); |
| button_node->AddStringAttribute(ax::mojom::StringAttribute::kValue, |
| button.value); |
| button_node->AddIntAttribute(ax::mojom::IntAttribute::kSetSize, |
| button.control_count); |
| button_node->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet, |
| button.control_index + 1); |
| button_node->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kCheck); |
| } else { |
| button_node->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kPress); |
| } |
| |
| button_node->relative_bounds.bounds = button.bounds; |
| return button_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateListboxOptionNode( |
| const chrome_pdf::AccessibilityChoiceFieldOptionInfo& choice_field_option, |
| ax::mojom::Restriction restriction) { |
| ui::AXNodeData* listbox_option_node = |
| CreateAndAppendNode(ax::mojom::Role::kListBoxOption, restriction); |
| |
| listbox_option_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| choice_field_option.name); |
| listbox_option_node->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, |
| choice_field_option.is_selected); |
| listbox_option_node->AddState(ax::mojom::State::kFocusable); |
| listbox_option_node->SetDefaultActionVerb( |
| ax::mojom::DefaultActionVerb::kSelect); |
| |
| return listbox_option_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateListboxNode( |
| const chrome_pdf::AccessibilityChoiceFieldInfo& choice_field, |
| ui::AXNodeData* control_node) { |
| ax::mojom::Restriction restriction = choice_field.is_read_only |
| ? ax::mojom::Restriction::kReadOnly |
| : ax::mojom::Restriction::kNone; |
| ui::AXNodeData* listbox_node = |
| CreateAndAppendNode(ax::mojom::Role::kListBox, restriction); |
| |
| if (choice_field.type != chrome_pdf::ChoiceFieldType::kComboBox) { |
| listbox_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| choice_field.name); |
| } |
| |
| ui::AXNodeData* first_selected_option = nullptr; |
| for (const chrome_pdf::AccessibilityChoiceFieldOptionInfo& option : |
| choice_field.options) { |
| ui::AXNodeData* listbox_option_node = |
| CreateListboxOptionNode(option, restriction); |
| if (!first_selected_option && listbox_option_node->GetBoolAttribute( |
| ax::mojom::BoolAttribute::kSelected)) { |
| first_selected_option = listbox_option_node; |
| } |
| // TODO(crbug.com/40661774): Add `listbox_option_node` specific bounds |
| // here. |
| listbox_option_node->relative_bounds.bounds = choice_field.bounds; |
| listbox_node->child_ids.push_back(listbox_option_node->id); |
| } |
| |
| if (control_node && first_selected_option) { |
| control_node->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId, |
| first_selected_option->id); |
| } |
| |
| if (choice_field.is_multi_select) { |
| listbox_node->AddState(ax::mojom::State::kMultiselectable); |
| } |
| listbox_node->AddState(ax::mojom::State::kFocusable); |
| listbox_node->relative_bounds.bounds = choice_field.bounds; |
| return listbox_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateComboboxInputNode( |
| const chrome_pdf::AccessibilityChoiceFieldInfo& choice_field, |
| ax::mojom::Restriction restriction) { |
| ax::mojom::Role input_role = choice_field.has_editable_text_box |
| ? ax::mojom::Role::kTextFieldWithComboBox |
| : ax::mojom::Role::kComboBoxMenuButton; |
| ui::AXNodeData* combobox_input_node = |
| CreateAndAppendNode(input_role, restriction); |
| combobox_input_node->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| choice_field.name); |
| for (const chrome_pdf::AccessibilityChoiceFieldOptionInfo& option : |
| choice_field.options) { |
| if (option.is_selected) { |
| combobox_input_node->AddStringAttribute( |
| ax::mojom::StringAttribute::kValue, option.name); |
| break; |
| } |
| } |
| |
| combobox_input_node->AddState(ax::mojom::State::kFocusable); |
| combobox_input_node->relative_bounds.bounds = choice_field.bounds; |
| if (input_role == ax::mojom::Role::kComboBoxMenuButton) { |
| combobox_input_node->SetDefaultActionVerb( |
| ax::mojom::DefaultActionVerb::kOpen); |
| } |
| |
| return combobox_input_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateComboboxNode( |
| const chrome_pdf::AccessibilityChoiceFieldInfo& choice_field) { |
| ax::mojom::Restriction restriction = choice_field.is_read_only |
| ? ax::mojom::Restriction::kReadOnly |
| : ax::mojom::Restriction::kNone; |
| ui::AXNodeData* combobox_node = |
| CreateAndAppendNode(ax::mojom::Role::kComboBoxGrouping, restriction); |
| ui::AXNodeData* input_element = |
| CreateComboboxInputNode(choice_field, restriction); |
| ui::AXNodeData* list_element = CreateListboxNode(choice_field, input_element); |
| input_element->AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds, |
| std::vector<int32_t>{list_element->id}); |
| combobox_node->child_ids.push_back(input_element->id); |
| combobox_node->child_ids.push_back(list_element->id); |
| combobox_node->AddState(ax::mojom::State::kFocusable); |
| combobox_node->relative_bounds.bounds = choice_field.bounds; |
| return combobox_node; |
| } |
| |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateChoiceFieldNode( |
| const chrome_pdf::AccessibilityChoiceFieldInfo& choice_field) { |
| switch (choice_field.type) { |
| case chrome_pdf::ChoiceFieldType::kListBox: |
| return CreateListboxNode(choice_field, /*control_node=*/nullptr); |
| case chrome_pdf::ChoiceFieldType::kComboBox: |
| return CreateComboboxNode(choice_field); |
| } |
| } |
| |
| #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) |
| ui::AXNodeData* PdfAccessibilityTreeBuilder::CreateOcrWrapperNode( |
| const gfx::PointF& position, |
| bool start) { |
| ui::AXNodeData* wrapper_node = CreateAndAppendNode( |
| start ? ax::mojom::Role::kBanner : ax::mojom::Role::kContentInfo, |
| ax::mojom::Restriction::kReadOnly); |
| wrapper_node->relative_bounds.bounds = gfx::RectF(position, gfx::SizeF(1, 1)); |
| |
| ui::AXNodeData* text_node = CreateStaticTextNode(); |
| text_node->SetNameChecked(l10n_util::GetStringUTF8( |
| start ? IDS_PDF_OCR_RESULT_BEGIN : IDS_PDF_OCR_RESULT_END)); |
| text_node->relative_bounds.bounds = wrapper_node->relative_bounds.bounds; |
| wrapper_node->child_ids.push_back(text_node->id); |
| return wrapper_node; |
| } |
| #endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) |
| |
| } // namespace pdf |