blob: b1ceb60ffe4a65815640aba3c89dbd92531cf7ac [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// 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/base64.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "content/public/test/browser_test.h"
#include "headless/public/switches.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "headless/test/headless_devtooled_browsertest.h"
#include "headless/test/pdf_utils.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 "third_party/inspector_protocol/crdtp/dispatch.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/size_f.h"
namespace headless {
class HeadlessPDFPagesBrowserTest : public HeadlessDevTooledBrowserTest {
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 script =
"document.body.style.background = '#123456';"
"document.body.style.height = '" +
base::NumberToString(kDocHeight) + "in'";
devtools_client_.SendCommand(
"Runtime.evaluate", Param("expression", script),
base::BindOnce(&HeadlessPDFPagesBrowserTest::OnPageSetupCompleted,
base::Unretained(this)));
}
void OnPageSetupCompleted(base::Value::Dict) {
base::Value::Dict params;
params.Set("printBackground", true);
params.Set("paperHeight", kPaperHeight);
params.Set("paperWidth", kPaperWidth);
params.Set("marginTop", 0);
params.Set("marginBottom", 0);
params.Set("marginLeft", 0);
params.Set("marginRight", 0);
devtools_client_.SendCommand(
"Page.printToPDF", std::move(params),
base::BindOnce(&HeadlessPDFPagesBrowserTest::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(base::Value::Dict result) {
std::string pdf_data_base64 = DictString(result, "result.data");
ASSERT_FALSE(pdf_data_base64.empty());
std::string pdf_data;
ASSERT_TRUE(base::Base64Decode(pdf_data_base64, &pdf_data));
EXPECT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(
reinterpret_cast<const uint8_t*>(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_DEVTOOLED_TEST_F(HeadlessPDFPagesBrowserTest);
class HeadlessPDFStreamBrowserTest : public HeadlessDevTooledBrowserTest {
public:
const double kPaperWidth = 10;
const double kPaperHeight = 15;
const double kDocHeight = 50;
void RunDevTooledTest() override {
std::string script = "document.body.style.height = '" +
base::NumberToString(kDocHeight) + "in'";
devtools_client_.SendCommand(
"Runtime.evaluate", Param("expression", script),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnPageSetupCompleted,
base::Unretained(this)));
}
void OnPageSetupCompleted(base::Value::Dict) {
base::Value::Dict params;
params.Set("transferMode", "ReturnAsStream");
params.Set("printBackground", true);
params.Set("paperHeight", kPaperHeight);
params.Set("paperWidth", kPaperWidth);
params.Set("marginTop", 0);
params.Set("marginBottom", 0);
params.Set("marginLeft", 0);
params.Set("marginRight", 0);
devtools_client_.SendCommand(
"Page.printToPDF", std::move(params),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(base::Value::Dict result) {
EXPECT_THAT(result, DictHasValue("result.data", std::string()));
stream_ = DictString(result, "result.stream");
devtools_client_.SendCommand(
"IO.read", Param("handle", std::string(stream_)),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnReadChunk,
base::Unretained(this)));
}
void OnReadChunk(base::Value::Dict result) {
EXPECT_THAT(result, DictHasValue("result.base64Encoded", true));
const std::string base64_pdf_data_chunk = DictString(result, "result.data");
base64_pdf_data_.append(base64_pdf_data_chunk);
if (DictBool(result, "result.eof")) {
OnPDFLoaded();
} else {
devtools_client_.SendCommand(
"IO.read", Param("handle", std::string(stream_)),
base::BindOnce(&HeadlessPDFStreamBrowserTest::OnReadChunk,
base::Unretained(this)));
}
}
void OnPDFLoaded() {
EXPECT_GT(base64_pdf_data_.size(), 0U);
std::string pdf_data;
ASSERT_TRUE(base::Base64Decode(base64_pdf_data_, &pdf_data));
EXPECT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(
reinterpret_cast<const uint8_t*>(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_pdf_data_;
};
HEADLESS_DEVTOOLED_TEST_F(HeadlessPDFStreamBrowserTest);
class HeadlessPDFBrowserTestBase : public HeadlessDevTooledBrowserTest {
public:
void RunDevTooledTest() override {
ASSERT_TRUE(embedded_test_server()->Start());
devtools_client_.AddEventHandler(
"Page.loadEventFired",
base::BindRepeating(&HeadlessPDFBrowserTestBase::OnLoadEventFired,
base::Unretained(this)));
SendCommandSync(devtools_client_, "Page.enable");
devtools_client_.SendCommand(
"Page.navigate",
Param("url", embedded_test_server()->GetURL(GetUrl()).spec()));
}
void OnLoadEventFired(const base::Value::Dict&) {
devtools_client_.SendCommand(
"Page.printToPDF", GetPrintToPDFParams(),
base::BindOnce(&HeadlessPDFBrowserTestBase::OnPDFCreated,
base::Unretained(this)));
}
void OnPDFCreated(base::Value::Dict result) {
absl::optional<int> error_code = result.FindIntByDottedPath("error.code");
const std::string* error_message =
result.FindStringByDottedPath("error.message");
ASSERT_EQ(error_code.has_value(), !!error_message);
if (error_code || error_message) {
OnPDFFailure(*error_code, *error_message);
} else {
std::string pdf_data_base64 = DictString(result, "result.data");
ASSERT_FALSE(pdf_data_base64.empty());
std::string pdf_data;
ASSERT_TRUE(base::Base64Decode(pdf_data_base64, &pdf_data));
ASSERT_GT(pdf_data.size(), 0U);
auto pdf_span = base::make_span(
reinterpret_cast<const uint8_t*>(pdf_data.data()), pdf_data.size());
int num_pages;
ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages, nullptr));
OnPDFReady(pdf_span, num_pages);
}
FinishAsynchronousTest();
}
virtual const char* GetUrl() = 0;
virtual base::Value::Dict GetPrintToPDFParams() {
base::Value::Dict params;
params.Set("printBackground", true);
params.Set("paperHeight", 41);
params.Set("paperWidth", 41);
params.Set("marginTop", 0);
params.Set("marginBottom", 0);
params.Set("marginLeft", 0);
params.Set("marginRight", 0);
return params;
}
virtual void OnPDFReady(base::span<const uint8_t> pdf_span,
int num_pages) = 0;
virtual void OnPDFFailure(int code, const std::string& message) {
ADD_FAILURE() << "code=" << code << " message: " << message;
}
};
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_DEVTOOLED_TEST_F(HeadlessPDFPageSizeRoundingBrowserTest);
class HeadlessPDFPageRangesBrowserTest
: public HeadlessPDFBrowserTestBase,
public testing::WithParamInterface<
std::tuple<const char*, int, const char*>> {
public:
const char* GetUrl() override { return "/lorem_ipsum.html"; }
base::Value::Dict GetPrintToPDFParams() override {
base::Value::Dict params;
params.Set("pageRanges", page_ranges());
params.Set("paperHeight", 8.5);
params.Set("paperWidth", 11);
params.Set("marginTop", 0.5);
params.Set("marginBottom", 0.5);
params.Set("marginLeft", 0.5);
params.Set("marginRight", 0.5);
return params;
}
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(expected_page_count()));
}
void OnPDFFailure(int code, const std::string& message) override {
EXPECT_THAT(-1, testing::Eq(expected_page_count()));
EXPECT_THAT(
code, testing::Eq(static_cast<int>(crdtp::DispatchCode::SERVER_ERROR)));
EXPECT_THAT(message, testing::Eq(expected_error_message()));
}
std::string page_ranges() { return std::get<0>(GetParam()); }
int expected_page_count() { return std::get<1>(GetParam()); }
std::string expected_error_message() { return std::get<2>(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, "Page range exceeds page count"),
std::make_tuple("9-5", -1, "Page range is invalid (start > end)"),
std::make_tuple("abc", -1, "Page range syntax error")));
HEADLESS_DEVTOOLED_TEST_P(HeadlessPDFPageRangesBrowserTest);
class HeadlessPDFOOPIFBrowserTest : public HeadlessPDFBrowserTestBase {
public:
const char* GetUrl() override { return "/oopif.html"; }
base::Value::Dict GetPrintToPDFParams() override {
base::Value::Dict params;
params.Set("printBackground", true);
params.Set("paperHeight", 10);
params.Set("paperWidth", 15);
params.Set("marginTop", 0);
params.Set("marginBottom", 0);
params.Set("marginLeft", 0);
params.Set("marginRight", 0);
return params;
}
void OnPDFReady(base::span<const uint8_t> pdf_span, int num_pages) override {
EXPECT_THAT(num_pages, testing::Eq(1));
PDFPageBitmap page_image;
ASSERT_TRUE(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_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_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_DEVTOOLED_TEST_P(HeadlessTaggedPDFDisabledBrowserTest);
INSTANTIATE_TEST_SUITE_P(All,
HeadlessTaggedPDFDisabledBrowserTest,
::testing::ValuesIn(kTaggedPDFTestData));
#endif // BUILDFLAG(ENABLE_TAGGED_PDF)
} // namespace headless