[PDF OCR] Add a browser test for the function that generates a transform
Add a browser test for the function that generates a transform for
ui::AXRelativeBounds. This function was added to fix the bounding box
misalignment issue in crrev.com/c/4363403.
AX-Relnotes: n/a.
Bug: 1423810
Change-Id: I4e4a76675b9166ce77690eaa7d4e2641d63662f5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4371269
Reviewed-by: David Tseng <dtseng@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Kyungjun Lee <kyungjunlee@google.com>
Cr-Commit-Position: refs/heads/main@{#1126372}
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 02d952e3..ba91007 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -911,6 +911,13 @@
]
}
+ if (enable_screen_ai_service) {
+ deps += [
+ "//components/services/screen_ai",
+ "//skia",
+ ]
+ }
+
if (is_android) {
sources += [
"browser_ui/client_certificate/android/ssl_client_certificate_request_browsertest.cc",
diff --git a/components/pdf/renderer/pdf_accessibility_tree.h b/components/pdf/renderer/pdf_accessibility_tree.h
index 2f854b62..678f50b 100644
--- a/components/pdf/renderer/pdf_accessibility_tree.h
+++ b/components/pdf/renderer/pdf_accessibility_tree.h
@@ -141,6 +141,8 @@
// request to the Screen AI library. The number of remaining OCR requests
// will decrement by one in `OnOcrDataReceived()`.
void IncrementNumberOfRemainingOcrRequests();
+
+ const ui::AXTree& tree_for_testing() const { return tree_; }
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
bool ShowContextMenu();
diff --git a/components/pdf/renderer/pdf_accessibility_tree_browsertest.cc b/components/pdf/renderer/pdf_accessibility_tree_browsertest.cc
index 900a802..3115a0c 100644
--- a/components/pdf/renderer/pdf_accessibility_tree_browsertest.cc
+++ b/components/pdf/renderer/pdf_accessibility_tree_browsertest.cc
@@ -30,6 +30,15 @@
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
+#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+#include "components/services/screen_ai/screen_ai_ax_tree_serializer.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/accessibility/accessibility_features.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_tree_id.h"
+#include "ui/gfx/geometry/transform.h"
+#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+
namespace pdf {
namespace {
@@ -83,6 +92,37 @@
return (a << 24) | (r << 16) | (g << 8) | b;
}
+#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+ui::AXTreeUpdate CreateMockOCRResult(const gfx::RectF& image_bounds,
+ const gfx::RectF& text_bounds1,
+ const gfx::RectF& text_bounds2) {
+ ui::AXNodeData page_node;
+ page_node.role = ax::mojom::Role::kRegion;
+ page_node.id = 1001;
+ page_node.relative_bounds.bounds = image_bounds;
+
+ ui::AXNodeData text_node1;
+ text_node1.role = ax::mojom::Role::kStaticText;
+ text_node1.id = 1002;
+ text_node1.relative_bounds.bounds = text_bounds1;
+ page_node.child_ids.push_back(text_node1.id);
+
+ ui::AXNodeData text_node2;
+ text_node2.role = ax::mojom::Role::kStaticText;
+ text_node2.id = 1003;
+ text_node2.relative_bounds.bounds = text_bounds2;
+ page_node.child_ids.push_back(text_node2.id);
+
+ ui::AXTreeUpdate initial_state;
+ initial_state.root_id = page_node.id;
+ initial_state.nodes = {page_node, text_node1, text_node2};
+ initial_state.has_tree_data = true;
+ initial_state.tree_data.title = "OCR results";
+
+ return initial_state;
+}
+#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+
// This class overrides PdfAccessibilityActionHandler to record received
// action data when tests make an accessibility action call.
class TestPdfAccessibilityActionHandler
@@ -2102,4 +2142,190 @@
}
}
+#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+TEST_F(PdfAccessibilityTreeTest, TestTransformFromOnOcrDataReceived) {
+ // Enable feature flag.
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(::features::kPdfOcr);
+
+ // Assume `image` contains some text that will be extracted by OCR. `image`
+ // will be passed to the function that creates a transform, which will be
+ // then applied to the text paragraphs extracted by OCR.
+ chrome_pdf::AccessibilityImageInfo image;
+ constexpr float kImageWidth = 200.0f;
+ constexpr float kImageHeight = 200.0f;
+ image.bounds = gfx::RectF(0.0f, 0.0f, kImageWidth, kImageHeight);
+ // Simulate that the width and height of `image` got shrunk by 80% in
+ // `image_data`.
+ constexpr float kScaleFactor = 0.8f;
+ image.image_data.allocN32Pixels(static_cast<int>(kImageWidth * kScaleFactor),
+ static_cast<int>(kImageHeight * kScaleFactor),
+ /*isOpaque=*/false);
+ page_objects_.images.push_back(image);
+
+ page_info_.text_run_count = text_runs_.size();
+ page_info_.char_count = chars_.size();
+
+ content::RenderFrame* render_frame = GetMainRenderFrame();
+ ASSERT_TRUE(render_frame);
+ render_frame->SetAccessibilityModeForTest(ui::AXMode::kWebContents);
+ ASSERT_TRUE(render_frame->GetRenderAccessibility());
+
+ TestPdfAccessibilityActionHandler action_handler;
+ PdfAccessibilityTree pdf_accessibility_tree(render_frame, &action_handler);
+
+ pdf_accessibility_tree.SetAccessibilityViewportInfo(viewport_info_);
+ pdf_accessibility_tree.SetAccessibilityDocInfo(doc_info_);
+ pdf_accessibility_tree.SetAccessibilityPageInfo(page_info_, text_runs_,
+ chars_, page_objects_);
+ WaitForThreadTasks();
+
+ // TODO(crbug.com/1423810): Convert these in-line comments into EXPECT() with
+ // ToString() output from AXTree. To do this in a more stable way, we need to
+ // use AXTreeFormatter and move the whole or some part of the following file
+ // (content/public/browser/ax_inspect_factory.h) into content/public/renderer
+ // or content/public/common along with its deps.
+ /*
+ * Expected PDF accessibility tree structure (with PDF OCR feature flag)
+ * Document
+ * ++ Status
+ * ++ Region
+ * ++++ Paragraph
+ * ++++++ image
+ */
+
+ const ui::AXTree& ax_tree_in_pdf = pdf_accessibility_tree.tree_for_testing();
+ ui::AXNode* root_node = ax_tree_in_pdf.root();
+ ASSERT_TRUE(root_node);
+ EXPECT_EQ(ax::mojom::Role::kPdfRoot, root_node->GetRole());
+ ASSERT_EQ(2u, root_node->children().size());
+
+ ui::AXNode* status_node = root_node->children()[0];
+ ASSERT_TRUE(status_node);
+ EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
+ ASSERT_EQ(0u, status_node->children().size());
+
+ ui::AXNode* page_node = root_node->children()[1];
+ ASSERT_TRUE(page_node);
+ EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
+ ASSERT_EQ(1u, page_node->children().size());
+
+ ui::AXNode* paragraph_node = page_node->children()[0];
+ ASSERT_TRUE(paragraph_node);
+ EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
+ ASSERT_EQ(1u, paragraph_node->children().size());
+
+ ui::AXNode* image_node = paragraph_node->children()[0];
+ ASSERT_TRUE(image_node);
+ EXPECT_EQ(ax::mojom::Role::kImage, image_node->GetRole());
+ ASSERT_EQ(0u, image_node->children().size());
+ EXPECT_EQ(image.bounds, image_node->data().relative_bounds.bounds);
+
+ // Simulate creating a child tree using OCR results.
+ pdf_accessibility_tree.IncrementNumberOfRemainingOcrRequests();
+ // Text bounds before applying the transform.
+ constexpr gfx::RectF kTextBoundsBeforeTransform1 = {{8.0f, 8.0f},
+ {80.0f, 24.0f}};
+ constexpr gfx::RectF kTextBoundsBeforeTransform2 = {{16.0f, 88.0f},
+ {40.0f, 56.0f}};
+ ui::AXTreeUpdate initial_state = CreateMockOCRResult(
+ image.bounds, kTextBoundsBeforeTransform1, kTextBoundsBeforeTransform2);
+ screen_ai::ScreenAIAXTreeSerializer serializer(
+ render_frame->GetRenderAccessibility()->GetTreeIDForPluginHost(),
+ std::move(initial_state.nodes));
+ WaitForThreadTasks();
+
+ const ui::AXTree* child_tree = serializer.tree_for_testing();
+ ASSERT_TRUE(child_tree);
+ EXPECT_NE(child_tree->GetAXTreeID(), ui::AXTreeIDUnknown());
+ EXPECT_NE(child_tree->GetAXTreeID(), ax_tree_in_pdf.GetAXTreeID());
+ pdf_accessibility_tree.OnOcrDataReceived(
+ image_node->id(), image, paragraph_node->id(), child_tree->GetAXTreeID());
+ WaitForThreadTasks();
+
+ // TODO(crbug.com/1423810): Convert these in-line comments into EXPECT() with
+ // ToString() output from AXTree. To do this in a more stable way, we need to
+ // use AXTreeFormatter and move the whole or some part of the following file
+ // (content/public/browser/ax_inspect_factory.h) into content/public/renderer
+ // or content/public/common along with its deps.
+ /*
+ * Expected PDF accessibility tree structure (after running OCR)
+ * Document
+ * ++ Status
+ * ++ Region
+ * ++++ Paragraph
+ * ++++++ Region (child tree)
+ * ++++++++ Static Text
+ * ++++++++ Static Text
+ */
+
+ root_node = ax_tree_in_pdf.root();
+ ASSERT_TRUE(root_node);
+ ASSERT_EQ(2u, root_node->children().size());
+
+ status_node = root_node->children()[0];
+ ASSERT_TRUE(status_node);
+ EXPECT_EQ(ax::mojom::Role::kStatus, status_node->GetRole());
+
+ page_node = root_node->children()[1];
+ ASSERT_TRUE(page_node);
+ EXPECT_EQ(ax::mojom::Role::kRegion, page_node->GetRole());
+ ASSERT_EQ(1u, page_node->children().size());
+
+ paragraph_node = page_node->children()[0];
+ ASSERT_TRUE(paragraph_node);
+ EXPECT_EQ(ax::mojom::Role::kParagraph, paragraph_node->GetRole());
+ ASSERT_EQ(1u, paragraph_node->children().size());
+
+ ui::AXNode* host_node = paragraph_node->children()[0];
+ ASSERT_TRUE(host_node);
+ EXPECT_TRUE(
+ host_node->HasStringAttribute(ax::mojom::StringAttribute::kChildTreeId));
+ ui::AXTreeID child_tree_id = ui::AXTreeID::FromString(
+ host_node->GetStringAttribute(ax::mojom::StringAttribute::kChildTreeId));
+ ASSERT_EQ(child_tree_id, child_tree->GetAXTreeID());
+
+ ASSERT_TRUE(host_node->data().relative_bounds.transform.get());
+
+ // Expected text bounds after applying the transform. These numbers are
+ // expected to be kTextBoundsBeforeTransform * 1 / kScaleFactor.
+ constexpr gfx::RectF kExpectedTextBoundRelativeToTreeBounds1 = {
+ {10.0f, 10.0f}, {100.0f, 30.0f}};
+ constexpr gfx::RectF kExpectedTextBoundRelativeToTreeBounds2 = {
+ {20.0f, 110.0f}, {50.0f, 70.0f}};
+
+ // Check the child tree.
+ ui::AXNode* child_tree_host_node = child_tree->root();
+ ASSERT_TRUE(child_tree_host_node);
+ EXPECT_EQ(ax::mojom::Role::kRegion, child_tree_host_node->GetRole());
+ ASSERT_EQ(2u, child_tree_host_node->children().size());
+
+ ui::AXNode* child_tree_node = child_tree_host_node->children()[0];
+ ASSERT_TRUE(child_tree_node);
+ EXPECT_EQ(ax::mojom::Role::kStaticText, child_tree_node->GetRole());
+ gfx::RectF bounds = child_tree_node->data().relative_bounds.bounds;
+ CompareRect(kTextBoundsBeforeTransform1, bounds);
+ // After applying the transform via RelativeToTreeBounds.
+ bounds = child_tree->RelativeToTreeBounds(child_tree_node, bounds,
+ /*offscreen=*/nullptr,
+ /*clip_bounds=*/true,
+ /*skip_container_offset=*/true);
+ bounds = ax_tree_in_pdf.RelativeToTreeBounds(host_node, bounds);
+ CompareRect(kExpectedTextBoundRelativeToTreeBounds1, bounds);
+
+ child_tree_node = child_tree_host_node->children()[1];
+ ASSERT_TRUE(child_tree_node);
+ EXPECT_EQ(ax::mojom::Role::kStaticText, child_tree_node->GetRole());
+ bounds = child_tree_node->data().relative_bounds.bounds;
+ CompareRect(kTextBoundsBeforeTransform2, bounds);
+ // After applying the transform via RelativeToTreeBounds.
+ bounds = child_tree->RelativeToTreeBounds(child_tree_node, bounds,
+ /*offscreen=*/nullptr,
+ /*clip_bounds=*/true,
+ /*skip_container_offset=*/true);
+ bounds = ax_tree_in_pdf.RelativeToTreeBounds(host_node, bounds);
+ CompareRect(kExpectedTextBoundRelativeToTreeBounds2, bounds);
+}
+#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
+
} // namespace pdf
diff --git a/components/services/screen_ai/screen_ai_ax_tree_serializer.h b/components/services/screen_ai/screen_ai_ax_tree_serializer.h
index 1d781d7..e5226988 100644
--- a/components/services/screen_ai/screen_ai_ax_tree_serializer.h
+++ b/components/services/screen_ai/screen_ai_ax_tree_serializer.h
@@ -36,6 +36,8 @@
ui::AXTreeUpdate Serialize() const;
+ const ui::AXTree* tree_for_testing() const { return tree_.get(); }
+
private:
const std::unique_ptr<ui::AXSerializableTree> tree_;
std::unique_ptr<ui::AXTreeSource<const ui::AXNode*>> tree_source_;