blob: 1f03bad8a8ffd2ad26da4f57a5b52891294ca097 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "content/public/test/browser_test.h"
#include "headless/app/headless_shell_switches.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/devtools/domains/io.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/test/headless_browser_test.h"
#include "pdf/pdf.h"
#include "printing/buildflags/buildflags.h"
#include "printing/pdf_render_settings.h"
#include "printing/units.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size_f.h"
namespace headless {
namespace {
// Utility class to render the specified PDF page into a bitmap and
// inspect the resulting pixels.
class PDFPageBitmap {
public:
static constexpr int kColorChannels = 4;
static constexpr int kDpi = 300;
PDFPageBitmap() = default;
~PDFPageBitmap() = default;
void Render(base::span<const uint8_t> pdf_span, int page_index) {
absl::optional<gfx::SizeF> page_size_in_points =
chrome_pdf::GetPDFPageSizeByIndex(pdf_span, page_index);
ASSERT_TRUE(page_size_in_points.has_value());
gfx::SizeF page_size_in_pixels =
gfx::ScaleSize(page_size_in_points.value(),
static_cast<float>(kDpi) / printing::kPointsPerInch);
gfx::Rect page_rect(gfx::ToCeiledSize(page_size_in_pixels));
constexpr chrome_pdf::RenderOptions options = {
.stretch_to_bounds = false,
.keep_aspect_ratio = true,
.autorotate = true,
.use_color = true,
.render_device_type = chrome_pdf::RenderDeviceType::kPrinter,
};
bitmap_size_ = page_rect.size();
bitmap_data_.resize(kColorChannels * bitmap_size_.GetArea());
ASSERT_TRUE(chrome_pdf::RenderPDFPageToBitmap(
pdf_span, page_index, bitmap_data_.data(), bitmap_size_,
gfx::Size(kDpi, kDpi), options));
}
uint32_t GetPixelRGB(int x, int y) {
int pixel_index =
bitmap_size_.width() * y * kColorChannels + x * kColorChannels;
return bitmap_data_[pixel_index + 0] // B
| (bitmap_data_[pixel_index + 1] << 8) // G
| (bitmap_data_[pixel_index + 2] << 16); // R
}
protected:
std::vector<uint8_t> bitmap_data_;
gfx::Size bitmap_size_;
};
} // namespace
class HeadlessPDFPagesBrowserTest : public HeadlessAsyncDevTooledBrowserTest {
public:
const double kPaperWidth = 10;
const double kPaperHeight = 15;
const double kDocHeight = 50;
// Number of color channels in a BGRA bitmap.
const int kColorChannels = 4;
const int kDpi = 300;
void RunDevTooledTest() override {
std::string height_expression = "document.body.style.height = '" +
base::NumberToString(kDocHeight) + "in'";
std::unique_ptr<runtime::EvaluateParams> params =
runtime::EvaluateParams::Builder()
.SetExpression("document.body.style.background = '#123456';" +
height_expression)
.Build();
devtools_client_->GetRuntime()->Evaluate(
std::move(params),
base::BindOnce(&HeadlessPDFPagesBrowserTest::OnPageSetupCompleted,
base::Unretained(this)));
}
void OnPageSetupCompleted(std::unique_ptr<runtime::EvaluateResult> result) {
devtools_client_->GetPage()->GetExperimental()->PrintToPDF(
page::PrintToPDFParams::Builder()
.SetPrintBackground(true)
.SetPaperHeight(kPaperHeight)
.SetPaperWidth(kPaperWidth)
.SetMarginTop(0)
.SetMarginBottom(0)
.SetMarginLeft(0)
.SetMarginRight(0)
.Build(),
base::BindOnce(&HeadlessPDFPagesBrowserTest::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(std::unique_ptr<page::PrintToPDFResult> result) {
protocol::Binary pdf_data = result->GetData();
EXPECT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(pdf_data.data(), pdf_data.size());
int num_pages;
EXPECT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages, nullptr));
EXPECT_EQ(std::ceil(kDocHeight / kPaperHeight), num_pages);
constexpr chrome_pdf::RenderOptions options = {
.stretch_to_bounds = false,
.keep_aspect_ratio = true,
.autorotate = true,
.use_color = true,
.render_device_type = chrome_pdf::RenderDeviceType::kPrinter,
};
for (int i = 0; i < num_pages; i++) {
absl::optional<gfx::SizeF> size_in_points =
chrome_pdf::GetPDFPageSizeByIndex(pdf_span, i);
ASSERT_TRUE(size_in_points.has_value());
EXPECT_EQ(static_cast<int>(size_in_points.value().width()),
static_cast<int>(kPaperWidth * printing::kPointsPerInch));
EXPECT_EQ(static_cast<int>(size_in_points.value().height()),
static_cast<int>(kPaperHeight * printing::kPointsPerInch));
gfx::Rect rect(kPaperWidth * kDpi, kPaperHeight * kDpi);
printing::PdfRenderSettings settings(
rect, gfx::Point(), gfx::Size(kDpi, kDpi), options.autorotate,
options.use_color, printing::PdfRenderSettings::Mode::NORMAL);
std::vector<uint8_t> page_bitmap_data(kColorChannels *
settings.area.size().GetArea());
EXPECT_TRUE(chrome_pdf::RenderPDFPageToBitmap(
pdf_span, i, page_bitmap_data.data(), settings.area.size(),
settings.dpi, options));
EXPECT_EQ(0x56, page_bitmap_data[0]); // B
EXPECT_EQ(0x34, page_bitmap_data[1]); // G
EXPECT_EQ(0x12, page_bitmap_data[2]); // R
}
FinishAsynchronousTest();
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessPDFPagesBrowserTest);
class HeadlessPDFStreamBrowserTest : public HeadlessAsyncDevTooledBrowserTest {
public:
const double kPaperWidth = 10;
const double kPaperHeight = 15;
const double kDocHeight = 50;
void RunDevTooledTest() override {
std::string height_expression = "document.body.style.height = '" +
base::NumberToString(kDocHeight) + "in'";
std::unique_ptr<runtime::EvaluateParams> params =
runtime::EvaluateParams::Builder()
.SetExpression(height_expression)
.Build();
devtools_client_->GetRuntime()->Evaluate(
std::move(params),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnPageSetupCompleted,
base::Unretained(this)));
}
void OnPageSetupCompleted(std::unique_ptr<runtime::EvaluateResult> result) {
devtools_client_->GetPage()->GetExperimental()->PrintToPDF(
page::PrintToPDFParams::Builder()
.SetTransferMode(page::PrintToPDFTransferMode::RETURN_AS_STREAM)
.SetPaperHeight(kPaperHeight)
.SetPaperWidth(kPaperWidth)
.SetMarginTop(0)
.SetMarginBottom(0)
.SetMarginLeft(0)
.SetMarginRight(0)
.Build(),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(std::unique_ptr<page::PrintToPDFResult> result) {
EXPECT_EQ(result->GetData().size(), 0U);
stream_ = result->GetStream();
devtools_client_->GetIO()->Read(
stream_, base::BindOnce(&HeadlessPDFStreamBrowserTest::OnReadChunk,
base::Unretained(this)));
}
void OnReadChunk(std::unique_ptr<io::ReadResult> result) {
base64_data_ = base64_data_ + result->GetData();
if (result->GetEof()) {
OnPDFLoaded();
} else {
devtools_client_->GetIO()->Read(
stream_, base::BindOnce(&HeadlessPDFStreamBrowserTest::OnReadChunk,
base::Unretained(this)));
}
}
void OnPDFLoaded() {
EXPECT_GT(base64_data_.size(), 0U);
bool success;
protocol::Binary pdf_data =
protocol::Binary::fromBase64(base64_data_, &success);
EXPECT_TRUE(success);
EXPECT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(pdf_data.data(), pdf_data.size());
int num_pages;
EXPECT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages, nullptr));
EXPECT_EQ(std::ceil(kDocHeight / kPaperHeight), num_pages);
absl::optional<bool> tagged = chrome_pdf::IsPDFDocTagged(pdf_span);
ASSERT_TRUE(tagged.has_value());
EXPECT_FALSE(tagged.value());
FinishAsynchronousTest();
}
private:
std::string stream_;
std::string base64_data_;
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessPDFStreamBrowserTest);
class HeadlessPDFBrowserTestBase : public HeadlessAsyncDevTooledBrowserTest,
public page::Observer {
public:
void RunDevTooledTest() override {
ASSERT_TRUE(embedded_test_server()->Start());
devtools_client_->GetPage()->AddObserver(this);
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
devtools_client_->GetPage()->Enable(run_loop.QuitClosure());
run_loop.Run();
devtools_client_->GetPage()->Navigate(
embedded_test_server()->GetURL(GetUrl()).spec());
}
void OnLoadEventFired(const page::LoadEventFiredParams&) override {
devtools_client_->GetPage()->GetExperimental()->PrintToPDF(
GetPrintToPDFParams(),
base::BindOnce(&HeadlessPDFBrowserTestBase::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(std::unique_ptr<page::PrintToPDFResult> result) {
if (result) {
protocol::Binary pdf_data = result->GetData();
ASSERT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(pdf_data.data(), pdf_data.size());
int num_pages;
ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages, nullptr));
OnPDFReady(pdf_span, num_pages);
} else {
OnPDFFailure();
}
FinishAsynchronousTest();
}
virtual const char* GetUrl() = 0;
virtual std::unique_ptr<page::PrintToPDFParams> GetPrintToPDFParams() {
return page::PrintToPDFParams::Builder()
.SetPrintBackground(true)
.SetPaperHeight(41)
.SetPaperWidth(41)
.SetMarginTop(0)
.SetMarginBottom(0)
.SetMarginLeft(0)
.SetMarginRight(0)
.Build();
}
virtual void OnPDFReady(base::span<const uint8_t> pdf_span,
int num_pages) = 0;
virtual void OnPDFFailure() { EXPECT_TRUE(false); }
};
class HeadlessPDFPageSizeRoundingBrowserTest
: public HeadlessPDFBrowserTestBase {
public:
const char* GetUrl() override { return "/red_square.html"; }
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(1));
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessPDFPageSizeRoundingBrowserTest);
class HeadlessPDFPageRangesBrowserTest
: public HeadlessPDFBrowserTestBase,
public testing::WithParamInterface<std::tuple<const char*, int>> {
public:
const char* GetUrl() override { return "/lorem_ipsum.html"; }
std::unique_ptr<page::PrintToPDFParams> GetPrintToPDFParams() override {
return page::PrintToPDFParams::Builder()
.SetPaperHeight(8.5)
.SetPaperWidth(11)
.SetMarginTop(0.5)
.SetMarginBottom(0.5)
.SetMarginLeft(0.5)
.SetMarginRight(0.5)
.SetPageRanges(page_ranges())
.Build();
}
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(expected_page_count()));
}
void OnPDFFailure() override {
EXPECT_THAT(-1, testing::Eq(expected_page_count()));
}
std::string page_ranges() { return std::get<0>(GetParam()); }
int expected_page_count() { return std::get<1>(GetParam()); }
};
INSTANTIATE_TEST_SUITE_P(All,
HeadlessPDFPageRangesBrowserTest,
testing::Values(std::make_tuple("1-9", 4),
std::make_tuple("1-3", 3),
std::make_tuple("2-4", 3),
std::make_tuple("4-9", 1),
std::make_tuple("5-9", -1),
std::make_tuple("9-5", -1),
std::make_tuple("abc", -1)));
HEADLESS_ASYNC_DEVTOOLED_TEST_P(HeadlessPDFPageRangesBrowserTest);
class HeadlessPDFOOPIFBrowserTest : public HeadlessPDFBrowserTestBase {
public:
const char* GetUrl() override { return "/oopif.html"; }
std::unique_ptr<page::PrintToPDFParams> GetPrintToPDFParams() override {
return page::PrintToPDFParams::Builder()
.SetPrintBackground(true)
.SetPaperHeight(10)
.SetPaperWidth(15)
.SetMarginTop(0)
.SetMarginBottom(0)
.SetMarginLeft(0)
.SetMarginRight(0)
.Build();
}
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(1));
PDFPageBitmap page_image;
page_image.Render(pdf_span, 0);
// Expect red iframe pixel at 1 inch into the page.
EXPECT_EQ(page_image.GetPixelRGB(1 * PDFPageBitmap::kDpi,
1 * PDFPageBitmap::kDpi),
0xFF0000u);
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessPDFOOPIFBrowserTest);
#if BUILDFLAG(ENABLE_TAGGED_PDF)
const char kExpectedStructTreeJSON[] = R"({
"lang": "en",
"type": "Document",
"~children": [ {
"type": "H1",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "P",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "L",
"~children": [ {
"type": "LI",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "LI",
"~children": [ {
"type": "NonStruct"
} ]
} ]
}, {
"type": "Div",
"~children": [ {
"type": "Link",
"~children": [ {
"type": "NonStruct"
} ]
} ]
}, {
"type": "Table",
"~children": [ {
"type": "TR",
"~children": [ {
"type": "TH",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "TH",
"~children": [ {
"type": "NonStruct"
} ]
} ]
}, {
"type": "TR",
"~children": [ {
"type": "TD",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "TD",
"~children": [ {
"type": "NonStruct"
} ]
} ]
} ]
}, {
"type": "H2",
"~children": [ {
"type": "NonStruct"
} ]
}, {
"type": "Div",
"~children": [ {
"alt": "Car at the beach",
"type": "Figure"
} ]
}, {
"lang": "fr",
"type": "P",
"~children": [ {
"type": "NonStruct"
} ]
} ]
}
)";
const char kExpectedFigureOnlyStructTreeJSON[] = R"({
"lang": "en",
"type": "Document",
"~children": [ {
"type": "Figure",
"~children": [ {
"alt": "Sample SVG image",
"type": "Figure"
}, {
"type": "NonStruct",
"~children": [ {
"type": "NonStruct"
} ]
} ]
} ]
}
)";
const char kExpectedFigureRoleOnlyStructTreeJSON[] = R"({
"lang": "en",
"type": "Document",
"~children": [ {
"alt": "Text that describes the figure.",
"type": "Figure",
"~children": [ {
"alt": "Sample SVG image",
"type": "Figure"
}, {
"type": "P",
"~children": [ {
"type": "NonStruct"
} ]
} ]
} ]
}
)";
const char kExpectedImageOnlyStructTreeJSON[] = R"({
"lang": "en",
"type": "Document",
"~children": [ {
"type": "Div",
"~children": [ {
"alt": "Sample SVG image",
"type": "Figure"
} ]
} ]
}
)";
const char kExpectedImageRoleOnlyStructTreeJSON[] = R"({
"lang": "en",
"type": "Document",
"~children": [ {
"alt": "That cat is so cute",
"type": "Figure",
"~children": [ {
"type": "P",
"~children": [ {
"type": "NonStruct"
} ]
} ]
} ]
}
)";
struct TaggedPDFTestData {
const char* url;
const char* expected_json;
};
constexpr TaggedPDFTestData kTaggedPDFTestData[] = {
{"/structured_doc.html", kExpectedStructTreeJSON},
{"/structured_doc_only_figure.html", kExpectedFigureOnlyStructTreeJSON},
{"/structured_doc_only_figure_role.html",
kExpectedFigureRoleOnlyStructTreeJSON},
{"/structured_doc_only_image.html", kExpectedImageOnlyStructTreeJSON},
{"/structured_doc_only_image_role.html",
kExpectedImageRoleOnlyStructTreeJSON},
};
class HeadlessTaggedPDFBrowserTest
: public HeadlessPDFBrowserTestBase,
public ::testing::WithParamInterface<TaggedPDFTestData> {
public:
const char* GetUrl() override { return GetParam().url; }
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(1));
absl::optional<bool> tagged = chrome_pdf::IsPDFDocTagged(pdf_span);
EXPECT_THAT(tagged, testing::Optional(true));
constexpr int kFirstPage = 0;
base::Value struct_tree =
chrome_pdf::GetPDFStructTreeForPage(pdf_span, kFirstPage);
std::string json;
base::JSONWriter::WriteWithOptions(
struct_tree, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json);
// Map Windows line endings to Unix by removing '\r'.
base::RemoveChars(json, "\r", &json);
EXPECT_EQ(GetParam().expected_json, json);
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_P(HeadlessTaggedPDFBrowserTest);
INSTANTIATE_TEST_SUITE_P(All,
HeadlessTaggedPDFBrowserTest,
::testing::ValuesIn(kTaggedPDFTestData));
class HeadlessTaggedPDFDisabledBrowserTest
: public HeadlessPDFBrowserTestBase,
public ::testing::WithParamInterface<TaggedPDFTestData> {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessPDFBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDisablePDFTagging);
}
const char* GetUrl() override { return GetParam().url; }
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(1));
absl::optional<bool> tagged = chrome_pdf::IsPDFDocTagged(pdf_span);
EXPECT_THAT(tagged, testing::Optional(false));
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_P(HeadlessTaggedPDFDisabledBrowserTest);
INSTANTIATE_TEST_SUITE_P(All,
HeadlessTaggedPDFDisabledBrowserTest,
::testing::ValuesIn(kTaggedPDFTestData));
#endif // BUILDFLAG(ENABLE_TAGGED_PDF)
} // namespace headless