Skip to content

Commit

Permalink
Merge pull request #16508 from brave/text_recognition
Browse files Browse the repository at this point in the history
Introduced copy text from image feature on macOS
  • Loading branch information
simonhong authored Jan 13, 2023
2 parents 0537d39 + 6855228 commit 8704c6f
Show file tree
Hide file tree
Showing 30 changed files with 918 additions and 6 deletions.
2 changes: 2 additions & 0 deletions app/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import("//brave/browser/shell_integrations/buildflags/buildflags.gni")
import("//brave/components/brave_vpn/common/buildflags/buildflags.gni")
import("//brave/components/speedreader/common/buildflags/buildflags.gni")
import("//brave/components/text_recognition/common/buildflags/buildflags.gni")
import("//brave/resources/brave_grit.gni")
import("//build/config/features.gni")
import("//build/config/locales.gni")
Expand All @@ -19,6 +20,7 @@ brave_grit("brave_generated_resources_grit") {
"enable_speedreader=$enable_speedreader",
"enable_brave_vpn=$enable_brave_vpn",
"enable_pin_shortcut=$enable_pin_shortcut",
"enable_text_recognition=$enable_text_recognition",
]
source = "brave_generated_resources.grd"
output_dir = "$root_gen_dir/brave"
Expand Down
1 change: 1 addition & 0 deletions app/brave_command_ids.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#define IDC_COPY_CLEAN_LINK 56040
#define IDC_TOGGLE_TAB_MUTE 56041
#define IDC_SIDEBAR_TOGGLE_POSITION 56042
#define IDC_CONTENT_CONTEXT_COPY_TEXT_FROM_IMAGE 56043

#define IDC_CONTENT_CONTEXT_IMPORT_IPNS_KEYS_START 56100
#define IDC_CONTENT_CONTEXT_IMPORT_IPNS_KEYS_END 56199
Expand Down
19 changes: 19 additions & 0 deletions app/brave_generated_resources.grd
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,25 @@ Or change later at <ph name="SETTINGS_EXTENIONS_LINK">$2<ex>brave://settings/ext
</message>
</if>

<!-- Text recognition dialog-->
<if expr="enable_text_recognition">
<message name="IDS_CONTENT_CONTEXT_COPY_TEXT_FROM_IMAGE" desc="The name of the context menu item to get text from image">
Copy Text From Image
</message>
<message name="IDS_TEXT_RECOGNITION_DIALOG_CLOSE_BUTTON" desc="The text for the close button of text recognition dialog">
Close
</message>
<message name="IDS_TEXT_RECOGNITION_DIALOG_HEADER_IN_PROGRESS" desc="The text for the header of text recognition dialog">
Copying text from image...
</message>
<message name="IDS_TEXT_RECOGNITION_DIALOG_HEADER_COMPLETE" desc="The text for the header of text recognition dialog">
Text copied from image
</message>
<message name="IDS_TEXT_RECOGNITION_DIALOG_HEADER_FAILED" desc="The text for the header of text recognition dialog">
Text copy failed
</message>
</if>

<!-- Brave VPN -->
<if expr="enable_brave_vpn">
<message name="IDS_SETTINGS_SHOW_VPN_BUTTON" desc="The label for showing vpn button in the toolbar">
Expand Down
19 changes: 19 additions & 0 deletions browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import("//brave/components/ipfs/buildflags/buildflags.gni")
import("//brave/components/ntp_background_images/buildflags/buildflags.gni")
import("//brave/components/playlist/common/buildflags/buildflags.gni")
import("//brave/components/speedreader/common/buildflags/buildflags.gni")
import("//brave/components/text_recognition/common/buildflags/buildflags.gni")
import("//brave/components/tor/buildflags/buildflags.gni")
import("//build/config/features.gni")
import("//chrome/common/features.gni")
Expand Down Expand Up @@ -404,6 +405,7 @@ source_set("ui") {
"//brave/components/p3a:buildflags",
"//brave/components/playlist/common/buildflags",
"//brave/components/resources:static_resources",
"//brave/components/text_recognition/common/buildflags",
"//brave/components/time_period_storage",
"//brave/components/tor/buildflags",
"//brave/components/vector_icons",
Expand Down Expand Up @@ -443,6 +445,17 @@ source_set("ui") {
]
}

