blob: 0de3f79e4b785866c614665ae385bc95dfad1481 [file] [log] [blame]
// 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 <queue>
#include <string>
#include "base/containers/fixed_flat_map.h"
#include "base/i18n/break_iterator.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "components/strings/grit/components_strings.h"
#include "pdf/accessibility_structs.h"
#include "pdf/page_character_index.h"
#include "pdf/pdf_features.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 {
// Don't try to apply font size thresholds to automatically identify headings
// if the median font size is not at least this many points.
const float kMinimumFontSize = 5.0f;
// Don't try to apply paragraph break thresholds to automatically identify
// paragraph breaks if the median line break is not at least this many points.
const float kMinimumLineSpacing = 5.0f;
// Ratio between the font size of one text run and the median on the page
// for that text run to be considered to be a heading instead of normal text.
const float kHeadingFontSizeRatio = 1.2f;
// Ratio between the line spacing between two lines and the median on the
// page for that line spacing to be considered a paragraph break.
const float kParagraphLineSpacingRatio = 1.2f;
// This class is used as part of our heuristic to determine which text runs live
// on the same "line". As we process runs, we keep a weighted average of the
// top and bottom coordinates of the line, and if a new run falls within that
// range (within a threshold) it is considered part of the line.
class LineHelper {
public:
explicit LineHelper(
const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs)
: text_runs_(text_runs) {
StartNewLine(0);
}
LineHelper(const LineHelper&) = delete;
LineHelper& operator=(const LineHelper&) = delete;
void StartNewLine(size_t current_index) {
DCHECK(current_index == 0 || current_index < text_runs_->size());
start_index_ = current_index;
accumulated_weight_top_ = 0.0f;
accumulated_weight_bottom_ = 0.0f;
accumulated_width_ = 0.0f;
}
void ProcessNextRun(size_t run_index) {
DCHECK_LT(run_index, text_runs_->size());
RemoveOldRunsUpTo(run_index);
AddRun((*text_runs_)[run_index].bounds);
}
bool IsRunOnSameLine(size_t run_index) const {
DCHECK_LT(run_index, text_runs_->size());
// Calculate new top/bottom bounds for our line.
if (accumulated_width_ == 0.0f) {
return false;
}
float line_top = accumulated_weight_top_ / accumulated_width_;
float line_bottom = accumulated_weight_bottom_ / accumulated_width_;
// Look at the next run, and determine how much it overlaps the line.
const auto& run_bounds = (*text_runs_)[run_index].bounds;
if (run_bounds.height() == 0.0f) {
return false;
}
float clamped_top = std::max(line_top, run_bounds.y());
float clamped_bottom =
std::min(line_bottom, run_bounds.y() + run_bounds.height());
if (clamped_bottom < clamped_top) {
return false;
}
float coverage = (clamped_bottom - clamped_top) / (run_bounds.height());
// See if it falls within the line (within our threshold).
constexpr float kLineCoverageThreshold = 0.25f;
return coverage > kLineCoverageThreshold;
}
private:
void AddRun(const gfx::RectF& run_bounds) {
float run_width = fabsf(run_bounds.width());
accumulated_width_ += run_width;
accumulated_weight_top_ += run_bounds.y() * run_width;
accumulated_weight_bottom_ +=
(run_bounds.y() + run_bounds.height()) * run_width;
}
void RemoveRun(const gfx::RectF& run_bounds) {
float run_width = fabsf(run_bounds.width());
accumulated_width_ -= run_width;
accumulated_weight_top_ -= run_bounds.y() * run_width;
accumulated_weight_bottom_ -=
(run_bounds.y() + run_bounds.height()) * run_width;
}
void RemoveOldRunsUpTo(size_t stop_index) {
// Remove older runs from the weighted average if we've exceeded the
// threshold distance from them. We remove them to prevent e.g. drop-caps
// from unduly influencing future lines.
constexpr float kBoxRemoveWidthThreshold = 3.0f;
while (start_index_ < stop_index &&
accumulated_width_ > (*text_runs_)[start_index_].bounds.width() *
kBoxRemoveWidthThreshold) {
const auto& old_bounds = (*text_runs_)[start_index_].bounds;
RemoveRun(old_bounds);
start_index_++;
}
}
const raw_ref<const std::vector<chrome_pdf::AccessibilityTextRunInfo>>
text_runs_;
size_t start_index_;
float accumulated_weight_top_;
float accumulated_weight_bottom_;
float accumulated_width_;
};
bool BreakParagraph(
const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs,
uint32_t text_run_index,
float paragraph_spacing_threshold) {
// Check to see if its also a new paragraph, i.e., if the distance between
// lines is greater than the threshold. If there's no threshold, that
// means there weren't enough lines to compute an accurate median, so
// we compare against the line size instead.
float line_spacing = fabsf(text_runs[text_run_index + 1].bounds.y() -
text_runs[text_run_index].bounds.y());
return ((paragraph_spacing_threshold > 0 &&
line_spacing > paragraph_spacing_threshold) ||
(paragraph_spacing_threshold == 0 &&
line_spacing > kParagraphLineSpacingRatio *
text_runs[text_run_index].bounds.height()));
}
void BuildStaticNode(ui::AXNodeData** static_text_node,
std::string* static_text) {
// If we're in the middle of building a static text node, finish it before
// moving on to the next object.
if (*static_text_node) {
(*static_text_node)
->AddStringAttribute(ax::mojom::StringAttribute::kName, (*static_text));
static_text->clear();
}
*static_text_node = nullptr;
}
void ComputeParagraphAndHeadingThresholds(
const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs,
float* out_heading_font_size_threshold,
float* out_paragraph_spacing_threshold) {
// Scan over the font sizes and line spacing within this page and
// set heuristic thresholds so that text larger than the median font
// size can be marked as a heading, and spacing larger than the median
// line spacing can be a paragraph break.
std::vector<float> font_sizes;
std::vector<float> line_spacings;
for (size_t i = 0; i < text_runs.size(); ++i) {
font_sizes.push_back(text_runs[i].style.font_size);
if (i > 0) {
const auto& cur = text_runs[i].bounds;
const auto& prev = text_runs[i - 1].bounds;
if (cur.y() > prev.y() + prev.height() / 2) {
line_spacings.push_back(cur.y() - prev.y());
}
}
}
if (font_sizes.size() > 2) {
std::sort(font_sizes.begin(), font_sizes.end());
float median_font_size = font_sizes[font_sizes.size() / 2];
if (median_font_size > kMinimumFontSize) {
*out_heading_font_size_threshold =
median_font_size * kHeadingFontSizeRatio;
}
}
if (line_spacings.size() > 4) {
std::sort(line_spacings.begin(), line_spacings.end());
float median_line_spacing = line_spacings[line_spacings.size() / 2];
if (median_line_spacing > kMinimumLineSpacing) {
*out_paragraph_spacing_threshold =
median_line_spacing * kParagraphLineSpacingRatio;
}
}
}
void ConnectPreviousAndNextOnLine(ui::AXNodeData* previous_on_line_node,
ui::AXNodeData* next_on_line_node) {
previous_on_line_node->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
next_on_line_node->id);
next_on_line_node->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
previous_on_line_node->id);
}
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;
}
template <typename T>
bool IsObjectInTextRun(const std::vector<T>& objects,
uint32_t object_index,
size_t text_run_index) {
return (object_index < objects.size() &&
objects[object_index].text_run_index <= text_run_index);
}
template <typename T>
bool IsObjectWithRangeInTextRun(const std::vector<T>& objects,
uint32_t object_index,
size_t text_run_index) {
return (object_index < objects.size() &&
objects[object_index].text_range.index <= text_run_index);
}
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;
}
}
size_t NormalizeTextRunIndex(uint32_t object_end_text_run_index,
size_t current_text_run_index) {
return std::max<size_t>(
object_end_text_run_index,
current_text_run_index ? current_text_run_index - 1 : 0);
}
// Please keep the below map as close as possible to the list defined in the PDF
// Specification, ISO 32000-1:2008, table 333.
ax::mojom::Role StructureElementTypeToAccessibilityRole(
const std::string& element_type) {
static constexpr auto kStructureElementTypeToAccessibilityRoleMap =
base::MakeFixedFlatMap<std::string_view, ax::mojom::Role>(
{{"Document", ax::mojom::Role::kDocument},
{"Part", ax::mojom::Role::kDocPart},
{"Art", ax::mojom::Role::kArticle},
{"Sect", ax::mojom::Role::kSection},
{"Div", ax::mojom::Role::kGenericContainer},
{"BlockQuote", ax::mojom::Role::kBlockquote},
{"Caption", ax::mojom::Role::kCaption},
{"TOC", ax::mojom::Role::kDocToc},
{"TOCI", ax::mojom::Role::kListItem},
{"Index", ax::mojom::Role::kDocIndex},
{"P", ax::mojom::Role::kParagraph},
{"H", ax::mojom::Role::kHeading},
{"H1", ax::mojom::Role::kHeading},
{"H2", ax::mojom::Role::kHeading},
{"H3", ax::mojom::Role::kHeading},
{"H4", ax::mojom::Role::kHeading},
{"H5", ax::mojom::Role::kHeading},
{"H6", ax::mojom::Role::kHeading},
{"L", ax::mojom::Role::kList},
{"LI", ax::mojom::Role::kListItem},
{"Lbl", ax::mojom::Role::kListMarker},
{"LBody", ax::mojom::Role::kNone}, // Presentational.
{"Table", ax::mojom::Role::kTable},
{"TR", ax::mojom::Role::kRow},
{"TH", ax::mojom::Role::kRowHeader},
{"THead", ax::mojom::Role::kRowGroup},
{"TBody", ax::mojom::Role::kRowGroup},
{"TFoot", ax::mojom::Role::kRowGroup},
{"TD", ax::mojom::Role::kCell},
{"Span", ax::mojom::Role::kStaticText},
{"Link", ax::mojom::Role::kLink},
{"Figure", ax::mojom::Role::kFigure},
{"Formula", ax::mojom::Role::kMath},
{"Form", ax::mojom::Role::kForm}});
if (auto iter =
kStructureElementTypeToAccessibilityRoleMap.find(element_type);
iter != kStructureElementTypeToAccessibilityRoleMap.end()) {
return iter->second;
}
// Return something that could at least make some sense, other than
// `kUnknown`.
return ax::mojom::Role::kParagraph;
}
std::optional<uint32_t> StructureElementTypeToHeadingLevel(
const std::string& element_type) {
if (StructureElementTypeToAccessibilityRole(element_type) ==
ax::mojom::Role::kHeading) {
if (element_type == "H" || element_type == "H1") {
return 1;
} else if (element_type == "H2") {
return 2;
} else if (element_type == "H3") {
return 3;
} else if (element_type == "H4") {
return 4;
} else if (element_type == "H5") {
return 5;
} else if (element_type == "H6") {
return 6;
}
}
return std::nullopt;
}
} // 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,
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_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() {
ComputeParagraphAndHeadingThresholds(*text_runs_,
&heading_font_size_threshold_,
&paragraph_spacing_threshold_);
ui::AXNodeData* block_node = nullptr;
ui::AXNodeData* static_text_node = nullptr;
ui::AXNodeData* previous_on_line_node = nullptr;
std::string static_text;
LineHelper line_helper(*text_runs_);
bool pdf_forms_enabled =
base::FeatureList::IsEnabled(chrome_pdf::features::kAccessiblePDFForm);
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
bool ocr_block = false;
bool has_ocr_text = false;
#endif
for (size_t text_run_index = 0; text_run_index < text_runs_->size();
++text_run_index) {
const chrome_pdf::AccessibilityTextRunInfo& text_run =
(*text_runs_)[text_run_index];
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
// OCR text should be marked by nodes before and after it.
bool ocr_block_start = text_run.is_searchified && !ocr_block;
bool ocr_block_end = !text_run.is_searchified && ocr_block;
if (ocr_block_start || ocr_block_end) {
// If already inside a block, end it.
// PDF searchifier only processes pages that have no text, hence OCR text
// is never added in the middle of a paragraph.
if (block_node) {
BuildStaticNode(&static_text_node, &static_text);
block_node = nullptr;
}
CHECK(ocr_block_start || text_run_index);
gfx::PointF position =
ocr_block_start
? text_run.bounds.origin()
: (*text_runs_)[text_run_index - 1].bounds.bottom_right();
page_node_->child_ids.push_back(
CreateOcrWrapperNode(position, ocr_block_start)->id);
ocr_block = ocr_block_start;
has_ocr_text = true;
}
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
// If we don't have a block level node, create one.
if (!block_node) {
block_node =
CreateBlockLevelNode(text_run.tag_type, text_run.style.font_size);
page_node_->child_ids.push_back(block_node->id);
}
// If the `text_run_index` is less than or equal to the link's
// `text_run_index`, then push the link node in the block.
if (IsObjectWithRangeInTextRun(*links_, current_link_index_,
text_run_index)) {
BuildStaticNode(&static_text_node, &static_text);
const chrome_pdf::AccessibilityLinkInfo& link =
(*links_)[current_link_index_++];
AddLinkToParaNode(link, block_node, &previous_on_line_node,
&text_run_index);
if (link.text_range.count == 0) {
continue;
}
} else if (IsObjectInTextRun(*images_, current_image_index_,
text_run_index)) {
BuildStaticNode(&static_text_node, &static_text);
AddImageToParaNode((*images_)[current_image_index_++], block_node,
&text_run_index);
continue;
} else if (IsObjectWithRangeInTextRun(
*highlights_, current_highlight_index_, text_run_index)) {
BuildStaticNode(&static_text_node, &static_text);
AddHighlightToParaNode((*highlights_)[current_highlight_index_++],
block_node, &previous_on_line_node,
&text_run_index);
} else if (IsObjectInTextRun(*text_fields_, current_text_field_index_,
text_run_index) &&
pdf_forms_enabled) {
BuildStaticNode(&static_text_node, &static_text);
AddTextFieldToParaNode((*text_fields_)[current_text_field_index_++],
block_node, &text_run_index);
continue;
} else if (IsObjectInTextRun(*buttons_, current_button_index_,
text_run_index) &&
pdf_forms_enabled) {
BuildStaticNode(&static_text_node, &static_text);
AddButtonToParaNode((*buttons_)[current_button_index_++], block_node,
&text_run_index);
continue;
} else if (IsObjectInTextRun(*choice_fields_, current_choice_field_index_,
text_run_index) &&
pdf_forms_enabled) {
BuildStaticNode(&static_text_node, &static_text);
AddChoiceFieldToParaNode((*choice_fields_)[current_choice_field_index_++],
block_node, &text_run_index);
continue;
} else {
chrome_pdf::PageCharacterIndex page_char_index = {
page_index_, text_run_start_indices_[text_run_index]};
// This node is for the text inside the block, it includes the text of all
// of the text runs.
if (!static_text_node) {
static_text_node = CreateStaticTextNode(page_char_index);
block_node->child_ids.push_back(static_text_node->id);
}
// Add this text run to the current static text node.
ui::AXNodeData* inline_text_box_node =
CreateInlineTextBoxNode(text_run, page_char_index);
static_text_node->child_ids.push_back(inline_text_box_node->id);
static_text += inline_text_box_node->GetStringAttribute(
ax::mojom::StringAttribute::kName);
block_node->relative_bounds.bounds.Union(
inline_text_box_node->relative_bounds.bounds);
static_text_node->relative_bounds.bounds.Union(
inline_text_box_node->relative_bounds.bounds);
if (previous_on_line_node) {
ConnectPreviousAndNextOnLine(previous_on_line_node,
inline_text_box_node);
} else {
line_helper.StartNewLine(text_run_index);
}
line_helper.ProcessNextRun(text_run_index);
if (text_run_index < text_runs_->size() - 1) {
if (line_helper.IsRunOnSameLine(text_run_index + 1)) {
// The next run is on the same line.
previous_on_line_node = inline_text_box_node;
} else {
// The next run is on a new line.
previous_on_line_node = nullptr;
}
}
}
if (text_run_index == text_runs_->size() - 1) {
BuildStaticNode(&static_text_node, &static_text);
break;
}
if (!previous_on_line_node) {
if (BreakParagraph(*text_runs_, text_run_index,
paragraph_spacing_threshold_)) {
BuildStaticNode(&static_text_node, &static_text);
block_node = nullptr;
}
}
}
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
// Add the wrapper node if still in OCR block and text runs finish.
if (ocr_block) {
page_node_->child_ids.push_back(
CreateOcrWrapperNode(text_runs_->back().bounds.bottom_right(),
/*start=*/false)
->id);
}
AddRemainingAnnotations(block_node, has_ocr_text);
#else
AddRemainingAnnotations(block_node);
#endif
}
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::CreateBlockLevelNode(
const std::string& text_run_type,
float font_size) {
ui::AXNodeData* block_node = CreateAndAppendNode(
StructureElementTypeToAccessibilityRole(text_run_type),
ax::mojom::Restriction::kReadOnly);
block_node->AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
if (std::optional<uint32_t> level =
StructureElementTypeToHeadingLevel(text_run_type);
level) {
block_node->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
*level);
// TODO(crbug.com/40707542): Set the HTML tag to "h*" by creating a helper
// in `AXEnumUtils`.
}
if (mark_headings_using_heuristic_ && heading_font_size_threshold_ > 0 &&
font_size > heading_font_size_threshold_) {
block_node->role = ax::mojom::Role::kHeading;
block_node->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 2);
block_node->AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h2");
}
return block_node;
}
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;
}
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);
inline_text_box_node->AddFloatAttribute(
ax::mojom::FloatAttribute::kFontWeight, text_run.style.font_weight);
if (text_run.style.is_italic) {
inline_text_box_node->AddTextStyle(ax::mojom::TextStyle::kItalic);
}
if (text_run.style.is_bold) {
inline_text_box_node->AddTextStyle(ax::mojom::TextStyle::kBold);
}
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)
void PdfAccessibilityTreeBuilder::AddTextToAXNode(
size_t start_text_run_index,
uint32_t end_text_run_index,
ui::AXNodeData* ax_node,
ui::AXNodeData** previous_on_line_node) {
chrome_pdf::PageCharacterIndex page_char_index = {
page_index_, text_run_start_indices_[start_text_run_index]};
ui::AXNodeData* ax_static_text_node = CreateStaticTextNode(page_char_index);
ax_node->child_ids.push_back(ax_static_text_node->id);
// Accumulate the text of the node.
std::string ax_name;
LineHelper line_helper(*text_runs_);
for (size_t text_run_index = start_text_run_index;
text_run_index <= end_text_run_index; ++text_run_index) {
const chrome_pdf::AccessibilityTextRunInfo& text_run =
(*text_runs_)[text_run_index];
page_char_index.char_index = text_run_start_indices_[text_run_index];
// Add this text run to the current static text node.
ui::AXNodeData* inline_text_box_node =
CreateInlineTextBoxNode(text_run, page_char_index);
ax_static_text_node->child_ids.push_back(inline_text_box_node->id);
ax_static_text_node->relative_bounds.bounds.Union(
inline_text_box_node->relative_bounds.bounds);
ax_name += inline_text_box_node->GetStringAttribute(
ax::mojom::StringAttribute::kName);
if (*previous_on_line_node) {
ConnectPreviousAndNextOnLine(*previous_on_line_node,
inline_text_box_node);
} else {
line_helper.StartNewLine(text_run_index);
}
line_helper.ProcessNextRun(text_run_index);
if (text_run_index < text_runs_->size() - 1) {
if (line_helper.IsRunOnSameLine(text_run_index + 1)) {
// The next run is on the same line.
*previous_on_line_node = inline_text_box_node;
} else {
// The next run is on a new line.
*previous_on_line_node = nullptr;
}
}
}
ax_node->AddStringAttribute(ax::mojom::StringAttribute::kName, ax_name);
ax_static_text_node->AddStringAttribute(ax::mojom::StringAttribute::kName,
ax_name);
}
void PdfAccessibilityTreeBuilder::AddTextToObjectNode(
size_t object_text_run_index,
uint32_t object_text_run_count,
ui::AXNodeData* object_node,
ui::AXNodeData* para_node,
ui::AXNodeData** previous_on_line_node,
size_t* text_run_index) {
// Annotation objects can overlap in PDF. There can be two overlapping
// scenarios: Partial overlap and Complete overlap.
// Partial overlap
//
// Link A starts Link B starts Link A ends Link B ends
// |a1 |b1 |a2 |b2
// -----------------------------------------------------------------------
// Text
//
// Complete overlap
// Link A starts Link B starts Link B ends Link A ends
// |a1 |b1 |b2 |a2
// -----------------------------------------------------------------------
// Text
//
// For overlapping annotations, both annotations would store the full
// text data and nothing will get truncated. For partial overlap, link `A`
// would contain text between a1 and a2 while link `B` would contain text
// between b1 and b2. For complete overlap as well, link `A` would contain
// text between a1 and a2 and link `B` would contain text between b1 and
// b2. The links would appear in the tree in the order of which they are
// present. In the tree for both overlapping scenarios, link `A` would
// appear first in the tree and link `B` after it.
// If `object_text_run_count` > 0, then the object is part of the page text.
// Make the text runs contained by the object children of the object node.
size_t end_text_run_index = object_text_run_index + object_text_run_count;
uint32_t object_end_text_run_index =
std::min(end_text_run_index, text_runs_->size()) - 1;
AddTextToAXNode(object_text_run_index, object_end_text_run_index, object_node,
previous_on_line_node);
para_node->relative_bounds.bounds.Union(object_node->relative_bounds.bounds);
*text_run_index =
NormalizeTextRunIndex(object_end_text_run_index, *text_run_index);
}
void PdfAccessibilityTreeBuilder::AddLinkToParaNode(
const chrome_pdf::AccessibilityLinkInfo& link,
ui::AXNodeData* para_node,
ui::AXNodeData** previous_on_line_node,
size_t* text_run_index) {
ui::AXNodeData* link_node = CreateLinkNode(link);
para_node->child_ids.push_back(link_node->id);
// If `link.text_range.count` == 0, then the link is not part of the page
// text. Push it ahead of the current text run.
if (link.text_range.count == 0) {
--(*text_run_index);
return;
}
// Make the text runs contained by the link children of
// the link node.
AddTextToObjectNode(link.text_range.index, link.text_range.count, link_node,
para_node, previous_on_line_node, text_run_index);
}
void PdfAccessibilityTreeBuilder::AddImageToParaNode(
const chrome_pdf::AccessibilityImageInfo& image,
ui::AXNodeData* para_node,
size_t* text_run_index) {
// If the `text_run_index` is less than or equal to the image's text run
// index, then push the image ahead of the current text run.
ui::AXNodeData* image_node = CreateImageNode(image);
para_node->child_ids.push_back(image_node->id);
--(*text_run_index);
}
void PdfAccessibilityTreeBuilder::AddHighlightToParaNode(
const chrome_pdf::AccessibilityHighlightInfo& highlight,
ui::AXNodeData* para_node,
ui::AXNodeData** previous_on_line_node,
size_t* text_run_index) {
ui::AXNodeData* highlight_node = CreateHighlightNode(highlight);
para_node->child_ids.push_back(highlight_node->id);
// Make the text runs contained by the highlight children of
// the highlight node.
AddTextToObjectNode(highlight.text_range.index, highlight.text_range.count,
highlight_node, para_node, previous_on_line_node,
text_run_index);
if (!highlight.note_text.empty()) {
ui::AXNodeData* popup_note_node = CreatePopupNoteNode(highlight);
highlight_node->child_ids.push_back(popup_note_node->id);
}
}
void PdfAccessibilityTreeBuilder::AddTextFieldToParaNode(
const chrome_pdf::AccessibilityTextFieldInfo& text_field,
ui::AXNodeData* para_node,
size_t* text_run_index) {
// If the `text_run_index` is less than or equal to the text_field's text
// run index, then push the text_field ahead of the current text run.
ui::AXNodeData* text_field_node = CreateTextFieldNode(text_field);
para_node->child_ids.push_back(text_field_node->id);
--(*text_run_index);
}
void PdfAccessibilityTreeBuilder::AddButtonToParaNode(
const chrome_pdf::AccessibilityButtonInfo& button,
ui::AXNodeData* para_node,
size_t* text_run_index) {
// If the `text_run_index` is less than or equal to the button's text
// run index, then push the button ahead of the current text run.
ui::AXNodeData* button_node = CreateButtonNode(button);
para_node->child_ids.push_back(button_node->id);
--(*text_run_index);
}
void PdfAccessibilityTreeBuilder::AddChoiceFieldToParaNode(
const chrome_pdf::AccessibilityChoiceFieldInfo& choice_field,
ui::AXNodeData* para_node,
size_t* text_run_index) {
// If the `text_run_index` is less than or equal to the choice_field's text
// run index, then push the choice_field ahead of the current text run.
ui::AXNodeData* choice_field_node = CreateChoiceFieldNode(choice_field);
para_node->child_ids.push_back(choice_field_node->id);
--(*text_run_index);
}
void PdfAccessibilityTreeBuilder::AddRemainingAnnotations(
ui::AXNodeData* para_node
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
,
bool ocr_applied
#endif
) {
// If we don't have additional links, images or form fields to insert in the
// tree, then return.
if (current_link_index_ >= links_->size() &&
current_image_index_ >= images_->size() &&
current_text_field_index_ >= text_fields_->size() &&
current_button_index_ >= buttons_->size() &&
current_choice_field_index_ >= choice_fields_->size()) {
return;
}
// If we don't have a paragraph node, create a new one.
if (!para_node) {
para_node = CreateAndAppendNode(ax::mojom::Role::kParagraph,
ax::mojom::Restriction::kReadOnly);
page_node_->child_ids.push_back(para_node->id);
}
// Push all the links not anchored to any text run to the last paragraph.
for (size_t i = current_link_index_; i < links_->size(); i++) {
ui::AXNodeData* link_node = CreateLinkNode((*links_)[i]);
para_node->child_ids.push_back(link_node->id);
}
// Push all the images not anchored to any text run to the last paragraph
// unless OCR has run. PDF Searchify either OCRs all images on a page, or none
// of them.
bool push_remaining_images = true;
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
push_remaining_images = !ocr_applied;
#endif
if (push_remaining_images) {
for (size_t i = current_image_index_; i < images_->size(); i++) {
const chrome_pdf::AccessibilityImageInfo& image_info = (*images_)[i];
ui::AXNodeData* image_node = CreateImageNode(image_info);
para_node->child_ids.push_back(image_node->id);
}
}
if (base::FeatureList::IsEnabled(chrome_pdf::features::kAccessiblePDFForm)) {
// Push all the text fields not anchored to any text run to the last
// paragraph.
for (size_t i = current_text_field_index_; i < text_fields_->size(); i++) {
ui::AXNodeData* text_field_node = CreateTextFieldNode((*text_fields_)[i]);
para_node->child_ids.push_back(text_field_node->id);
}
// Push all the buttons not anchored to any text run to the last
// paragraph.
for (size_t i = current_button_index_; i < buttons_->size(); i++) {
ui::AXNodeData* button_node = CreateButtonNode((*buttons_)[i]);
para_node->child_ids.push_back(button_node->id);
}
// Push all the choice fields not anchored to any text run to the last
// paragraph.
for (size_t i = current_choice_field_index_; i < choice_fields_->size();
i++) {
ui::AXNodeData* choice_field_node =
CreateChoiceFieldNode((*choice_fields_)[i]);
para_node->child_ids.push_back(choice_field_node->id);
}
}
}
} // namespace pdf