Unify Print Preview extraction across all desktop platforms (#30632)

This commit is contained in:
Anthony Tseng
2025-08-21 03:31:34 +09:00
committed by GitHub
parent 595b297296
commit b13af2e384
13 changed files with 334 additions and 589 deletions
-71
View File
@@ -227,77 +227,6 @@ class AIChatUIBrowserTest : public InProcessBrowserTest {
content::ContentMockCertVerifier mock_cert_verifier_;
};
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreview) {
NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html"),
false);
#if BUILDFLAG(ENABLE_TEXT_RECOGNITION)
FetchPageContent(
FROM_HERE, "This is the way.\n\nI have spoken.\nWherever I Go, He Goes.");
// Panel is still active so we don't need to set it up again
// Page recognition host with a canvas element
NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html"), false);
FetchPageContent(FROM_HERE, "this is the way");
// Ignores all dom content, only does print preview extraction
NavigateURL(https_server_.GetURL("docs.google.com",
"/long_canvas_with_dom_content.html"));
FetchPageContent(FROM_HERE,
"This is the way.\n\nI have spoken.\nWherever I Go, He "
"Goes.\nOr maybe not.");
#if BUILDFLAG(IS_WIN)
// Unsupported locale should return no content for Windows only
// Other platforms do not use locale for extraction
const brave_l10n::test::ScopedDefaultLocale locale("xx_XX");
NavigateURL(https_server_.GetURL("docs.google.com", "/canvas.html"), false);
FetchPageContent(FROM_HERE, "");
#endif // #if BUILDFLAG(IS_WIN)
#else
FetchPageContent(FROM_HERE, "");
#endif
// Each request is cleared after extraction.
EXPECT_FALSE(HasPendingGetContentRequest());
}
#if BUILDFLAG(ENABLE_TEXT_RECOGNITION) && BUILDFLAG(ENABLE_PRINT_PREVIEW)
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreviewPagesLimit) {
NavigateURL(
https_server_.GetURL("docs.google.com", "/extra_long_canvas.html"),
false);
std::string expected_string(ai_chat::kMaxPreviewPages - 1, '\n');
base::StrAppend(&expected_string, {"This is the way."});
FetchPageContent(FROM_HERE, expected_string);
}
// Test print preview extraction while print dialog open
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintDiaglogExtraction) {
NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html"),
false);
printing::TestPrintPreviewObserver print_preview_observer(
/*wait_for_loaded=*/true);
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
"window.print();");
print_preview_observer.WaitUntilPreviewIsReady();
FetchPageContent(
FROM_HERE, "This is the way.\n\nI have spoken.\nWherever I Go, He Goes.");
}
// Test print dialog can still be open after print preview extraction
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, ExtractionPrintDialog) {
NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html"),
false);
FetchPageContent(
FROM_HERE, "This is the way.\n\nI have spoken.\nWherever I Go, He Goes.");
printing::TestPrintPreviewObserver print_preview_observer(
/*wait_for_loaded=*/true);
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
"window.print();");
print_preview_observer.WaitUntilPreviewIsReady();
}
#endif // BUILDFLAG(ENABLE_TEXT_RECOGNITION) && BUILDFLAG(ENABLE_PRINT_PREVIEW)
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreviewDisabled) {
prefs()->SetBoolean(prefs::kPrintPreviewDisabled, true);
+6 -25
View File
@@ -26,38 +26,19 @@ PrintPreviewExtractor::PrintPreviewExtractor(content::WebContents* web_contents,
PrintPreviewExtractor::~PrintPreviewExtractor() = default;
void PrintPreviewExtractor::Extract(ExtractCallback callback) {
// Overwrite any existing extraction in progress, cancelling the operation.
// If AIChatTabHelper for this WebContents is asking for a new extraction
// then it has navigated, or the previous extraction failed to report itself
// somehow.
extractor_ = create_extractor_callback_.Run(
web_contents_, IsPdf(web_contents_),
Extractor::CallbackVariant(base::BindOnce(
&PrintPreviewExtractor::OnComplete<ExtractCallback, std::string>,
weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
extractor_->CreatePrintPreview();
}
void PrintPreviewExtractor::CapturePdf(CapturePdfCallback callback) {
if (!IsPdf(web_contents_)) {
std::move(callback).Run(base::unexpected("Not pdf content"));
return;
}
void PrintPreviewExtractor::CaptureImages(CaptureImagesCallback callback) {
// Overwrite any existing extraction in progress, cancelling the operation.
extractor_ = create_extractor_callback_.Run(
web_contents_, IsPdf(web_contents_),
Extractor::CallbackVariant(base::BindOnce(
&PrintPreviewExtractor::OnComplete<CapturePdfCallback,
std::vector<std::vector<uint8_t>>>,
weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
Extractor::ImageCallback(
base::BindOnce(&PrintPreviewExtractor::OnComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
extractor_->CreatePrintPreview();
}
template <typename CallbackType, typename ResultType>
void PrintPreviewExtractor::OnComplete(
CallbackType callback,
base::expected<ResultType, std::string> result) {
Extractor::ImageCallback callback,
base::expected<std::vector<std::vector<uint8_t>>, std::string> result) {
extractor_.reset();
std::move(callback).Run(std::move(result));
}
+6 -12
View File
@@ -9,7 +9,6 @@
#include <cstdint>
#include <memory>
#include <string>
#include <variant>
#include "base/memory/weak_ptr.h"
#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h"
@@ -30,11 +29,8 @@ class PrintPreviewExtractor
// Performs the print preview extraction. Used only for a single operation.
class Extractor {
public:
using TextCallback =
AIChatTabHelper::PrintPreviewExtractionDelegate::ExtractCallback;
using ImageCallback =
AIChatTabHelper::PrintPreviewExtractionDelegate::CapturePdfCallback;
using CallbackVariant = std::variant<TextCallback, ImageCallback>;
AIChatTabHelper::PrintPreviewExtractionDelegate::CaptureImagesCallback;
virtual ~Extractor() = default;
virtual void CreatePrintPreview() = 0;
@@ -45,22 +41,20 @@ class PrintPreviewExtractor
base::RepeatingCallback<std::unique_ptr<Extractor>(
content::WebContents* web_contents,
bool is_pdf,
Extractor::CallbackVariant&&)>;
Extractor::ImageCallback&&)>;
PrintPreviewExtractor(content::WebContents* web_contents,
CreateExtractorCallback callback);
~PrintPreviewExtractor() override;
PrintPreviewExtractor(const PrintPreviewExtractor&) = delete;
PrintPreviewExtractor& operator=(const PrintPreviewExtractor&) = delete;
void Extract(ExtractCallback callback) override;
void CapturePdf(CapturePdfCallback callback) override;
void CaptureImages(CaptureImagesCallback callback) override;
private:
friend class PrintPreviewExtractorTest;
template <typename CallbackType, typename ResultType>
void OnComplete(CallbackType callback,
base::expected<ResultType, std::string> result);
void OnComplete(
Extractor::ImageCallback callback,
base::expected<std::vector<std::vector<uint8_t>>, std::string> result);
CreateExtractorCallback create_extractor_callback_;
std::unique_ptr<Extractor> extractor_;
@@ -9,7 +9,6 @@
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "base/check.h"
@@ -18,14 +17,11 @@
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/types/expected.h"
#include "brave/browser/ai_chat/print_preview_extractor.h"
#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h"
#include "brave/components/ai_chat/content/browser/pdf_utils.h"
#include "brave/components/ai_chat/core/browser/constants.h"
#include "brave/components/ai_chat/core/browser/utils.h"
#include "brave/components/text_recognition/common/buildflags/buildflags.h"
#include "chrome/browser/pdf/pdf_pref_names.h"
#include "chrome/browser/printing/print_compositor_util.h"
#include "chrome/browser/printing/print_preview_data_service.h"
@@ -62,8 +58,6 @@ using printing::mojom::PrintPreviewUI;
namespace ai_chat {
using CallbackVariant = PrintPreviewExtractor::Extractor::CallbackVariant;
using TextCallback = PrintPreviewExtractor::Extractor::TextCallback;
using ImageCallback = PrintPreviewExtractor::Extractor::ImageCallback;
namespace {
@@ -90,13 +84,13 @@ content::RenderFrameHost* GetRenderFrameHostToUse(
} // namespace
PreviewPageTextExtractor::PreviewPageTextExtractor() = default;
PreviewPageImageExtractor::PreviewPageImageExtractor() = default;
PreviewPageTextExtractor::~PreviewPageTextExtractor() = default;
PreviewPageImageExtractor::~PreviewPageImageExtractor() = default;
void PreviewPageTextExtractor::StartExtract(
void PreviewPageImageExtractor::StartExtract(
base::ReadOnlySharedMemoryRegion pdf_region,
CallbackVariant callback,
ImageCallback callback,
std::optional<bool> pdf_use_skia_renderer_enabled) {
pdf_region_ = std::move(pdf_region);
callback_ = std::move(callback);
@@ -105,110 +99,63 @@ void PreviewPageTextExtractor::StartExtract(
pdf_to_bitmap_converter_.BindNewPipeAndPassReceiver());
}
pdf_to_bitmap_converter_.set_disconnect_handler(
base::BindOnce(&PreviewPageTextExtractor::BitmapConverterDisconnected,
base::BindOnce(&PreviewPageImageExtractor::BitmapConverterDisconnected,
base::Unretained(this)));
if (pdf_use_skia_renderer_enabled.has_value()) {
pdf_to_bitmap_converter_->SetUseSkiaRendererPolicy(
pdf_use_skia_renderer_enabled.value());
}
current_page_index_ = 0;
preview_text_.clear();
// No text preview to clear for image extraction
pdf_to_bitmap_converter_->GetPdfPageCount(
pdf_region_.Duplicate(),
base::BindOnce(&PreviewPageTextExtractor::OnGetPageCount,
base::BindOnce(&PreviewPageImageExtractor::OnGetPageCount,
base::Unretained(this)));
}
void PreviewPageTextExtractor::BindForTesting(
void PreviewPageImageExtractor::BindForTesting(
mojo::PendingRemote<printing::mojom::PdfToBitmapConverter> converter) {
CHECK_IS_TEST();
pdf_to_bitmap_converter_.Bind(std::move(converter));
}
void PreviewPageTextExtractor::ScheduleNextPageOrComplete() {
void PreviewPageImageExtractor::ScheduleNextPageOrComplete() {
DCHECK_GT(total_page_count_, 0u);
if (current_page_index_ < total_page_count_) {
if (current_page_index_ &&
std::holds_alternative<TextCallback>(callback_)) {
base::StrAppend(&preview_text_, {"\n"});
}
pdf_to_bitmap_converter_->GetBitmap(
pdf_region_.Duplicate(), current_page_index_,
base::BindOnce(&PreviewPageTextExtractor::OnGetBitmap,
base::BindOnce(&PreviewPageImageExtractor::OnGetBitmap,
base::Unretained(this)));
} else {
if (auto* text_cb = std::get_if<TextCallback>(&callback_)) {
std::move(*text_cb).Run(base::ok(preview_text_));
} else if (auto* image_cb = std::get_if<ImageCallback>(&callback_)) {
std::move(*image_cb).Run(base::ok(std::move(pdf_pages_image_data_)));
}
std::move(callback_).Run(base::ok(std::move(pdf_pages_image_data_)));
}
}
void PreviewPageTextExtractor::OnGetPageCount(
void PreviewPageImageExtractor::OnGetPageCount(
std::optional<uint32_t> page_count) {
if (!page_count.has_value() || !page_count.value()) {
if (auto* text_cb = std::get_if<TextCallback>(&callback_)) {
std::move(*text_cb).Run(base::unexpected("Failed to get page count"));
} else if (auto* image_cb = std::get_if<ImageCallback>(&callback_)) {
std::move(*image_cb).Run(base::unexpected("Failed to get page count"));
}
std::move(callback_).Run(base::unexpected("Failed to get page count"));
return;
}
total_page_count_ = page_count.value();
ScheduleNextPageOrComplete();
}
void PreviewPageTextExtractor::OnGetBitmap(const SkBitmap& bitmap) {
void PreviewPageImageExtractor::OnGetBitmap(const SkBitmap& bitmap) {
if (bitmap.drawsNothing()) {
if (auto* text_cb = std::get_if<TextCallback>(&callback_)) {
std::move(*text_cb).Run(base::unexpected("Invalid bitmap"));
} else if (auto* image_cb = std::get_if<ImageCallback>(&callback_)) {
std::move(*image_cb).Run(base::unexpected("Invalid bitmap"));
}
std::move(callback_).Run(base::unexpected("Invalid bitmap"));
return;
}
if (std::holds_alternative<ImageCallback>(callback_)) {
ProcessNextBitmapPage(bitmap);
} else if (std::holds_alternative<TextCallback>(callback_)) {
#if BUILDFLAG(ENABLE_TEXT_RECOGNITION)
GetOCRText(bitmap,
base::BindOnce(&PreviewPageTextExtractor::ProcessNextTextPage,
weak_ptr_factory_.GetWeakPtr()));
#else
auto* text_cb = std::get_if<TextCallback>(&callback_);
std::move(*text_cb).Run(base::ok(""));
#endif
}
ProcessNextBitmapPage(bitmap);
}
void PreviewPageTextExtractor::ProcessNextTextPage(std::string page_content) {
auto* text_cb = std::get_if<TextCallback>(&callback_);
DCHECK(text_cb);
VLOG(4) << "Page index(" << current_page_index_
<< ") content: " << page_content;
base::StrAppend(&preview_text_, {page_content});
// Stop processing if we have reached the maximum number of pages
if (current_page_index_ + 1 >= kMaxPreviewPages) {
std::move(*text_cb).Run(base::ok(preview_text_));
return;
}
++current_page_index_;
ScheduleNextPageOrComplete();
}
void PreviewPageTextExtractor::ProcessNextBitmapPage(const SkBitmap& bitmap) {
auto* image_cb = std::get_if<ImageCallback>(&callback_);
DCHECK(image_cb);
void PreviewPageImageExtractor::ProcessNextBitmapPage(const SkBitmap& bitmap) {
// Encode bitmap to PNG for capture
auto png_data =
gfx::PNGCodec::EncodeBGRASkBitmap(ScaleDownBitmap(bitmap), false);
if (!png_data) {
std::move(*image_cb).Run(base::unexpected("Failed to encode the bitmap"));
std::move(callback_).Run(base::unexpected("Failed to encode the bitmap"));
return;
} else {
pdf_pages_image_data_.push_back(*png_data);
@@ -218,19 +165,15 @@ void PreviewPageTextExtractor::ProcessNextBitmapPage(const SkBitmap& bitmap) {
ScheduleNextPageOrComplete();
}
void PreviewPageTextExtractor::BitmapConverterDisconnected() {
if (auto* text_cb = std::get_if<TextCallback>(&callback_)) {
std::move(*text_cb).Run(base::unexpected("Bitmap converter disconnected"));
} else if (auto* image_cb = std::get_if<ImageCallback>(&callback_)) {
std::move(*image_cb).Run(base::unexpected("Bitmap converter disconnected"));
}
void PreviewPageImageExtractor::BitmapConverterDisconnected() {
std::move(callback_).Run(base::unexpected("Bitmap converter disconnected"));
}
PrintPreviewExtractorInternal::PrintPreviewExtractorInternal(
content::WebContents* web_contents,
Profile* profile,
bool is_pdf,
CallbackVariant callback,
ImageCallback callback,
GetPrintPreviewUIIdMapCallback id_map_callback,
GetPrintPreviewUIRequestIdMapCallback request_id_map_callback)
: is_pdf_(is_pdf),
@@ -320,19 +263,15 @@ PrintPreviewExtractorInternal::GetPrintPreviewUIIdForTesting() {
return print_preview_ui_id_;
}
void PrintPreviewExtractorInternal::SetPreviewPageTextExtractorForTesting(
std::unique_ptr<PreviewPageTextExtractor> extractor) {
void PrintPreviewExtractorInternal::SetPreviewPageImageExtractorForTesting(
std::unique_ptr<PreviewPageImageExtractor> extractor) {
CHECK_IS_TEST();
preview_page_text_extractor_ = std::move(extractor);
preview_page_image_extractor_ = std::move(extractor);
}
void PrintPreviewExtractorInternal::SendError(const std::string& error) {
PreviewCleanup();
if (auto* text_cb = std::get_if<ExtractCallback>(&callback_)) {
std::move(*text_cb).Run(base::unexpected(error));
} else if (auto* image_cb = std::get_if<CapturePdfCallback>(&callback_)) {
std::move(*image_cb).Run(base::unexpected(error));
}
std::move(callback_).Run(base::unexpected(error));
}
void PrintPreviewExtractorInternal::SetOptionsFromDocument(
@@ -584,44 +523,22 @@ void PrintPreviewExtractorInternal::OnPreviewReady() {
prefs->GetBoolean(::prefs::kPdfUseSkiaRendererEnabled);
}
// Create the appropriate callback based on the variant type
CallbackVariant callback;
if (std::holds_alternative<ExtractCallback>(callback_)) {
callback = base::BindOnce(&PrintPreviewExtractorInternal::OnGetOCRResult,
weak_ptr_factory_.GetWeakPtr());
} else if (std::holds_alternative<CapturePdfCallback>(callback_)) {
callback =
base::BindOnce(&PrintPreviewExtractorInternal::OnCaptureBitmapResult,
weak_ptr_factory_.GetWeakPtr());
}
if (!preview_page_text_extractor_) {
preview_page_text_extractor_ = std::make_unique<PreviewPageTextExtractor>();
}
preview_page_text_extractor_->StartExtract(std::move(pdf_region.region),
std::move(callback),
pdf_use_skia_renderer_enabled);
}
void PrintPreviewExtractorInternal::OnGetOCRResult(
base::expected<std::string, std::string> result) {
if (result.has_value()) {
PreviewCleanup();
if (auto* text_cb = std::get_if<ExtractCallback>(&callback_)) {
std::move(*text_cb).Run(std::move(result));
}
} else {
SendError(result.error());
if (!preview_page_image_extractor_) {
preview_page_image_extractor_ =
std::make_unique<PreviewPageImageExtractor>();
}
preview_page_image_extractor_->StartExtract(
std::move(pdf_region.region),
base::BindOnce(&PrintPreviewExtractorInternal::OnCaptureBitmapResult,
weak_ptr_factory_.GetWeakPtr()),
pdf_use_skia_renderer_enabled);
}
void PrintPreviewExtractorInternal::OnCaptureBitmapResult(
base::expected<std::vector<std::vector<uint8_t>>, std::string> result) {
if (result.has_value()) {
PreviewCleanup();
if (auto* image_cb = std::get_if<CapturePdfCallback>(&callback_)) {
std::move(*image_cb).Run(std::move(result));
}
std::move(callback_).Run(std::move(result));
} else {
SendError(result.error());
}
@@ -38,17 +38,16 @@ static_assert(BUILDFLAG(ENABLE_PRINT_PREVIEW));
namespace ai_chat {
using ExtractCallback = PrintPreviewExtractor::ExtractCallback;
using CapturePdfCallback = PrintPreviewExtractor::CapturePdfCallback;
using CaptureImagesCallback = PrintPreviewExtractor::CaptureImagesCallback;
class PreviewPageTextExtractor {
class PreviewPageImageExtractor {
public:
PreviewPageTextExtractor();
virtual ~PreviewPageTextExtractor();
PreviewPageImageExtractor();
virtual ~PreviewPageImageExtractor();
virtual void StartExtract(
base::ReadOnlySharedMemoryRegion pdf_region,
PrintPreviewExtractor::Extractor::CallbackVariant callback,
PrintPreviewExtractor::Extractor::ImageCallback callback,
std::optional<bool> pdf_use_skia_renderer_enabled);
void BindForTesting(
@@ -58,19 +57,17 @@ class PreviewPageTextExtractor {
void ScheduleNextPageOrComplete();
void OnGetPageCount(std::optional<uint32_t> page_count);
void OnGetBitmap(const SkBitmap& bitmap);
void ProcessNextTextPage(std::string page_content);
void ProcessNextBitmapPage(const SkBitmap& bitmap);
void BitmapConverterDisconnected();
std::string preview_text_;
size_t current_page_index_ = 0;
size_t total_page_count_ = 0;
base::ReadOnlySharedMemoryRegion pdf_region_;
PrintPreviewExtractor::Extractor::CallbackVariant callback_;
PrintPreviewExtractor::Extractor::ImageCallback callback_;
// raw bytes data of captured pdf pages
std::vector<std::vector<uint8_t>> pdf_pages_image_data_;
mojo::Remote<printing::mojom::PdfToBitmapConverter> pdf_to_bitmap_converter_;
base::WeakPtrFactory<PreviewPageTextExtractor> weak_ptr_factory_{this};
base::WeakPtrFactory<PreviewPageImageExtractor> weak_ptr_factory_{this};
};
class PrintPreviewExtractorInternal : public PrintPreviewExtractor::Extractor,
@@ -84,7 +81,7 @@ class PrintPreviewExtractorInternal : public PrintPreviewExtractor::Extractor,
content::WebContents* web_contents,
Profile* profile,
bool is_pdf,
PrintPreviewExtractor::Extractor::CallbackVariant callback,
PrintPreviewExtractor::Extractor::ImageCallback callback,
GetPrintPreviewUIIdMapCallback id_map_callback,
GetPrintPreviewUIRequestIdMapCallback request_id_map_callback);
@@ -98,8 +95,8 @@ class PrintPreviewExtractorInternal : public PrintPreviewExtractor::Extractor,
std::optional<int32_t> GetPrintPreviewUIIdForTesting() override;
void SetPreviewPageTextExtractorForTesting(
std::unique_ptr<PreviewPageTextExtractor> extractor);
void SetPreviewPageImageExtractorForTesting(
std::unique_ptr<PreviewPageImageExtractor> extractor);
void SendError(const std::string& error);
@@ -167,14 +164,12 @@ class PrintPreviewExtractorInternal : public PrintPreviewExtractor::Extractor,
void OnPreviewReady();
void OnGetOCRResult(base::expected<std::string, std::string> result);
void OnCaptureBitmapResult(
base::expected<std::vector<std::vector<uint8_t>>, std::string> result);
private:
bool is_pdf_ = false;
PrintPreviewExtractor::Extractor::CallbackVariant callback_;
PrintPreviewExtractor::Extractor::ImageCallback callback_;
GetPrintPreviewUIIdMapCallback id_map_callback_;
GetPrintPreviewUIRequestIdMapCallback request_id_map_callback_;
// unique id to avoid conflicts with other print preview UIs
@@ -182,7 +177,7 @@ class PrintPreviewExtractorInternal : public PrintPreviewExtractor::Extractor,
mojo::AssociatedReceiver<PrintPreviewUI> print_preview_ui_receiver_{this};
int preview_request_id_ = -1;
std::unique_ptr<PreviewPageTextExtractor> preview_page_text_extractor_;
std::unique_ptr<PreviewPageImageExtractor> preview_page_image_extractor_;
mojo::AssociatedRemote<printing::mojom::PrintRenderFrame> print_render_frame_;
raw_ptr<content::WebContents> web_contents_ = nullptr;
@@ -148,45 +148,33 @@ class MockPrintPreviewPrintRenderFrame
mojo::AssociatedReceiver<printing::mojom::PrintRenderFrame> receiver_{this};
};
class MockPreviewPageTextExtractor : public PreviewPageTextExtractor {
class MockPreviewPageImageExtractor : public PreviewPageImageExtractor {
public:
MockPreviewPageTextExtractor(base::ReadOnlySharedMemoryRegion expected_region,
bool expected_error)
MockPreviewPageImageExtractor(
base::ReadOnlySharedMemoryRegion expected_region,
bool expected_error)
: expected_region_(std::move(expected_region)),
expected_error_(expected_error) {}
~MockPreviewPageTextExtractor() override = default;
~MockPreviewPageImageExtractor() override = default;
MockPreviewPageTextExtractor(const MockPreviewPageTextExtractor&) = delete;
MockPreviewPageTextExtractor& operator=(const MockPreviewPageTextExtractor&) =
delete;
MockPreviewPageImageExtractor(const MockPreviewPageImageExtractor&) = delete;
MockPreviewPageImageExtractor& operator=(
const MockPreviewPageImageExtractor&) = delete;
void StartExtract(
base::ReadOnlySharedMemoryRegion pdf_region,
PrintPreviewExtractor::Extractor::CallbackVariant callback,
PrintPreviewExtractor::Extractor::ImageCallback callback,
std::optional<bool> pdf_use_skia_renderer_enabled) override {
// verify the correct memory region is passed
EXPECT_EQ(pdf_region.Map().GetMemoryAsSpan<const uint8_t>(),
expected_region_.Map().GetMemoryAsSpan<const uint8_t>());
if (auto* text_cb =
std::get_if<PrintPreviewExtractor::Extractor::TextCallback>(
&callback)) {
if (!expected_error_) {
std::move(*text_cb).Run(base::ok("extracted text"));
} else {
std::move(*text_cb).Run(
base::unexpected("PreviewPageTextExtractor error"));
}
} else if (auto* image_cb =
std::get_if<PrintPreviewExtractor::Extractor::ImageCallback>(
&callback)) {
if (!expected_error_) {
std::vector<std::vector<uint8_t>> result = {{0xde, 0xad}, {0xbe, 0xef}};
std::move(*image_cb).Run(base::ok(std::move(result)));
} else {
std::move(*image_cb).Run(
base::unexpected("PreviewPageTextExtractor error"));
}
if (!expected_error_) {
std::vector<std::vector<uint8_t>> result = {{0xde, 0xad}, {0xbe, 0xef}};
std::move(callback).Run(base::ok(std::move(result)));
} else {
std::move(callback).Run(
base::unexpected("PreviewPageImageExtractor error"));
}
}
@@ -244,7 +232,6 @@ class MockPdfToBitmapConverter : public printing::mojom::PdfToBitmapConverter {
using ImageResult =
base::expected<std::vector<std::vector<uint8_t>>, std::string>;
using TextResult = base::expected<std::string, std::string>;
class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
public:
@@ -265,13 +252,13 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
web_contents(),
base::BindRepeating([](content::WebContents* web_contents, bool is_pdf,
ai_chat::PrintPreviewExtractor::Extractor::
CallbackVariant&& variant)
ImageCallback&& callback)
-> std::unique_ptr<
ai_chat::PrintPreviewExtractor::Extractor> {
return std::make_unique<ai_chat::PrintPreviewExtractorInternal>(
web_contents,
Profile::FromBrowserContext(web_contents->GetBrowserContext()),
is_pdf, std::move(variant),
is_pdf, std::move(callback),
base::BindRepeating(
[]() -> base::IDMap<printing::mojom::PrintPreviewUI*>& {
return printing::PrintPreviewUI::GetPrintPreviewUIIdMap();
@@ -321,7 +308,6 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
// Intentionally setup a PrintRenderFrame error to stop early so we can
// verify the print settings we pass is correct.
void RunPrintSettingsTest(const std::string& mime_type,
bool use_capture_pdf,
bool expect_preview_modifiable,
const base::Location& location = FROM_HERE) {
SCOPED_TRACE(location.ToString());
@@ -374,21 +360,13 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewFailed,
std::move(on_complete));
if (use_capture_pdf) {
base::test::TestFuture<
base::expected<std::vector<std::vector<uint8_t>>, std::string>>
future;
pp_extractor_->CapturePdf(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), "PrintPreviewFailed");
} else {
base::test::TestFuture<base::expected<std::string, std::string>> future;
pp_extractor_->Extract(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), "PrintPreviewFailed");
}
base::test::TestFuture<
base::expected<std::vector<std::vector<uint8_t>>, std::string>>
future;
pp_extractor_->CaptureImages(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), "PrintPreviewFailed");
EXPECT_TRUE(
printing::PrintPreviewUI::GetPrintPreviewUIRequestIdMap().empty());
@@ -414,7 +392,6 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
// Test for all possible PrintRenderFrame errors
void RunErrorTest(
const std::string& mime_type,
bool use_capture_pdf,
MockPrintPreviewPrintRenderFrame::ExpectedError expected_error,
const std::string& expected_error_message,
const base::Location& location = FROM_HERE) {
@@ -422,21 +399,13 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
auto print_render_frame = SetupPrintPreviewTest(mime_type, expected_error);
if (use_capture_pdf) {
base::test::TestFuture<
base::expected<std::vector<std::vector<uint8_t>>, std::string>>
future;
pp_extractor_->CapturePdf(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), expected_error_message);
} else {
base::test::TestFuture<base::expected<std::string, std::string>> future;
pp_extractor_->Extract(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), expected_error_message);
}
base::test::TestFuture<
base::expected<std::vector<std::vector<uint8_t>>, std::string>>
future;
pp_extractor_->CaptureImages(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), expected_error_message);
EXPECT_TRUE(
printing::PrintPreviewUI::GetPrintPreviewUIRequestIdMap().empty());
@@ -445,8 +414,8 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
void SimulateFullFlow(bool extractor_error) {
auto full_pdf_region = CreatePageRegion(64).region;
internal_extractor()->SetPreviewPageTextExtractorForTesting(
std::make_unique<MockPreviewPageTextExtractor>(
internal_extractor()->SetPreviewPageImageExtractorForTesting(
std::make_unique<MockPreviewPageImageExtractor>(
full_pdf_region.Duplicate(), extractor_error));
// Simulate page composition
@@ -461,12 +430,11 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
full_pdf_region.Duplicate());
}
// Test all possible flows with mocked PreviewPageTextExtractor that retruns
// Test all possible flows with mocked PreviewPageImageExtractor that returns
// fixed successful results.
template <typename T>
void RunTestCase(base::test::TestFuture<T>& future,
const std::string& mime_type,
bool use_capture_pdf,
const std::string& expected_error_msg,
bool extractor_error,
bool simulate_partial_composition) {
@@ -496,13 +464,11 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
if (expect_error) {
ASSERT_FALSE(result.has_value())
<< "Expected error for mime_type=" << mime_type
<< ", use_capture_pdf=" << use_capture_pdf
<< ", partial_composition=" << simulate_partial_composition;
EXPECT_EQ(result.error(), expected_error_msg);
} else {
ASSERT_TRUE(result.has_value())
<< "Expected success for mime_type=" << mime_type
<< ", use_capture_pdf=" << use_capture_pdf;
<< "Expected success for mime_type=" << mime_type;
if constexpr (std::is_same_v<T, ImageResult>) {
const std::vector<std::vector<uint8_t>> expected = {{0xde, 0xad},
{0xbe, 0xef}};
@@ -522,67 +488,65 @@ class PrintPreviewExtractorTest : public ChromeRenderViewHostTestHarness {
std::unique_ptr<PrintPreviewExtractor> pp_extractor_;
};
TEST_F(PrintPreviewExtractorTest, CapturePdfWithNotPdf) {
TEST_F(PrintPreviewExtractorTest, CaptureImagesWithNonPdf) {
content::WebContentsTester::For(web_contents())
->SetMainFrameMimeType("text/html");
auto print_render_frame = SetupPrintPreviewTest(
"text/html",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewFailed);
base::test::TestFuture<
base::expected<std::vector<std::vector<uint8_t>>, std::string>>
future;
pp_extractor_->CapturePdf(future.GetCallback());
pp_extractor_->CaptureImages(future.GetCallback());
auto result = future.Take();
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), "Not pdf content");
EXPECT_EQ(result.error(), "PrintPreviewFailed");
}
TEST_F(PrintPreviewExtractorTest, PrintSettings) {
// Test with non-PDF content using Extract()
RunPrintSettingsTest("text/html", false, true);
// Test with non-PDF content
RunPrintSettingsTest("text/html", true);
// Test with PDF content using Extract()
RunPrintSettingsTest("application/pdf", false, false);
// Test with PDF content using CapturePdf()
RunPrintSettingsTest("application/pdf", true, false);
// Test with PDF content using CaptureImages()
RunPrintSettingsTest("application/pdf", false);
}
TEST_F(PrintPreviewExtractorTest, Errors) {
// Test Extract() with various errors
// Test CaptureImages() with various errors
RunErrorTest(
"text/html", false,
"text/html",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewFailed,
"PrintPreviewFailed");
RunErrorTest(
"text/html", false,
"text/html",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewCanceled,
"PrintPreviewCancelled");
RunErrorTest(
"text/html", false,
"text/html",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrinterSettingsInvalid,
"PrinterSettingsInvalid");
// Test CapturePdf() with various errors
RunErrorTest(
"application/pdf", true,
"application/pdf",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewFailed,
"PrintPreviewFailed");
RunErrorTest(
"application/pdf", true,
"application/pdf",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrintPreviewCanceled,
"PrintPreviewCancelled");
RunErrorTest(
"application/pdf", true,
"application/pdf",
MockPrintPreviewPrintRenderFrame::ExpectedError::kPrinterSettingsInvalid,
"PrinterSettingsInvalid");
// Test disabled print preview
profile()->GetPrefs()->SetBoolean(::prefs::kPrintPreviewDisabled, true);
RunErrorTest("application/pdf", true,
RunErrorTest("application/pdf",
MockPrintPreviewPrintRenderFrame::ExpectedError::kNone,
"Print preview is disabled");
RunErrorTest("text/html", false,
MockPrintPreviewPrintRenderFrame::ExpectedError::kNone,
"Print preview is disabled");
RunErrorTest("application/pdf", false,
RunErrorTest("text/html",
MockPrintPreviewPrintRenderFrame::ExpectedError::kNone,
"Print preview is disabled");
}
@@ -590,7 +554,6 @@ TEST_F(PrintPreviewExtractorTest, Errors) {
TEST_F(PrintPreviewExtractorTest, PrintPreviewData) {
struct TestParams {
std::string mime_type;
bool use_capture_pdf;
std::string expected_error_msg; // Empty string means success
bool extractor_error;
bool simulate_partial_composition;
@@ -598,21 +561,21 @@ TEST_F(PrintPreviewExtractorTest, PrintPreviewData) {
const TestParams kTestCases[] = {
// Missing preview data cases - error on OnCompositeToPdfDone
{"text/html", false, "Failed to get preview data", false, false},
{"application/pdf", true, "Failed to get preview data", false, false},
{"text/html", "Failed to get preview data", false, false},
{"application/pdf", "Failed to get preview data", false, false},
// Missing preview data cases - missing OnCompositePdfPageDone and error
// on OnCompositeToPdfDone
{"text/html", false, "Failed to get preview data", false, true},
{"application/pdf", true, "Failed to get preview data", false, true},
{"text/html", "Failed to get preview data", false, true},
{"application/pdf", "Failed to get preview data", false, true},
// Successful extraction cases
{"text/html", false, "", false, false},
{"application/pdf", true, "", false, false},
{"text/html", "", false, false},
{"application/pdf", "", false, false},
// Extraction error cases
{"text/html", false, "PreviewPageTextExtractor error", true, false},
{"application/pdf", true, "PreviewPageTextExtractor error", true, false},
{"text/html", "PreviewPageImageExtractor error", true, false},
{"application/pdf", "PreviewPageImageExtractor error", true, false},
};
for (const auto& test_case : kTestCases) {
@@ -620,34 +583,26 @@ TEST_F(PrintPreviewExtractorTest, PrintPreviewData) {
test_case.mime_type,
MockPrintPreviewPrintRenderFrame::ExpectedError::kNone);
if (test_case.use_capture_pdf) {
base::test::TestFuture<ImageResult> future;
pp_extractor_->CapturePdf(future.GetCallback());
RunTestCase(future, test_case.mime_type, test_case.use_capture_pdf,
test_case.expected_error_msg, test_case.extractor_error,
test_case.simulate_partial_composition);
} else {
base::test::TestFuture<TextResult> future;
pp_extractor_->Extract(future.GetCallback());
RunTestCase(future, test_case.mime_type, test_case.use_capture_pdf,
test_case.expected_error_msg, test_case.extractor_error,
test_case.simulate_partial_composition);
}
base::test::TestFuture<ImageResult> future;
pp_extractor_->CaptureImages(future.GetCallback());
RunTestCase(future, test_case.mime_type, test_case.expected_error_msg,
test_case.extractor_error,
test_case.simulate_partial_composition);
}
}
class PreviewPageTextExtractorTest : public testing::Test {
class PreviewPageImageExtractorTest : public testing::Test {
public:
PreviewPageTextExtractorTest() = default;
~PreviewPageTextExtractorTest() override = default;
PreviewPageImageExtractorTest() = default;
~PreviewPageImageExtractorTest() override = default;
void SetUp() override {
extractor_ = std::make_unique<PreviewPageTextExtractor>();
extractor_ = std::make_unique<PreviewPageImageExtractor>();
converter_ = std::make_unique<MockPdfToBitmapConverter>();
extractor()->BindForTesting(converter()->Bind());
}
PreviewPageTextExtractor* extractor() { return extractor_.get(); }
PreviewPageImageExtractor* extractor() { return extractor_.get(); }
MockPdfToBitmapConverter* converter() { return converter_.get(); }
@@ -664,7 +619,7 @@ class PreviewPageTextExtractorTest : public testing::Test {
EXPECT_EQ(result.error(), expected_error);
}
{
base::test::TestFuture<TextResult> future;
base::test::TestFuture<ImageResult> future;
extractor()->StartExtract(CreatePageRegion(50).region,
future.GetCallback(), std::nullopt);
auto result = future.Take();
@@ -690,47 +645,18 @@ class PreviewPageTextExtractorTest : public testing::Test {
}));
}
// Testing text extraction with page count and inspect page separators are
// placed correctly or none for single page.
void RunExtractTextTest(uint32_t page_count,
const base::Location& location = FROM_HERE) {
SCOPED_TRACE(location.ToString());
converter()->SetExpectedPageCount(page_count);
base::test::TestFuture<TextResult> future;
extractor()->StartExtract(CreatePageRegion(50).region, future.GetCallback(),
std::nullopt);
auto result = future.Take();
ASSERT_TRUE(result.has_value());
#if BUILDFLAG(ENABLE_TEXT_RECOGNITION)
// OCR will fail intentionally so we are testing if each page is processed
// as expected.
std::string expected;
if (page_count == 1) {
expected = "";
} else if (page_count > kMaxPreviewPages) {
expected = std::string(kMaxPreviewPages - 1, '\n');
} else {
expected = std::string(page_count - 1, '\n');
}
EXPECT_EQ(result.value(), expected);
#else
EXPECT_EQ(result.value(), "");
#endif
}
private:
base::test::TaskEnvironment task_environment_;
std::unique_ptr<PreviewPageTextExtractor> extractor_;
std::unique_ptr<PreviewPageImageExtractor> extractor_;
std::unique_ptr<MockPdfToBitmapConverter> converter_;
};
TEST_F(PreviewPageTextExtractorTest, GetPdfPageCountError) {
TEST_F(PreviewPageImageExtractorTest, GetPdfPageCountError) {
converter()->SetExpectedPageCount(std::nullopt);
RunErrorTest("Failed to get page count");
}
TEST_F(PreviewPageTextExtractorTest, GetBitmapError) {
TEST_F(PreviewPageImageExtractorTest, GetBitmapError) {
converter()->SetExpectedPageCount(1);
converter()->SetExpectedEmptyBitmap(true);
RunErrorTest("Invalid bitmap");
@@ -739,7 +665,7 @@ TEST_F(PreviewPageTextExtractorTest, GetBitmapError) {
RunErrorTest("Invalid bitmap");
}
TEST_F(PreviewPageTextExtractorTest, CaptureImages) {
TEST_F(PreviewPageImageExtractorTest, CaptureImages) {
converter()->SetExpectedEmptyBitmap(false);
// Test with single page
@@ -748,30 +674,11 @@ TEST_F(PreviewPageTextExtractorTest, CaptureImages) {
// Test with multiple pages
RunCaptureImageTest(3);
// Test with max pages
RunCaptureImageTest(kMaxPreviewPages);
// Test with many pages
RunCaptureImageTest(20);
// Test exceeding max pages
RunCaptureImageTest(kMaxPreviewPages + 1);
}
TEST_F(PreviewPageTextExtractorTest, ExtractText) {
converter()->SetExpectedEmptyBitmap(false);
// Test single page (empty string expected)
RunExtractTextTest(1);
// Test two pages (one newline)
RunExtractTextTest(2);
// Test max pages
RunExtractTextTest(kMaxPreviewPages);
// Test less than max pages
RunExtractTextTest(kMaxPreviewPages - 1);
// Test exceeding max pages (should be capped)
RunExtractTextTest(kMaxPreviewPages + 1);
// Test with even more pages
RunCaptureImageTest(25);
}
} // namespace ai_chat
+3 -3
View File
@@ -135,8 +135,8 @@ void AttachTabHelpers(content::WebContents* web_contents) {
web_contents,
base::BindRepeating(
[](content::WebContents* web_contents, bool is_pdf,
ai_chat::PrintPreviewExtractor::Extractor::CallbackVariant&&
variant)
ai_chat::PrintPreviewExtractor::Extractor::ImageCallback&&
callback)
-> std::unique_ptr<
ai_chat::PrintPreviewExtractor::Extractor> {
return std::make_unique<
@@ -144,7 +144,7 @@ void AttachTabHelpers(content::WebContents* web_contents) {
web_contents,
Profile::FromBrowserContext(
web_contents->GetBrowserContext()),
is_pdf, std::move(variant),
is_pdf, std::move(callback),
base::BindRepeating(
[]() -> base::IDMap<
printing::mojom::PrintPreviewUI*>& {
+164 -25
View File
@@ -44,8 +44,7 @@ class MockPrintPreviewExtractor
MockPrintPreviewExtractor() = default;
~MockPrintPreviewExtractor() override = default;
MOCK_METHOD(void, Extract, (ExtractCallback), (override));
MOCK_METHOD(void, CapturePdf, (CapturePdfCallback), (override));
MOCK_METHOD(void, CaptureImages, (CaptureImagesCallback), (override));
};
class MockPageContentFetcher
@@ -152,6 +151,11 @@ class AIChatTabHelperUnitTest : public content::RenderViewHostTestHarness,
helper_->GetPageContent(std::move(callback), invalidation_token);
}
void GetScreenshots(
mojom::ConversationHandler::GetScreenshotsCallback callback) {
helper_->GetScreenshots(std::move(callback));
}
void TitleWasSet(content::NavigationEntry* entry) {
helper_->TitleWasSet(entry);
}
@@ -243,10 +247,8 @@ TEST_P(AIChatTabHelperUnitTest, GetPageContent_HasContent) {
NavigateTo(GURL("https://www.brave.com"));
EXPECT_CALL(*page_content_fetcher_, FetchPageContent)
.WillOnce(base::test::RunOnceCallback<1>(kSuppliedText, false, ""));
if (print_preview_extractor_) {
// Fallback won't initiate if we already have content
EXPECT_CALL(*print_preview_extractor_, Extract).Times(0);
}
// Note: No need to mock CaptureImages since print preview hosts
// are not triggered when regular content is available
base::MockCallback<AIChatTabHelper::FetchPageContentCallback> callback;
EXPECT_CALL(callback, Run(kExpectedText, false, ""));
GetPageContent(callback.Get(), "");
@@ -257,10 +259,8 @@ TEST_P(AIChatTabHelperUnitTest, GetPageContent_VideoContent) {
NavigateTo(GURL("https://www.brave.com"));
EXPECT_CALL(*page_content_fetcher_, FetchPageContent)
.WillOnce(base::test::RunOnceCallback<1>("", true, ""));
if (print_preview_extractor_) {
// Fallback won't initiate for video content.
EXPECT_CALL(*print_preview_extractor_, Extract).Times(0);
}
// Note: No need to mock CaptureImages since print preview hosts
// are not triggered for video content
base::MockCallback<AIChatTabHelper::FetchPageContentCallback> callback;
EXPECT_CALL(callback, Run("", true, ""));
GetPageContent(callback.Get(), "");
@@ -268,20 +268,20 @@ TEST_P(AIChatTabHelperUnitTest, GetPageContent_VideoContent) {
TEST_P(AIChatTabHelperUnitTest, GetPageContent_PrintPreviewTriggeringURL) {
base::MockCallback<AIChatTabHelper::FetchPageContentCallback> callback;
constexpr char kExpectedText[] = "This is the way.";
// A url that does by itself trigger print preview extraction.
// A url that triggers print preview extraction - should return empty content
// to allow autoscreenshots mechanism to handle server-side OCR
for (const auto& host : kPrintPreviewRetrievalHosts) {
NavigateTo(GURL(base::StrCat({"https://", host})));
if (is_print_preview_supported_) {
// PrintPreview always initiated on URL
// PrintPreview returns empty content to trigger autoscreenshots
EXPECT_CALL(*page_content_fetcher_, FetchPageContent).Times(0);
EXPECT_CALL(*print_preview_extractor_, Extract)
.WillOnce(base::test::RunOnceCallback<0>(base::ok(kExpectedText)));
// No Extract call - we now return empty to trigger autoscreenshots
} else {
EXPECT_CALL(*page_content_fetcher_, FetchPageContent)
.WillOnce(base::test::RunOnceCallback<1>(kExpectedText, false, ""));
.WillOnce(base::test::RunOnceCallback<1>("", false, ""));
}
EXPECT_CALL(callback, Run(kExpectedText, false, ""));
// Expect empty content which will trigger autoscreenshots in real usage
EXPECT_CALL(callback, Run("", false, ""));
GetPageContent(callback.Get(), "");
}
}
@@ -293,8 +293,7 @@ TEST_P(AIChatTabHelperUnitTest,
// Don't fallback to regular fetch on failed print preview extraction.
if (print_preview_extractor_) {
EXPECT_CALL(*page_content_fetcher_, FetchPageContent).Times(0);
EXPECT_CALL(*print_preview_extractor_, Extract)
.WillOnce(base::test::RunOnceCallback<0>(base::unexpected("")));
// Print preview now returns empty content directly
} else {
EXPECT_CALL(*page_content_fetcher_, FetchPageContent)
.WillOnce(base::test::RunOnceCallback<1>("", false, ""));
@@ -314,7 +313,7 @@ TEST_P(AIChatTabHelperUnitTest,
if (is_print_preview_supported_) {
// Nothing should be called until page load
EXPECT_CALL(*page_content_fetcher_, FetchPageContent).Times(0);
EXPECT_CALL(*print_preview_extractor_, Extract).Times(0);
// No CaptureImages calls expected until page loads
GetPageContent(callback.Get(), "");
testing::Mock::VerifyAndClearExpectations(&page_content_fetcher_);
testing::Mock::VerifyAndClearExpectations(&print_preview_extractor_);
@@ -324,8 +323,7 @@ TEST_P(AIChatTabHelperUnitTest,
// empty content, callback should run.
EXPECT_CALL(callback, Run("", false, ""));
EXPECT_CALL(*page_content_fetcher_, FetchPageContent).Times(0);
EXPECT_CALL(*print_preview_extractor_, Extract)
.WillOnce(base::test::RunOnceCallback<0>(base::unexpected("")));
// Print preview now returns empty content directly
SimulateLoadFinished();
testing::Mock::VerifyAndClearExpectations(&page_content_fetcher_);
@@ -371,9 +369,7 @@ TEST_P(AIChatTabHelperUnitTest,
// Navigatng should result in our pending callback being run with no content
// and no content extraction initiated.
EXPECT_CALL(*page_content_fetcher_, FetchPageContent).Times(0);
if (is_print_preview_supported_) {
EXPECT_CALL(*print_preview_extractor_, Extract).Times(0);
}
// No CaptureImages calls expected during navigation
EXPECT_CALL(callback, Run("", false, ""));
NavigateTo(initial_url.Resolve("/2"), /*keep_loading=*/true,
is_same_document);
@@ -455,4 +451,147 @@ TEST_P(AIChatTabHelperUnitTest, GetPageContent_NoFallbackWhenNotPDF) {
testing::Mock::VerifyAndClearExpectations(&page_content_fetcher_);
}
// Tests for GetScreenshots method decision logic
TEST_P(AIChatTabHelperUnitTest, GetScreenshots_PrintPreviewHost) {
// Test that print preview hosts use CaptureImages when delegate is available
NavigateTo(GURL("https://docs.google.com/document"));
base::test::TestFuture<std::optional<std::vector<mojom::UploadedFilePtr>>>
future;
if (is_print_preview_supported_) {
// Should use print preview extraction
EXPECT_CALL(*print_preview_extractor_, CaptureImages)
.WillOnce(base::test::RunOnceCallback<0>(
base::expected<std::vector<std::vector<uint8_t>>, std::string>(
std::vector<std::vector<uint8_t>>{{0x89, 0x50, 0x4E, 0x47}})));
}
GetScreenshots(future.GetCallback());
auto result = future.Take();
if (is_print_preview_supported_) {
// Should receive screenshots when print preview is supported
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(result->empty());
} else {
// When print preview is not supported, should return empty result
EXPECT_FALSE(result.has_value());
}
}
TEST_P(AIChatTabHelperUnitTest, GetScreenshots_RegularHost) {
// Test that regular hosts use FullScreenshotter (we can't easily mock this)
NavigateTo(GURL("https://www.example.com"));
base::test::TestFuture<std::optional<std::vector<mojom::UploadedFilePtr>>>
future;
if (is_print_preview_supported_) {
// Print preview extractor should NOT be called for regular hosts
EXPECT_CALL(*print_preview_extractor_, CaptureImages).Times(0);
}
// Note: We can't easily mock FullScreenshotter since it's created internally,
// but we can verify that CaptureImages is not called on the print preview
// extractor
GetScreenshots(future.GetCallback());
// The result should be provided by FullScreenshotter
auto result = future.Take();
// We can't predict the exact result since FullScreenshotter behavior
// depends on the actual rendering, but we can verify the call completed
}
TEST_P(AIChatTabHelperUnitTest, GetScreenshots_MultipleHosts) {
// Test all print preview hosts
for (const auto& host : kPrintPreviewRetrievalHosts) {
SCOPED_TRACE(testing::Message() << "Testing host: " << host);
NavigateTo(GURL(base::StrCat({"https://", host, "/document"})));
base::test::TestFuture<std::optional<std::vector<mojom::UploadedFilePtr>>>
future;
if (is_print_preview_supported_) {
// Should use print preview extraction for all print preview hosts
EXPECT_CALL(*print_preview_extractor_, CaptureImages)
.WillOnce(base::test::RunOnceCallback<0>(
base::expected<std::vector<std::vector<uint8_t>>, std::string>(
std::vector<std::vector<uint8_t>>{
{0x89, 0x50, 0x4E, 0x47}})));
}
GetScreenshots(future.GetCallback());
auto result = future.Take();
if (is_print_preview_supported_) {
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(result->empty());
} else {
EXPECT_FALSE(result.has_value());
}
if (is_print_preview_supported_) {
testing::Mock::VerifyAndClearExpectations(&print_preview_extractor_);
}
}
}
TEST_P(AIChatTabHelperUnitTest, GetScreenshots_PrintPreviewError) {
// Test error handling when print preview extraction fails
NavigateTo(GURL("https://docs.google.com/document"));
base::test::TestFuture<std::optional<std::vector<mojom::UploadedFilePtr>>>
future;
if (is_print_preview_supported_) {
// Simulate print preview extraction error
EXPECT_CALL(*print_preview_extractor_, CaptureImages)
.WillOnce(base::test::RunOnceCallback<0>(
base::unexpected<std::string>("Print preview extraction failed")));
}
GetScreenshots(future.GetCallback());
auto result = future.Take();
// Should return empty result on error or when not supported
EXPECT_FALSE(result.has_value());
}
#if BUILDFLAG(ENABLE_PDF)
TEST_P(AIChatTabHelperUnitTest, GetScreenshots_PDFContent) {
// Test that PDF content uses print preview extraction
NavigateTo(GURL("https://example.com/document.pdf"));
// Set the main frame MIME type to PDF
content::WebContentsTester::For(web_contents())
->SetMainFrameMimeType(pdf::kPDFMimeType);
base::test::TestFuture<std::optional<std::vector<mojom::UploadedFilePtr>>>
future;
if (is_print_preview_supported_) {
// Should use print preview extraction for PDFs even on non-print-preview
// hosts
EXPECT_CALL(*print_preview_extractor_, CaptureImages)
.WillOnce(base::test::RunOnceCallback<0>(
base::expected<std::vector<std::vector<uint8_t>>, std::string>(
std::vector<std::vector<uint8_t>>{{0x25, 0x50, 0x44, 0x46}})));
}
GetScreenshots(future.GetCallback());
auto result = future.Take();
if (is_print_preview_supported_) {
// When print preview is supported, should receive screenshots from print
// preview extraction
EXPECT_TRUE(result.has_value());
EXPECT_FALSE(result->empty());
}
// When print preview is not supported, PDFs fall back to FullScreenshotter
// We can't predict the exact FullScreenshotter result, but the call should
// complete
}
#endif // BUILDFLAG(ENABLE_PDF)
} // namespace ai_chat
@@ -142,14 +142,25 @@ void AIChatTabHelper::GetPageContent(FetchPageContentCallback callback,
std::move(callback).Run("", false, "");
return;
}
if (kPrintPreviewRetrievalHosts.contains(
if (print_preview_extraction_delegate_ &&
kPrintPreviewRetrievalHosts.contains(
web_contents()->GetLastCommittedURL().host_piece())) {
// Get content using a printing / OCR mechanism, instead of
// directly from the source, if available.
// Get content using print preview image capture for server-side OCR
DVLOG(1) << __func__ << " print preview url";
if (MaybePrintPreviewExtract(callback)) {
// For print preview hosts, we always return empty content to trigger
// the autoscreenshots mechanism which will use CaptureImages for
// server-side OCR. However, if the page isn't loaded yet, wait for load
// completion.
if (!is_page_loaded_) {
DVLOG(1) << "print preview page was not loaded yet, will return empty "
"after load";
SetPendingGetContentCallback(std::move(callback));
return;
}
DVLOG(1) << "print preview host detected, returning empty to trigger "
"autoscreenshots";
std::move(callback).Run("", false, "");
return;
}
page_content_fetcher_delegate_->FetchPageContent(
invalidation_token,
@@ -182,37 +193,6 @@ void AIChatTabHelper::SetPendingGetContentCallback(
pending_get_page_content_callback_ = std::move(callback);
}
bool AIChatTabHelper::MaybePrintPreviewExtract(
FetchPageContentCallback& callback) {
if (print_preview_extraction_delegate_ == nullptr) {
DVLOG(1) << "print preview extraction not supported";
return false;
}
if (!is_page_loaded_) {
DVLOG(1) << "will extract print preview content when page is loaded";
SetPendingGetContentCallback(std::move(callback));
} else {
// When page is already loaded, fallback to print preview extraction
DVLOG(1) << "extracting print preview content now";
print_preview_extraction_delegate_->Extract(
base::BindOnce(&AIChatTabHelper::OnExtractPrintPreviewContentComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
return true;
}
void AIChatTabHelper::OnExtractPrintPreviewContentComplete(
FetchPageContentCallback callback,
base::expected<std::string, std::string> result) {
// Invalidation token not applicable for print preview OCR
if (result.has_value()) {
std::move(callback).Run(std::move(result.value()), false, "");
} else {
VLOG(1) << result.error();
std::move(callback).Run("", false, "");
}
}
void AIChatTabHelper::OnNewPage(int64_t navigation_id) {
DVLOG(3) << __func__ << " id: " << navigation_id;
AssociatedContentDriver::OnNewPage(navigation_id);
@@ -341,8 +321,13 @@ bool AIChatTabHelper::HasOpenAIChatPermission() const {
void AIChatTabHelper::GetScreenshots(
mojom::ConversationHandler::GetScreenshotsCallback callback) {
if (IsPdf(web_contents())) {
print_preview_extraction_delegate_->CapturePdf(
if (print_preview_extraction_delegate_ &&
(IsPdf(web_contents()) ||
kPrintPreviewRetrievalHosts.contains(
web_contents()->GetLastCommittedURL().host_piece()))) {
// Use print preview extraction for PDFs and print preview hosts
// when delegate is available
print_preview_extraction_delegate_->CaptureImages(
base::BindOnce(&AIChatTabHelper::OnScreenshotsCaptured,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
} else {
@@ -53,18 +53,13 @@ class AIChatTabHelper : public content::WebContentsObserver,
// Delegate to extract print preview content
class PrintPreviewExtractionDelegate {
public:
// Result is extracted text or error
using ExtractCallback =
base::OnceCallback<void(base::expected<std::string, std::string>)>;
// Result is image data of pdf pages or error
using CapturePdfCallback = base::OnceCallback<void(
// Result is image data of pages or error
using CaptureImagesCallback = base::OnceCallback<void(
base::expected<std::vector<std::vector<uint8_t>>, std::string>)>;
virtual ~PrintPreviewExtractionDelegate() = default;
// Get the current text from the WebContents using Print Preview and OCR
virtual void Extract(ExtractCallback callback) = 0;
// Capture images of pdf without doing OCR
virtual void CapturePdf(CapturePdfCallback callback) = 0;
// Capture images of content without doing OCR
virtual void CaptureImages(CaptureImagesCallback callback) = 0;
};
class PageContentFetcherDelegate {
@@ -151,10 +146,6 @@ class AIChatTabHelper : public content::WebContentsObserver,
bool is_video,
std::string invalidation_token);
void OnExtractPrintPreviewContentComplete(
FetchPageContentCallback callback,
base::expected<std::string, std::string>);
#if BUILDFLAG(ENABLE_PDF)
void OnPDFDocumentLoadComplete(FetchPageContentCallback callback);
@@ -168,7 +159,6 @@ class AIChatTabHelper : public content::WebContentsObserver,
const std::vector<std::pair<size_t, std::string>>& page_texts);
#endif // BUILDFLAG(ENABLE_PDF)
bool MaybePrintPreviewExtract(FetchPageContentCallback& callback);
void SetPendingGetContentCallback(FetchPageContentCallback callback);
@@ -28,7 +28,6 @@ inline constexpr auto kPrintPreviewRetrievalHosts =
"watermark.silverchair.com",
});
inline constexpr uint8_t kMaxPreviewPages = 20;
inline constexpr char kLeoModelSupportUrl[] =
"https://support.brave.app/hc/en-us/articles/26727364100493-"
"What-are-the-differences-between-Leo-s-AI-Models";
-36
View File
@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Leo test</title>
</head>
<body>
<script>
// Here we create 21 pages to test kMaxPreviewPages which is 20.
// These pages will be created only after receiving the beforeprint event.
window.addEventListener('beforeprint', () => {
for (let i = 0; i < 21; i++) {
const div = document.createElement('div');
div.style.width = '559px';
div.style.height = '794px';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 559
canvas.height = 794
context.font = '30px Arial';
if (i == 19) {
// This should be the last page because of the truncation.
context.fillText('This is the way.', 50, 100);
} else if (i == 20) {
// This will be truncated.
context.fillText('There is no way.', 50, 100);
}
div.appendChild(canvas);
document.body.appendChild(div);
}
});
</script>
</body>
</html>
@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Leo test</title>
</head>
<body>
<div style="width: 559px; height: 794px;">
<canvas id="page1" width="559" height="794">
</canvas>
</div>
<div style="width: 559px; height: 794px;">
<canvas id="page2" width="559" height="794">
</canvas>
</div>
<div style="width: 559px; height: 794px;">
<canvas id="page3" width="559" height="794">
</canvas>
</div>
<div style="width: 559px; height: 794px;">
<canvas id="page4" width="559" height="794">
</canvas>
</div>
<script>
var canvas = document.getElementById('page1');
var context = canvas.getContext('2d');
context.font = '30px Arial';
context.fillText('This is the way.', 50, 100);
// leave page 2 empty
canvas = document.getElementById('page3');
context = canvas.getContext('2d');
context.font = '30px Arial';
context.fillText('I have spoken.', 50, 100);
// smaller text with higher DPI
canvas = document.getElementById('page4');
const dpr = 2;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
context = canvas.getContext('2d');
context.scale(dpr, dpr);
context.font = '15px Arial';
context.fillText('Wherever I Go, He Goes.', 50, 100);
</script>
<main>
<p>Or maybe not.</p>
</main>
</body>
</html>