blob: 758a2d389a1154e24dd7bbbb73fce96d3ae968c2 [file] [log] [blame] [edit]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <optional>
#include <utility>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/path_service.h"
#include "base/test/mock_callback.h"
#include "pdf/pdf.h"
#include "pdf/pdfium/pdfium_engine.h"
#include "services/screen_ai/buildflags/buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/pdfium/public/cpp/fpdf_scopers.h"
#include "third_party/pdfium/public/fpdf_text.h"
#include "third_party/pdfium/public/fpdfview.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/test/geometry_util.h"
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
#include <memory>
#include <string>
#include "base/check_op.h"
#include "base/strings/utf_string_conversions.h"
#include "pdf/pdf_progressive_searchifier.h"
#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/skia/include/core/SkBitmap.h"
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
namespace chrome_pdf {
namespace {
class ScopedLibraryInitializer {
public:
ScopedLibraryInitializer() {
InitializeSDK(/*enable_v8=*/true, /*use_skia=*/false,
FontMappingMode::kNoMapping);
}
~ScopedLibraryInitializer() { ShutdownSDK(); }
};
class PDFiumEngineExportsTest : public testing::Test {
public:
PDFiumEngineExportsTest() = default;
PDFiumEngineExportsTest(const PDFiumEngineExportsTest&) = delete;
PDFiumEngineExportsTest& operator=(const PDFiumEngineExportsTest&) = delete;
~PDFiumEngineExportsTest() override = default;
protected:
void SetUp() override {
pdf_data_dir_ = base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
.Append(FILE_PATH_LITERAL("pdf"))
.Append(FILE_PATH_LITERAL("test"))
.Append(FILE_PATH_LITERAL("data"));
}
const base::FilePath& pdf_data_dir() const { return pdf_data_dir_; }
private:
base::FilePath pdf_data_dir_;
};
// Returns all characters in the page.
std::string GetText(base::span<const uint8_t> pdf, int page_index) {
ScopedFPDFDocument document(
FPDF_LoadMemDocument64(pdf.data(), pdf.size(), nullptr));
CHECK(document);
ScopedFPDFPage page(FPDF_LoadPage(document.get(), page_index));
CHECK(page);
ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get()));
CHECK(text_page);
int char_count = FPDFText_CountChars(text_page.get());
CHECK_GE(char_count, 0);
std::u16string text;
text.reserve(char_count);
for (int i = 0; i < char_count; ++i) {
unsigned int char_code = FPDFText_GetUnicode(text_page.get(), i);
text += static_cast<char16_t>(char_code);
}
return base::UTF16ToUTF8(text);
}
std::vector<gfx::RectF> GetTextPositions(base::span<const uint8_t> pdf,
int page_index) {
ScopedFPDFDocument document(
FPDF_LoadMemDocument64(pdf.data(), pdf.size(), nullptr));
CHECK(document);
ScopedFPDFPage page(FPDF_LoadPage(document.get(), page_index));
CHECK(page);
ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get()));
CHECK(text_page);
int char_count = FPDFText_CountChars(text_page.get());
CHECK_GE(char_count, 0);
std::vector<gfx::RectF> positions;
positions.reserve(char_count);
for (int i = 0; i < char_count; ++i) {
double left;
double right;
double bottom;
double top;
CHECK(
FPDFText_GetCharBox(text_page.get(), i, &left, &right, &bottom, &top));
positions.push_back(gfx::RectF(left, bottom, right - left, top - bottom));
}
return positions;
}
} // namespace
TEST_F(PDFiumEngineExportsTest, GetPDFDocInfo) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("hello_world2.pdf"));
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_data.has_value());
ASSERT_TRUE(GetPDFDocInfo(pdf_data.value(), nullptr, nullptr));
int page_count;
float max_page_width;
ASSERT_TRUE(GetPDFDocInfo(pdf_data.value(), &page_count, &max_page_width));
EXPECT_EQ(2, page_count);
EXPECT_DOUBLE_EQ(200.0, max_page_width);
}
TEST_F(PDFiumEngineExportsTest, GetPDFPageSizeByIndex) {
// TODO(thestig): Use a better PDF for this test, as hello_world2.pdf's page
// dimensions are uninteresting.
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("hello_world2.pdf"));
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_data.has_value());
int page_count;
ASSERT_TRUE(GetPDFDocInfo(pdf_data.value(), &page_count, nullptr));
ASSERT_EQ(2, page_count);
for (int page_index = 0; page_index < page_count; ++page_index) {
std::optional<gfx::SizeF> page_size =
GetPDFPageSizeByIndex(pdf_data.value(), page_index);
ASSERT_TRUE(page_size.has_value());
EXPECT_EQ(gfx::SizeF(200, 200), page_size.value());
}
}
TEST_F(PDFiumEngineExportsTest, ConvertPdfPagesToNupPdf) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("rectangles.pdf"));
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_data.has_value());
std::vector<base::span<const uint8_t>> pdf_buffers;
std::vector<uint8_t> output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 1, gfx::Size(612, 792), gfx::Rect(22, 20, 570, 750));
EXPECT_TRUE(output_pdf_buffer.empty());
pdf_buffers.push_back(pdf_data.value());
pdf_buffers.push_back(pdf_data.value());
output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 2, gfx::Size(612, 792), gfx::Rect(22, 20, 0, 750));
EXPECT_TRUE(output_pdf_buffer.empty());
output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 2, gfx::Size(612, 792), gfx::Rect(22, 20, 570, 0));
EXPECT_TRUE(output_pdf_buffer.empty());
output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 2, gfx::Size(612, 792), gfx::Rect(300, 20, 570, 750));
EXPECT_TRUE(output_pdf_buffer.empty());
output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 2, gfx::Size(612, 792), gfx::Rect(22, 400, 570, 750));
EXPECT_TRUE(output_pdf_buffer.empty());
output_pdf_buffer = ConvertPdfPagesToNupPdf(
pdf_buffers, 2, gfx::Size(612, 792), gfx::Rect(22, 20, 570, 750));
ASSERT_GT(output_pdf_buffer.size(), 0U);
base::span<const uint8_t> output_pdf_span =
base::make_span(output_pdf_buffer);
int page_count;
ASSERT_TRUE(GetPDFDocInfo(output_pdf_span, &page_count, nullptr));
ASSERT_EQ(1, page_count);
std::optional<gfx::SizeF> page_size =
GetPDFPageSizeByIndex(output_pdf_span, 0);
ASSERT_TRUE(page_size.has_value());
EXPECT_EQ(gfx::SizeF(792, 612), page_size.value());
}
TEST_F(PDFiumEngineExportsTest, ConvertPdfDocumentToNupPdf) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_data.has_value());
std::vector<uint8_t> output_pdf_buffer = ConvertPdfDocumentToNupPdf(
{}, 1, gfx::Size(612, 792), gfx::Rect(32, 20, 570, 750));
EXPECT_TRUE(output_pdf_buffer.empty());
output_pdf_buffer = ConvertPdfDocumentToNupPdf(
pdf_data.value(), 4, gfx::Size(612, 792), gfx::Rect(22, 20, 570, 750));
ASSERT_GT(output_pdf_buffer.size(), 0U);
base::span<const uint8_t> output_pdf_span =
base::make_span(output_pdf_buffer);
int page_count;
ASSERT_TRUE(GetPDFDocInfo(output_pdf_span, &page_count, nullptr));
ASSERT_EQ(2, page_count);
for (int page_index = 0; page_index < page_count; ++page_index) {
std::optional<gfx::SizeF> page_size =
GetPDFPageSizeByIndex(output_pdf_span, page_index);
ASSERT_TRUE(page_size.has_value());
EXPECT_EQ(gfx::SizeF(612, 792), page_size.value());
}
}
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
constexpr char kExpectedText[] = "Hello World! 你好!🙂";
TEST_F(PDFiumEngineExportsTest, Searchify) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("image_alt_text.pdf"));
std::optional<std::vector<uint8_t>> pdf_buffer =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_buffer.has_value());
base::MockCallback<base::RepeatingCallback<
screen_ai::mojom::VisualAnnotationPtr(const SkBitmap&)>>
perform_ocr_callback;
// The PDF has 3 images, but one of them has a matrix that results in an image
// with size 0, and its image cannot be extracted.
EXPECT_CALL(perform_ocr_callback, Run)
.Times(2)
.WillRepeatedly([](const SkBitmap& bitmap) {
CHECK(!bitmap.empty());
auto annotation = screen_ai::mojom::VisualAnnotation::New();
auto line_box = screen_ai::mojom::LineBox::New();
line_box->baseline_box = gfx::Rect(0, 0, 100, 100);
line_box->baseline_box_angle = 0;
line_box->bounding_box = gfx::Rect(0, 0, 100, 100);
line_box->bounding_box_angle = 0;
auto word_box = screen_ai::mojom::WordBox::New();
word_box->word = kExpectedText;
word_box->bounding_box = gfx::Rect(0, 0, 100, 100);
word_box->bounding_box_angle = 0;
line_box->words.push_back(std::move(word_box));
annotation->lines.push_back(std::move(line_box));
return annotation;
});
{
ScopedLibraryInitializer initializer;
EXPECT_THAT(GetText(*pdf_buffer, 0),
testing::Not(testing::HasSubstr(kExpectedText)));
}
std::vector<uint8_t> output_pdf_buffer =
Searchify(*pdf_buffer, perform_ocr_callback.Get());
ASSERT_FALSE(output_pdf_buffer.empty());
{
ScopedLibraryInitializer initializer;
EXPECT_THAT(GetText(output_pdf_buffer, 0),
testing::HasSubstr(kExpectedText));
}
}
TEST_F(PDFiumEngineExportsTest, SearchifyBroken) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("image_alt_text.pdf"));
std::optional<std::vector<uint8_t>> pdf_buffer =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_buffer.has_value());
auto broken_perform_ocr_callback =
base::BindRepeating([](const SkBitmap& bitmap) {
auto annotation = screen_ai::mojom::VisualAnnotationPtr();
// Simulate the scenarios that fail to set the value of `annotation`.
CHECK(!annotation);
return annotation;
});
std::vector<uint8_t> output_pdf_buffer =
Searchify(*pdf_buffer, std::move(broken_perform_ocr_callback));
EXPECT_TRUE(output_pdf_buffer.empty());
}
TEST_F(PDFiumEngineExportsTest, SearchifyBigImage) {
base::FilePath pdf_path =
pdf_data_dir().Append(FILE_PATH_LITERAL("big_image.pdf"));
std::optional<std::vector<uint8_t>> pdf_buffer =
base::ReadFileToBytes(pdf_path);
ASSERT_TRUE(pdf_buffer.has_value());
base::MockCallback<base::RepeatingCallback<
screen_ai::mojom::VisualAnnotationPtr(const SkBitmap&)>>
perform_ocr_callback;
EXPECT_CALL(perform_ocr_callback, Run).WillOnce([](const SkBitmap& bitmap) {
// The returned image is 267x267 as OCR needs at most 300 DPI.
EXPECT_EQ(267, bitmap.width());
EXPECT_EQ(267, bitmap.height());
auto annotation = screen_ai::mojom::VisualAnnotation::New();
constexpr gfx::Rect kRect1(0, 30, 10, 5);
auto line_box1 = screen_ai::mojom::LineBox::New();
line_box1->baseline_box = kRect1;
line_box1->baseline_box_angle = 0;
line_box1->bounding_box = kRect1;
line_box1->bounding_box_angle = 0;
auto word_box1 = screen_ai::mojom::WordBox::New();
word_box1->word = "a";
word_box1->bounding_box = kRect1;
word_box1->bounding_box_angle = 0;
line_box1->words.push_back(std::move(word_box1));
annotation->lines.push_back(std::move(line_box1));
constexpr gfx::Rect kRect2(200, 210, 67, 57);
auto line_box2 = screen_ai::mojom::LineBox::New();
line_box2->baseline_box = kRect2;
line_box2->baseline_box_angle = 0;
line_box2->bounding_box = kRect2;
line_box2->bounding_box_angle = 0;
auto word_box2 = screen_ai::mojom::WordBox::New();
word_box2->word = "b";
word_box2->bounding_box = kRect2;
word_box2->bounding_box_angle = 0;
line_box2->words.push_back(std::move(word_box2));
annotation->lines.push_back(std::move(line_box2));
return annotation;
});
{
ScopedLibraryInitializer initializer;
EXPECT_TRUE(GetTextPositions(*pdf_buffer, 0).empty());
}
std::vector<uint8_t> output_pdf_buffer =
Searchify(*pdf_buffer, perform_ocr_callback.Get());
ASSERT_FALSE(output_pdf_buffer.empty());
{
constexpr float kFloatTolerance = 0.0001f;
ScopedLibraryInitializer initializer;
// The middle 2 positions are for auto-generated "\r\n".
const std::vector<gfx::RectF> positions =
GetTextPositions(output_pdf_buffer, 0);
ASSERT_EQ(4u, positions.size());
EXPECT_RECTF_NEAR(gfx::RectF(0, 55.6105f, 2.397f, 1.1985f), positions[0],
kFloatTolerance);
EXPECT_TRUE(positions[1].IsEmpty());
EXPECT_TRUE(positions[2].IsEmpty());
EXPECT_RECTF_NEAR(gfx::RectF(47.9401f, 0, 16.0599f, 13.6629f), positions[3],
kFloatTolerance);
}
}
TEST_F(PDFiumEngineExportsTest, PdfProgressiveSearchifier) {
std::unique_ptr<PdfProgressiveSearchifier> progressive_searchifier =
CreateProgressiveSearchifier();
std::vector<uint8_t> zero_page_pdf = progressive_searchifier->Save();
ASSERT_GT(zero_page_pdf.size(), 0U);
auto annotation = screen_ai::mojom::VisualAnnotation::New();
SkBitmap bitmap_1x1;
bitmap_1x1.allocN32Pixels(1, 1);
SkBitmap bitmap_100x100;
bitmap_100x100.allocN32Pixels(100, 100);
progressive_searchifier->AddPage(bitmap_1x1, 0, annotation->Clone());
// Pages: [1x1]
std::vector<uint8_t> one_page_pdf = progressive_searchifier->Save();
ASSERT_GT(one_page_pdf.size(), 0U);
progressive_searchifier->AddPage(bitmap_100x100, 1, annotation->Clone());
// Pages: [1x1, 100x100]
std::vector<uint8_t> two_page_pdf = progressive_searchifier->Save();
ASSERT_GT(two_page_pdf.size(), 0U);
progressive_searchifier->AddPage(bitmap_1x1, 0, annotation->Clone());
progressive_searchifier->AddPage(bitmap_1x1, 2, annotation->Clone());
// Pages: [1x1, 100x100, 1x1]
std::vector<uint8_t> three_page_pdf_replaced =
progressive_searchifier->Save();
ASSERT_GT(three_page_pdf_replaced.size(), 0U);
progressive_searchifier->DeletePage(1);
progressive_searchifier->DeletePage(1);
// Pages: [1x1]
std::vector<uint8_t> one_page_pdf_deleted = progressive_searchifier->Save();
ASSERT_GT(one_page_pdf_deleted.size(), 0U);
progressive_searchifier.reset();
int page_count;
float max_page_width;
ASSERT_TRUE(GetPDFDocInfo(zero_page_pdf, &page_count, &max_page_width));
EXPECT_EQ(page_count, 0);
EXPECT_EQ(max_page_width, 0);
ASSERT_TRUE(GetPDFDocInfo(one_page_pdf, &page_count, &max_page_width));
EXPECT_EQ(page_count, 1);
EXPECT_EQ(max_page_width, bitmap_1x1.width());
ASSERT_TRUE(GetPDFDocInfo(two_page_pdf, &page_count, &max_page_width));
EXPECT_EQ(page_count, 2);
EXPECT_EQ(max_page_width, bitmap_100x100.width());
ASSERT_TRUE(
GetPDFDocInfo(three_page_pdf_replaced, &page_count, &max_page_width));
EXPECT_EQ(page_count, 3);
EXPECT_EQ(max_page_width, bitmap_100x100.width());
ASSERT_TRUE(
GetPDFDocInfo(one_page_pdf_deleted, &page_count, &max_page_width));
EXPECT_EQ(page_count, 1);
EXPECT_EQ(max_page_width, bitmap_1x1.width());
}
TEST_F(PDFiumEngineExportsTest, PdfProgressiveSearchifierText) {
std::unique_ptr<PdfProgressiveSearchifier> progressive_searchifier =
CreateProgressiveSearchifier();
SkBitmap bitmap;
bitmap.allocN32Pixels(100, 100);
auto annotation = screen_ai::mojom::VisualAnnotation::New();
auto line_box = screen_ai::mojom::LineBox::New();
line_box->baseline_box = gfx::Rect(0, 0, 100, 100);
line_box->baseline_box_angle = 0;
line_box->bounding_box = gfx::Rect(0, 0, 100, 100);
line_box->bounding_box_angle = 0;
auto word_box = screen_ai::mojom::WordBox::New();
word_box->word = kExpectedText;
word_box->bounding_box = gfx::Rect(0, 0, 100, 100);
word_box->bounding_box_angle = 0;
line_box->words.push_back(std::move(word_box));
annotation->lines.push_back(std::move(line_box));
progressive_searchifier->AddPage(bitmap, 0, std::move(annotation));
std::vector<uint8_t> pdf = progressive_searchifier->Save();
progressive_searchifier.reset();
{
ScopedLibraryInitializer initializer;
EXPECT_THAT(GetText(pdf, 0), testing::HasSubstr(kExpectedText));
}
}
#endif // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
} // namespace chrome_pdf