0

[lensoverlay] Send PDF text prior to full PDF

Retrieves the first X characters rounded to the nearest page from the
PDF renderer to be used as early suggest signals prior to processing
the entire PDF. The server is not ready for the partial PDF upload,
so that will come in a follow up CL, but will follow the same process
as `SendPageContentUpdateRequest`, i.e. the LensOverlayQueryController
does not change the bytes, only attaches them to the request.

Bug: 379344946
Change-Id: I054e3b9b040bb6d3fca3c0b8451f193b99756346
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6035260
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Duncan Mercer <mercerd@google.com>
Cr-Commit-Position: refs/heads/main@{#1395166}
This commit is contained in:
Duncan Mercer
2024-12-11 16:17:16 -08:00
committed by Chromium LUCI CQ
parent d426061714
commit a784cffb39
16 changed files with 320 additions and 29 deletions

@ -9,6 +9,7 @@
#include "base/json/json_reader.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/numerics/checked_math.h"
#include "base/process/kill.h"
#include "base/strings/utf_string_conversions.h"
#include "base/system/sys_info.h"
@ -1540,6 +1541,7 @@ void LensOverlayController::StorePageContentAndContinueInitialization(
std::optional<uint32_t> page_count) {
initialization_data->page_content_bytes_ = bytes;
initialization_data->page_content_type_ = content_type;
initialization_data->pdf_page_count_ = page_count;
InitializeOverlay(std::move(initialization_data));
RecordDocumentMetrics(page_count);
@ -1600,13 +1602,73 @@ void LensOverlayController::OnPdfBytesReceived(
const std::vector<uint8_t>& bytes,
uint32_t page_count) {
// TODO(b/370530197): Show user error message if status is not success.
if (status != pdf::mojom::PdfListener::GetPdfBytesStatus::kSuccess) {
if (status != pdf::mojom::PdfListener::GetPdfBytesStatus::kSuccess ||
page_count == 0) {
std::move(callback).Run(std::vector<uint8_t>(), lens::MimeType::kPdf,
page_count);
return;
}
std::move(callback).Run(bytes, lens::MimeType::kPdf, page_count);
}
void LensOverlayController::GetPartialPdfText(uint32_t page_count) {
pdf::PDFDocumentHelper* pdf_helper =
pdf::PDFDocumentHelper::MaybeGetForWebContents(tab_->GetContents());
if (!pdf_helper ||
lens::features::GetLensOverlayPdfSuggestCharacterTarget() == 0) {
return;
}
// Fetch the first page of text which will be then recursively fetch following
// pages.
initialization_data_->pdf_pages_text_.clear();
pdf_helper->GetPageText(
0, base::BindOnce(&LensOverlayController::GetPartialPdfTextCallback,
weak_factory_.GetWeakPtr(), /*page_index=*/0,
page_count, /*total_characters_retrieved=*/0));
}
void LensOverlayController::GetPartialPdfTextCallback(
uint32_t page_index,
uint32_t total_page_count,
uint32_t total_characters_retrieved,
const std::u16string& page_text) {
// Sanity checks that the input is expected.
CHECK_GE(total_page_count, 1u);
CHECK_LT(page_index, total_page_count);
CHECK_EQ(initialization_data_->pdf_pages_text_.size(), page_index);
// Add the page text to the list of pages and update the total characters
// retrieved count.
initialization_data_->pdf_pages_text_.push_back(page_text);
// Ensure no integer overflow. If overflow, set the total characters retrieved
// to the max value so the loop will exit.
base::CheckedNumeric<uint32_t> total_characters_retrieved_check =
total_characters_retrieved;
total_characters_retrieved_check += page_text.size();
total_characters_retrieved = total_characters_retrieved_check.ValueOrDefault(
std::numeric_limits<uint32_t>::max());
pdf::PDFDocumentHelper* pdf_helper =
pdf::PDFDocumentHelper::MaybeGetForWebContents(tab_->GetContents());
// Stop the loop if the character limit is reached or if the page index is
// out of bounds or the PDF helper no longer exists.
if (!pdf_helper ||
total_characters_retrieved >=
lens::features::GetLensOverlayPdfSuggestCharacterTarget() ||
page_index + 1 >= total_page_count) {
lens_overlay_query_controller_->SendPartialPageContentRequest(
initialization_data_->pdf_pages_text_);
return;
}
pdf_helper->GetPageText(
page_index + 1,
base::BindOnce(&LensOverlayController::GetPartialPdfTextCallback,
weak_factory_.GetWeakPtr(), page_index + 1,
total_page_count, total_characters_retrieved));
}
#endif // BUILDFLAG(ENABLE_PDF)
void LensOverlayController::OnInnerTextReceived(
@ -1700,6 +1762,10 @@ void LensOverlayController::UpdatePageContextualization(
return;
}
// Since the page content has changed so let the query controller know to
// avoid dangling pointers.
lens_overlay_query_controller_->ResetPageContentData();
initialization_data_->page_content_bytes_ = bytes;
initialization_data_->page_content_type_ = content_type;
@ -1711,6 +1777,12 @@ void LensOverlayController::UpdatePageContextualization(
/*suppress_ghost_loader=*/bytes.empty(),
/*reset_loading_state=*/false);
// If the new page is a PDF, fetch the text from the page to be used as early
// suggest signals.
if (content_type == lens::MimeType::kPdf) {
GetPartialPdfText(page_count.value_or(0));
}
lens_overlay_query_controller_->SendPageContentUpdateRequest(
initialization_data_->page_content_bytes_,
initialization_data_->page_content_type_, GetPageURL());
@ -1847,6 +1919,11 @@ void LensOverlayController::CloseUIPart2(
->RemoveObserver(this);
}
// LensOverlayQueryController points to initialization data and therefore must
// be reset before the initialization data to avoid dangling pointers.
lens_overlay_query_controller_.reset();
initialization_data_.reset();
tab_contents_view_observer_.Reset();
omnibox_tab_helper_observer_.Reset();
find_tab_observer_.Reset();
@ -1854,9 +1931,7 @@ void LensOverlayController::CloseUIPart2(
side_panel_page_.reset();
receiver_.reset();
page_.reset();
initialization_data_.reset();
languages_controller_.reset();
lens_overlay_query_controller_.reset();
scoped_tab_modal_ui_.reset();
pending_side_panel_url_.reset();
pending_text_query_.reset();
@ -1918,6 +1993,13 @@ void LensOverlayController::InitializeOverlay(
InitializeOverlayUI(*initialization_data_);
base::UmaHistogramBoolean("Lens.Overlay.Shown", true);
// If PDF content was extracted from the page, fetch the text from the PDF to
// be used as early suggest signals.
if (initialization_data_->page_content_type_ == lens::MimeType::kPdf) {
CHECK(initialization_data_->pdf_page_count_.has_value());
GetPartialPdfText(initialization_data_->pdf_page_count_.value());
}
// If the StartQueryFlow optimization is enabled, the page contents will not
// be sent with the initial image request, so we need to send it here.
if (lens::features::IsLensOverlayContextualSearchboxEnabled() &&

@ -6,6 +6,8 @@
#define CHROME_BROWSER_UI_LENS_LENS_OVERLAY_CONTROLLER_H_
#include <optional>
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
@ -605,6 +607,13 @@ class LensOverlayController : public LensSearchboxClient,
// empty.
lens::MimeType page_content_type_ = lens::MimeType::kUnknown;
// The page count of the PDF document if page_content_type_ is kPdf.
std::optional<uint32_t> pdf_page_count_;
// The partial representation of a PDF document. The element at a given
// index holds the text of the PDF page at the same index.
std::vector<std::u16string> pdf_pages_text_;
// Bounding boxes for significant regions identified in the screenshot.
std::vector<lens::mojom::CenterRotatedBoxPtr> significant_region_boxes_;
@ -703,6 +712,19 @@ class LensOverlayController : public LensSearchboxClient,
pdf::mojom::PdfListener::GetPdfBytesStatus status,
const std::vector<uint8_t>& bytes,
uint32_t pdf_page_count);
// Starts the process of fetching the text from the PDF to be used for suggest
// signals.
void GetPartialPdfText(uint32_t total_page_count);
// Gets the partial text from the PDF to be used for suggest. Schedules for
// the next page of text to be fetched, from the PDF in page order until
// either 1) all the text is received or 2) the character limit is reached.
// This method should only be called by GetPartialPdfText.
void GetPartialPdfTextCallback(uint32_t page_index,
uint32_t total_page_count,
uint32_t total_characters_retrieved,
const std::u16string& page_text);
#endif // BUILDFLAG(ENABLE_PDF)
// Callback for when the inner text is retrieved from the underlying page.

@ -126,6 +126,7 @@ constexpr char kDocumentWithNamedElementWithFragment[] =
constexpr char kDocumentWithImage[] = "/test_visual.html";
constexpr char kDocumentWithDynamicColor[] = "/lens/dynamic_color.html";
constexpr char kPdfDocument[] = "/pdf/test.pdf";
constexpr char kMultiPagePdf[] = "/pdf/test-bookmarks.pdf";
constexpr char kPdfDocumentWithForm[] = "/pdf/submit_form.pdf";
constexpr char kDocumentWithNonAsciiCharacters[] = "/non-ascii.html";
@ -4396,7 +4397,7 @@ class LensOverlayControllerBrowserPDFContextualizationTest
{{"use-pdfs-as-context", "true"},
{"use-inner-html-as-context", "true"},
{"file-upload-limit-bytes",
base::NumberToString(file_size_limit_bytes_)}}});
base::NumberToString(kFileSizeLimitBytes)}}});
return enabled;
}
@ -4405,7 +4406,7 @@ class LensOverlayControllerBrowserPDFContextualizationTest
}
protected:
const uint32_t file_size_limit_bytes_ = 10000;
static constexpr uint32_t kFileSizeLimitBytes = 10000;
};
IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
@ -4442,6 +4443,35 @@ IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
.last_received_should_show_contextual_searchbox_);
}
IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
PartialPdfIncludedInRequest) {
// Open the PDF document and wait for it to finish loading.
const GURL url = embedded_test_server()->GetURL(kPdfDocument);
content::RenderFrameHost* extension_host = LoadPdfGetExtensionHost(url);
ASSERT_TRUE(extension_host);
// State should start in off.
auto* controller = GetLensOverlayController();
ASSERT_TRUE(controller);
ASSERT_EQ(controller->state(), State::kOff);
// Open the overlay.
controller->ShowUI(LensOverlayInvocationSource::kAppMenu);
ASSERT_EQ(controller->state(), State::kScreenshot);
ASSERT_TRUE(base::test::RunUntil(
[&] { return controller->state() == State::kOverlay; }));
// Verify pdf content was included in the query.
auto* fake_query_controller =
static_cast<lens::TestLensOverlayQueryController*>(
controller->get_lens_overlay_query_controller_for_testing());
ASSERT_TRUE(base::test::RunUntil([&] {
return fake_query_controller->last_sent_partial_content().size() == 1;
}));
ASSERT_EQ(u"this is some text\r\nsome more text",
fake_query_controller->last_sent_partial_content()[0]);
}
IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
PageUrlIncludedInRequest) {
// Open the PDF document and wait for it to finish loading.
@ -4538,7 +4568,7 @@ IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFContextualizationTest,
// Verify the document is over the size limit.
auto file_size = GetFileSizeForTestDataFile(kPdfDocument12KbFileName);
ASSERT_TRUE(file_size.has_value());
ASSERT_GT(file_size.value(), file_size_limit_bytes_);
ASSERT_GT(file_size.value(), kFileSizeLimitBytes);
// Open the PDF document that is over our size limit and wait for it to finish
// loading.
@ -4783,11 +4813,68 @@ IN_PROC_BROWSER_TEST_P(
true, /*expected_count=*/1);
}
class LensOverlayControllerBrowserPDFIncreaseLimitTest
: public LensOverlayControllerBrowserPDFContextualizationTest {
public:
std::vector<base::test::FeatureRefAndParams> GetEnabledFeatures()
const override {
auto enabled = PDFExtensionTestBase::GetEnabledFeatures();
enabled.push_back({lens::features::kLensOverlayContextualSearchbox,
{{"use-pdfs-as-context", "true"},
{"use-inner-html-as-context", "true"},
{"pdf-text-character-limit", "50"},
{"file-upload-limit-bytes",
base::NumberToString(kFileSizeLimitBytes)}}});
return enabled;
}
std::vector<base::test::FeatureRef> GetDisabledFeatures() const override {
return {};
}
protected:
static constexpr uint32_t kFileSizeLimitBytes = 200000;
};
IN_PROC_BROWSER_TEST_P(LensOverlayControllerBrowserPDFIncreaseLimitTest,
PartialPdfCharacterLimitReached_IncludedInRequest) {
// Open the PDF document and wait for it to finish loading.
const GURL url = embedded_test_server()->GetURL(kMultiPagePdf);
content::RenderFrameHost* extension_host = LoadPdfGetExtensionHost(url);
ASSERT_TRUE(extension_host);
// State should start in off.
auto* controller = GetLensOverlayController();
ASSERT_TRUE(controller);
ASSERT_EQ(controller->state(), State::kOff);
// Open the overlay.
controller->ShowUI(LensOverlayInvocationSource::kAppMenu);
ASSERT_EQ(controller->state(), State::kScreenshot);
ASSERT_TRUE(base::test::RunUntil(
[&] { return controller->state() == State::kOverlay; }));
// Verify the first two pages were sent, excluding the last page of the PDF.
auto* fake_query_controller =
static_cast<lens::TestLensOverlayQueryController*>(
controller->get_lens_overlay_query_controller_for_testing());
ASSERT_TRUE(base::test::RunUntil([&] {
return 2u == fake_query_controller->last_sent_partial_content().size();
}));
ASSERT_EQ(u"1 First Section\r\nThis is the first section.\r\n1",
fake_query_controller->last_sent_partial_content()[0]);
ASSERT_EQ(u"1.1 First Subsection\r\nThis is the first subsection.\r\n2",
fake_query_controller->last_sent_partial_content()[1]);
}
// TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer
// launches.
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(LensOverlayControllerBrowserPDFTest);
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
LensOverlayControllerBrowserPDFContextualizationTest);
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
LensOverlayControllerBrowserPDFIncreaseLimitTest);
// Test with --enable-pixel-output-in-tests enabled, required to actually grab
// screenshots for color extraction.