if (enable_text_recognition) {
sources += [
"views/text_recognition_dialog_tracker.cc",
"views/text_recognition_dialog_tracker.h",
"views/text_recognition_dialog_view.cc",
"views/text_recognition_dialog_view.h",
]

deps += [ "//brave/components/text_recognition/browser" ]
}

# This is no longer compiled into Chromium on Android, but we still
# need it
if (is_android) {
Expand Down Expand Up @@ -790,16 +803,22 @@ source_set("browser_tests") {
]

deps = [
"//brave/app:command_ids",
"//brave/browser/ui",
"//brave/components/constants",
"//chrome/browser",
"//chrome/browser/devtools:test_support",
"//chrome/browser/profiles:profile",
"//chrome/browser/ui",
"//chrome/test:test_support",
"//chrome/test:test_support_ui",
"//components/javascript_dialogs",
"//components/prefs",
"//content/test:test_support",
]

if (enable_text_recognition) {
sources += [ "text_recognition_browsertest.cc" ]
}
}
}
12 changes: 12 additions & 0 deletions browser/ui/browser_dialogs.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
#define BRAVE_BROWSER_UI_BROWSER_DIALOGS_H_

#include "base/callback_forward.h"
#include "brave/components/text_recognition/common/buildflags/buildflags.h"

class Browser;
class SkBitmap;

namespace content {
class WebContents;
} // namespace content

namespace brave {

Expand All @@ -18,6 +24,12 @@ void ShowCrashReportPermissionAskDialog(Browser* browser);
// Run |callback| when dialog closed.
void ShowObsoleteSystemConfirmDialog(base::OnceCallback<void(bool)> callback);

#if BUILDFLAG(ENABLE_TEXT_RECOGNITION)
// Show web modal dialog for showing text that recognized from |image|.
void ShowTextRecognitionDialog(content::WebContents* web_contents,
const SkBitmap& image);
#endif

} // namespace brave

#endif // BRAVE_BROWSER_UI_BROWSER_DIALOGS_H_
150 changes: 150 additions & 0 deletions browser/ui/text_recognition_browsertest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* Copyright (c) 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

#include <memory>

#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "brave/app/brave_command_ids.h"
#include "brave/browser/ui/browser_dialogs.h"
#include "brave/browser/ui/views/text_recognition_dialog_tracker.h"
#include "brave/browser/ui/views/text_recognition_dialog_view.h"
#include "brave/components/constants/brave_paths.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"

namespace {

constexpr char kEmbeddedTestServerDirectory[] = "text_recognition";

} // namespace

class TextRecognitionBrowserTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());

brave::RegisterPathProvider();
base::FilePath test_data_dir;
base::PathService::Get(brave::DIR_TEST_DATA, &test_data_dir);
test_data_dir = test_data_dir.AppendASCII(kEmbeddedTestServerDirectory);
embedded_test_server()->ServeFilesFromDirectory(test_data_dir);

ASSERT_TRUE(embedded_test_server()->Start());
image_html_url_ = embedded_test_server()->GetURL("a.com", "/image.html");
}

void OnGetTextFromImage(const std::vector<std::string>& text) {
// Test image has "brave" text.
EXPECT_EQ("brave", text[0]);
run_loop_->Quit();
}

void OnGetImageForTextCopy(base::WeakPtr<content::WebContents> web_contents,
const SkBitmap& image) {
if (!web_contents)
return;

brave::ShowTextRecognitionDialog(web_contents.get(), image);
}

void WaitUntil(base::RepeatingCallback<bool()> condition) {
if (condition.Run())
return;

base::RepeatingTimer scheduler;
scheduler.Start(FROM_HERE, base::Milliseconds(100),
base::BindLambdaForTesting([this, &condition]() {
if (condition.Run())
run_loop_->Quit();
}));
Run();
}

void Run() {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop()->Run();
}

base::RunLoop* run_loop() const { return run_loop_.get(); }

GURL image_html_url_;
std::unique_ptr<base::RunLoop> run_loop_;
};