@ -382,14 +382,17 @@ void LensOverlayQueryController::SendEndTranslateModeQuery() {
PrepareAndFetchFullImageRequest();
}
void LensOverlayQueryController::ResetPageContentData() {
underlying_content_bytes_ = base::span<const uint8_t>();
underlying_content_type_ = lens::MimeType::kUnknown;
page_url_ = GURL();
partial_content_ = base::span<const std::u16string>();
}
void LensOverlayQueryController::SendPageContentUpdateRequest(
base::span<const uint8_t> new_content_bytes,
lens::MimeType new_content_type,
GURL new_page_url) {
if (new_content_bytes == underlying_content_bytes_ &&
new_content_type == underlying_content_type_) {
return;
}
underlying_content_bytes_ = new_content_bytes;
underlying_content_type_ = new_content_type;
page_url_ = new_page_url;
@ -421,6 +424,13 @@ void LensOverlayQueryController::SendPageContentUpdateRequest(
PrepareAndFetchPageContentRequest();
}
void LensOverlayQueryController::SendPartialPageContentRequest(
base::span<const std::u16string> partial_content) {
partial_content_ = partial_content;
// TODO(379344946): Attach partial content to a new request and send.
}
void LensOverlayQueryController::SendRegionSearch(
lens::mojom::CenterRotatedBoxPtr region,
lens::LensOverlaySelectionType lens_selection_type,

@ -6,10 +6,10 @@
#define CHROME_BROWSER_UI_LENS_LENS_OVERLAY_QUERY_CONTROLLER_H_
#include <optional>
#include <string>
#include "base/containers/span.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/time/time.h"
#include "chrome/browser/lens/core/mojom/lens.mojom.h"
@ -108,12 +108,23 @@ class LensOverlayQueryController {
// ending translate mode.
virtual void SendEndTranslateModeQuery();
// Resets the page content data to avoid using stale data in the request flow.
// Caller should call this before changing the page content data this class
// points to, to avoid dangling pointers.
virtual void ResetPageContentData();
// Sends a request to the server to update the page content.
virtual void SendPageContentUpdateRequest(
base::span<const uint8_t> new_content_bytes,
lens::MimeType new_content_type,
GURL new_page_url);
// Sends a request to the server with a portion of the page content.
// `partial_content` should be a subset of the full page content. This request
// is used to give the server an early signal of the page content.
virtual void SendPartialPageContentRequest(
base::span<const std::u16string> partial_content);
// Sends a region search interaction. Expected to be called multiple times. If
// region_bytes are included, those will be sent to Lens instead of cropping
// the region out of the screenshot. This should be used to provide a higher
@ -599,13 +610,16 @@ class LensOverlayQueryController {
// The bytes of the content the user is viewing. Owned by
// LensOverlayController. Will be empty if no bytes to the underlying page
// could be provided.
// TODO(367764863) Rewrite to base::raw_span.
RAW_PTR_EXCLUSION base::span<const uint8_t> underlying_content_bytes_;
base::raw_span<const uint8_t> underlying_content_bytes_;
// The mime type of underlying_content_bytes. Will be kNone if
// underlying_content_bytes_ is empty.
lens::MimeType underlying_content_type_;
// A span of text that represents a part of the content held in underlying
// content bytes.
base::raw_span<const std::u16string> partial_content_;
// Whether or not the parent interaction query has been sent. This should
// always be the first interaction in a query flow.
bool parent_query_sent_ = false;

@ -1198,6 +1198,9 @@ TEST_F(LensOverlayQueryControllerTest,
base::test::TestFuture<lens::proto::LensOverlayUrlResponse>
url_response_future;
base::test::TestFuture<const std::string&> thumbnail_created_future;
// kFakeContentBytes needs to outlive the query controller, so initialize it
// here.
const std::vector<uint8_t> kFakeContentBytes({1, 2, 3, 4});
TestLensOverlayQueryController query_controller(
full_image_response_future.GetRepeatingCallback(),
url_response_future.GetRepeatingCallback(), GetSuggestInputsCallback(),
@ -1222,11 +1225,10 @@ TEST_F(LensOverlayQueryControllerTest,
SkBitmap bitmap = CreateNonEmptyBitmap(100, 100);
std::map<std::string, std::string> additional_search_query_params;
std::vector<uint8_t> fake_content_bytes({1, 2, 3, 4});
query_controller.StartQueryFlow(
bitmap, GURL(kTestPageUrl),
std::make_optional<std::string>(kTestPageTitle),
std::vector<lens::mojom::CenterRotatedBoxPtr>(), fake_content_bytes,
std::vector<lens::mojom::CenterRotatedBoxPtr>(), kFakeContentBytes,
lens::MimeType::kPdf, 0, base::TimeTicks::Now());
ASSERT_TRUE(full_image_response_future.Wait());
@ -1333,6 +1335,9 @@ TEST_F(LensOverlayQueryControllerTest,
base::test::TestFuture<lens::proto::LensOverlayUrlResponse>
url_response_future;
base::test::TestFuture<const std::string&> thumbnail_created_future;
// kFakeContentBytes needs to outlive the query controller, so initialize it
// here.
const std::vector<uint8_t> kFakeContentBytes({1, 2, 3, 4});
TestLensOverlayQueryController query_controller(
full_image_response_future.GetRepeatingCallback(),
url_response_future.GetRepeatingCallback(), GetSuggestInputsCallback(),
@ -1357,11 +1362,10 @@ TEST_F(LensOverlayQueryControllerTest,
SkBitmap bitmap = CreateNonEmptyBitmap(100, 100);
std::map<std::string, std::string> additional_search_query_params;
std::vector<uint8_t> fake_content_bytes({1, 2, 3, 4});
query_controller.StartQueryFlow(
bitmap, GURL(kTestPageUrl),
std::make_optional<std::string>(kTestPageTitle),
std::vector<lens::mojom::CenterRotatedBoxPtr>(), fake_content_bytes,
std::vector<lens::mojom::CenterRotatedBoxPtr>(), kFakeContentBytes,
lens::MimeType::kHtml, 0, base::TimeTicks::Now());
ASSERT_TRUE(full_image_response_future.Wait());
query_controller.SendContextualTextQuery(
@ -1467,6 +1471,9 @@ TEST_F(LensOverlayQueryControllerTest,
base::test::TestFuture<lens::proto::LensOverlayUrlResponse>
url_response_future;
base::test::TestFuture<const std::string&> thumbnail_created_future;
// kFakeContentBytes needs to outlive the query controller, so initialize it
// here.
const std::vector<uint8_t> kFakeContentBytes({1, 2, 3, 4});
TestLensOverlayQueryController query_controller(
full_image_response_future.GetRepeatingCallback(),
url_response_future.GetRepeatingCallback(), GetSuggestInputsCallback(),
@ -1491,11 +1498,10 @@ TEST_F(LensOverlayQueryControllerTest,
SkBitmap bitmap = CreateNonEmptyBitmap(100, 100);
std::map<std::string, std::string> additional_search_query_params;
std::vector<uint8_t> fake_content_bytes({1, 2, 3, 4});
query_controller.StartQueryFlow(
bitmap, GURL(kTestPageUrl),
std::make_optional<std::string>(kTestPageTitle),
std::vector<lens::mojom::CenterRotatedBoxPtr>(), fake_content_bytes,
std::vector<lens::mojom::CenterRotatedBoxPtr>(), kFakeContentBytes,
lens::MimeType::kPlainText, 0, base::TimeTicks::Now());
ASSERT_TRUE(full_image_response_future.Wait());
query_controller.SendContextualTextQuery(
@ -1601,6 +1607,9 @@ TEST_F(LensOverlayQueryControllerTest,
base::test::TestFuture<lens::proto::LensOverlayUrlResponse>
url_response_future;
base::test::TestFuture<const std::string&> thumbnail_created_future;
// kFakeContentBytes needs to outlive the query controller, so initialize it
// here.
const std::vector<uint8_t> kFakeContentBytes({1, 2, 3, 4});
TestLensOverlayQueryController query_controller(
full_image_response_future.GetRepeatingCallback(),
url_response_future.GetRepeatingCallback(), GetSuggestInputsCallback(),
@ -1629,11 +1638,10 @@ TEST_F(LensOverlayQueryControllerTest,
SkBitmap bitmap = CreateNonEmptyBitmap(100, 100);
std::map<std::string, std::string> additional_search_query_params;
std::vector<uint8_t> fake_content_bytes({1, 2, 3, 4});
query_controller.StartQueryFlow(
bitmap, GURL(kTestPageUrl),
std::make_optional<std::string>(kTestPageTitle),
std::vector<lens::mojom::CenterRotatedBoxPtr>(), fake_content_bytes,
std::vector<lens::mojom::CenterRotatedBoxPtr>(), kFakeContentBytes,
lens::MimeType::kPlainText, 0, base::TimeTicks::Now());
ASSERT_TRUE(full_image_response_future.Wait());
query_controller.SendContextualTextQuery(
@ -1660,6 +1668,10 @@ TEST_F(LensOverlayQueryControllerTest,
base::test::TestFuture<lens::proto::LensOverlayUrlResponse>
url_response_future;
base::test::TestFuture<const std::string&> thumbnail_created_future;
// kFakeContentBytes and kNewFakeContentBytes needs to outlive the query
// controller, so initialize it here.
const std::vector<uint8_t> kFakeContentBytes({1, 2, 3, 4});
const std::vector<uint8_t> kNewFakeContentBytes({5, 6, 7, 8});
TestLensOverlayQueryController query_controller(
full_image_response_future.GetRepeatingCallback(),
url_response_future.GetRepeatingCallback(), GetSuggestInputsCallback(),
@ -1684,7 +1696,6 @@ TEST_F(LensOverlayQueryControllerTest,
SkBitmap bitmap = CreateNonEmptyBitmap(100, 100);
std::map<std::string, std::string> additional_search_query_params;
std::vector<uint8_t> fake_content_bytes({1, 2, 3, 4});
query_controller.StartQueryFlow(
bitmap, GURL(kTestPageUrl),
std::make_optional<std::string>(kTestPageTitle),
@ -1693,7 +1704,7 @@ TEST_F(LensOverlayQueryControllerTest,
// Immediately send a page content update request.
query_controller.SendPageContentUpdateRequest(
fake_content_bytes, lens::MimeType::kPlainText, GURL(kTestPageUrl));
kFakeContentBytes, lens::MimeType::kPlainText, GURL(kTestPageUrl));
ASSERT_TRUE(full_image_response_future.Wait());
@ -1706,9 +1717,8 @@ TEST_F(LensOverlayQueryControllerTest,
1);
// Send a new page content update request.
std::vector<uint8_t> new_fake_content_bytes({5, 6, 7, 8});
query_controller.SendPageContentUpdateRequest(
new_fake_content_bytes, lens::MimeType::kPlainText, GURL(kTestPageUrl));
kNewFakeContentBytes, lens::MimeType::kPlainText, GURL(kTestPageUrl));
// The new page content request should have a different sequence ID.
ASSERT_EQ(query_controller.sent_request_id().sequence_id(), 1);

@ -139,12 +139,19 @@ void TestLensOverlayQueryController::SendPageContentUpdateRequest(
new_content_bytes, new_content_type, new_page_url);
}
void TestLensOverlayQueryController::SendPartialPageContentRequest(
base::span<const std::u16string> partial_content) {
last_sent_partial_content_ = partial_content;
LensOverlayQueryController::SendPartialPageContentRequest(partial_content);
}
void TestLensOverlayQueryController::ResetTestingState() {
last_lens_selection_type_ = lens::UNKNOWN_SELECTION_TYPE;
last_queried_region_.reset();
last_queried_text_.clear();
last_queried_region_bytes_ = std::nullopt;
last_sent_underlying_content_bytes_ = base::span<const uint8_t>();
last_sent_partial_content_ = base::span<const std::u16string>();
last_sent_underlying_content_type_ = lens::MimeType::kUnknown;
last_sent_page_url_ = GURL();
num_interaction_requests_sent_ = 0;

@ -115,7 +115,7 @@ class TestLensOverlayQueryController : public LensOverlayQueryController {
return last_queried_region_bytes_;
}
const base::span<const uint8_t>& last_sent_underlying_content_bytes() const {
base::span<const uint8_t> last_sent_underlying_content_bytes() const {
return last_sent_underlying_content_bytes_;
}
@ -123,6 +123,10 @@ class TestLensOverlayQueryController : public LensOverlayQueryController {
return last_sent_underlying_content_type_;
}
base::span<const std::u16string> last_sent_partial_content() const {
return last_sent_partial_content_;
}
const GURL& last_sent_page_url() const { return last_sent_page_url_; }
const std::optional<lens::mojom::UserAction>& last_user_action() const {
@ -188,6 +192,9 @@ class TestLensOverlayQueryController : public LensOverlayQueryController {
lens::MimeType new_content_type,
GURL new_page_url) override;
void SendPartialPageContentRequest(
base::span<const std::u16string> partial_content) override;
// Resets the test state.
void ResetTestingState();
@ -261,13 +268,15 @@ class TestLensOverlayQueryController : public LensOverlayQueryController {
// The last region bytes sent by the query controller.
std::optional<SkBitmap> last_queried_region_bytes_;
// TODO(367764863) Rewrite to base::raw_span.
RAW_PTR_EXCLUSION base::span<const uint8_t>
last_sent_underlying_content_bytes_;
// The last underlying content bytes sent by the query controller.
base::raw_span<const uint8_t> last_sent_underlying_content_bytes_;
// The last underlying content type sent by the query controller.
lens::MimeType last_sent_underlying_content_type_;
// The last partial content sent by the query controller.
base::raw_span<const std::u16string> last_sent_partial_content_;
// The last page url sent by the query controller.
GURL last_sent_page_url_;

@ -307,6 +307,9 @@ constexpr base::FeatureParam<bool>
constexpr base::FeatureParam<size_t> kLensOverlayFileUploadLimitBytes{
&kLensOverlayContextualSearchbox, "file-upload-limit-bytes", 200000000};
constexpr base::FeatureParam<size_t> kLensOverlayPdfTextCharacterLimit{
&kLensOverlayContextualSearchbox, "pdf-text-character-limit", 2500};
const base::FeatureParam<base::TimeDelta> kLensOverlaySurveyResultsTime{
&kLensOverlaySurvey, "results-time", base::Seconds(1)};
@ -533,6 +536,13 @@ uint32_t GetLensOverlayFileUploadLimitBytes() {
: 0;
}
uint32_t GetLensOverlayPdfSuggestCharacterTarget() {
size_t limit = kLensOverlayPdfTextCharacterLimit.Get();
return base::IsValueInRangeForNumericType<uint32_t>(limit)
? static_cast<uint32_t>(limit)
: 0;
}
bool UsePdfVitParam() {
return kUsePdfVitParam.Get();
}

@ -323,6 +323,14 @@ extern bool GetLensOverlaySendLensVisualInteractionDataForLensSuggest();
COMPONENT_EXPORT(LENS_FEATURES)
extern uint32_t GetLensOverlayFileUploadLimitBytes();
// Returns the number of characters to be retrieved from the PDF for generating
// suggestions. This is a target and not a hard limit. The actual number of
// characters returned may be more than this value since the characters are
// rounded to the nearest page. The actual number of characters may also be
// less than this value if the PDF is too small.
COMPONENT_EXPORT(LENS_FEATURES)
extern uint32_t GetLensOverlayPdfSuggestCharacterTarget();
// Returns whether to use the &vit=pdf param for the search request.
COMPONENT_EXPORT(LENS_FEATURES)
extern bool UsePdfVitParam();

@ -243,6 +243,16 @@ void PDFDocumentHelper::GetPdfBytes(
remote_pdf_client_->GetPdfBytes(size_limit, std::move(callback));
}
void PDFDocumentHelper::GetPageText(
int32_t page_index,
pdf::mojom::PdfListener::GetPageTextCallback callback) {
if (!remote_pdf_client_) {
std::move(callback).Run(std::u16string());
return;
}
remote_pdf_client_->GetPageText(page_index, std::move(callback));
}
void PDFDocumentHelper::OnSelectionEvent(ui::SelectionEventType event) {
// Should be handled by `TouchSelectionControllerClientAura`.
NOTREACHED();

@ -97,6 +97,9 @@ class PDFDocumentHelper
void GetPdfBytes(uint32_t size_limit,
pdf::mojom::PdfListener::GetPdfBytesCallback callback);
void GetPageText(int32_t page_index,
pdf::mojom::PdfListener::GetPageTextCallback callback);
private:
friend class content::DocumentUserData<PDFDocumentHelper>;

@ -45,6 +45,10 @@ class FakePdfListener : public pdf::mojom::PdfListener {
GetPdfBytes,
(uint32_t, GetPdfBytesCallback callback),
(override));
MOCK_METHOD(void,
GetPageText,
(int32_t, GetPageTextCallback callback),
(override));
};
class TestPDFDocumentHelperClient : public PDFDocumentHelperClient {

@ -4,6 +4,7 @@
module pdf.mojom;
import "mojo/public/mojom/base/string16.mojom";
import "services/network/public/mojom/referrer_policy.mojom";
import "ui/gfx/geometry/mojom/geometry.mojom";
import "url/mojom/url.mojom";
@ -33,6 +34,10 @@ interface PdfListener {
// returned in `bytes`.
GetPdfBytes(uint32 size_limit)
=> (GetPdfBytesStatus status, array<uint8> bytes, uint32 page_count);
// Get the text contained on the given page of the PDF. `page_index` should be
// the range [0, # of pages).
GetPageText(int32 page_index) => (mojo_base.mojom.String16 text);
};
// Browser-side interface used by PDF renderers.

@ -1539,6 +1539,15 @@ void PdfViewWebPlugin::GetPdfBytes(uint32_t size_limit,
page_count);
}
void PdfViewWebPlugin::GetPageText(int32_t page_index,
GetPageTextCallback callback) {
if (page_index < 0 || page_index >= engine_->GetNumberOfPages()) {
std::move(callback).Run(std::u16string());
return;
}
std::move(callback).Run(engine_->GetPageText(page_index));
}
bool PdfViewWebPlugin::IsValid() const {
return client_->HasFrame();
}

@ -394,6 +394,7 @@ class PdfViewWebPlugin final : public PDFiumEngineClient,
void SetSelectionBounds(const gfx::PointF& base,
const gfx::PointF& extent) override;
void GetPdfBytes(uint32_t size_limit, GetPdfBytesCallback callback) override;
void GetPageText(int32_t page_index, GetPageTextCallback callback) override;
// UrlLoader::Client:
bool IsValid() const override;