IN_PROC_BROWSER_TEST_F(TextRecognitionBrowserTest, TextRecognitionTest) {
content::ContextMenuParams params;
params.media_type = blink::mojom::ContextMenuDataMediaType::kImage;

// kImage type can have copy text from image menu entry.
{
TestRenderViewContextMenu menu(*browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame(),
params);
menu.Init();

EXPECT_TRUE(menu.IsItemPresent(IDC_CONTENT_CONTEXT_COPY_TEXT_FROM_IMAGE));
}

// Other type should not have.
params.media_type = blink::mojom::ContextMenuDataMediaType::kVideo;
{
TestRenderViewContextMenu menu(*browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame(),
params);
menu.Init();

EXPECT_FALSE(menu.IsItemPresent(IDC_CONTENT_CONTEXT_COPY_TEXT_FROM_IMAGE));
}

content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), image_html_url_));
ASSERT_TRUE(WaitForLoadStop(contents));

// Using (10, 10) position will be fine because test image is set at (0, 0).
browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame()
->GetImageAt(
10, 10,
base::BindOnce(&TextRecognitionBrowserTest::OnGetImageForTextCopy,
base::Unretained(this), contents->GetWeakPtr()));
TextRecognitionDialogTracker::CreateForWebContents(contents);
auto* dialog_tracker =
TextRecognitionDialogTracker::FromWebContents(contents);

// Wait till text recognition dialog is launched.
WaitUntil(base::BindLambdaForTesting(
[&]() { return !!dialog_tracker->active_dialog(); }));

TextRecognitionDialogView* text_recognition_dialog =
static_cast<TextRecognitionDialogView*>(
dialog_tracker->active_dialog()->widget_delegate());

// OnGetTextFromImage() verifies extracted text from test image.
text_recognition_dialog->on_get_text_callback_for_test_ = base::BindOnce(
&TextRecognitionBrowserTest::OnGetTextFromImage, base::Unretained(this));

Run();
}
29 changes: 29 additions & 0 deletions browser/ui/views/text_recognition_dialog_tracker.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* Copyright (c) 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

#include "brave/browser/ui/views/text_recognition_dialog_tracker.h"

TextRecognitionDialogTracker::TextRecognitionDialogTracker(
content::WebContents* web_contents)
: content::WebContentsUserData<TextRecognitionDialogTracker>(
*web_contents) {}

TextRecognitionDialogTracker::~TextRecognitionDialogTracker() = default;

void TextRecognitionDialogTracker::SetActiveDialog(views::Widget* widget) {
DCHECK(!active_dialog_ && !observation_.IsObserving());
active_dialog_ = widget;
observation_.Observe(widget);
}

void TextRecognitionDialogTracker::OnWidgetDestroying(views::Widget* widget) {
DCHECK_EQ(active_dialog_, widget);
DCHECK(observation_.IsObservingSource(widget));

observation_.Reset();
active_dialog_ = nullptr;
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(TextRecognitionDialogTracker);
43 changes: 43 additions & 0 deletions browser/ui/views/text_recognition_dialog_tracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Copyright (c) 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

#ifndef BRAVE_BROWSER_UI_VIEWS_TEXT_RECOGNITION_DIALOG_TRACKER_H_
#define BRAVE_BROWSER_UI_VIEWS_TEXT_RECOGNITION_DIALOG_TRACKER_H_

#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "content/public/browser/web_contents_user_data.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"

// Tracks whether text recognition dialog is active or not for WebContents.
class TextRecognitionDialogTracker
: public content::WebContentsUserData<TextRecognitionDialogTracker>,
public views::WidgetObserver {
public:
TextRecognitionDialogTracker(const TextRecognitionDialogTracker&) = delete;
TextRecognitionDialogTracker& operator=(const TextRecognitionDialogTracker&) =
delete;
~TextRecognitionDialogTracker() override;

void SetActiveDialog(views::Widget* widget);

views::Widget* active_dialog() { return active_dialog_; }

private:
friend class content::WebContentsUserData<TextRecognitionDialogTracker>;
explicit TextRecognitionDialogTracker(content::WebContents* web_contents);

// views::WidgetObserver overrides
void OnWidgetDestroying(views::Widget* widget) override;

raw_ptr<views::Widget> active_dialog_ = nullptr;
base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
this};

WEB_CONTENTS_USER_DATA_KEY_DECL();
};

#endif // BRAVE_BROWSER_UI_VIEWS_TEXT_RECOGNITION_DIALOG_TRACKER_H_
Loading

0 comments on commit 8704c6f

Please sign in to comment.