| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/containers/span.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_span.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/values_test_util.h" |
| #include "base/values.h" |
| #include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/headless/test/pdf_utils.h" |
| #include "components/printing/browser/print_manager_utils.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "pdf/pdf.h" |
| #include "printing/pdf_render_settings.h" |
| #include "printing/units.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/size_conversions.h" |
| |
| using DevToolsProtocolTest = DevToolsProtocolTestBase; |
| |
| namespace { |
| |
| class PrintToPdfProtocolTest : public DevToolsProtocolTest, |
| public testing::WithParamInterface<bool> { |
| protected: |
| static constexpr double kPaperWidth = 10; |
| static constexpr double kPaperHeight = 15; |
| static constexpr int kDpi = headless::PDFPageBitmap::kDpi; |
| |
| bool headless() const { return GetParam(); } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| DevToolsProtocolTest::SetUpCommandLine(command_line); |
| if (headless()) |
| command_line->AppendSwitchASCII("headless", "chrome"); |
| } |
| |
| void PreRunTestOnMainThread() override { |
| DevToolsProtocolTest::PreRunTestOnMainThread(); |
| https_server_.ServeFilesFromSourceDirectory(GetChromeTestDataDir()); |
| ASSERT_TRUE(https_server_.Start()); |
| } |
| |
| void NavigateToURLBlockUntilNavigationsComplete(const std::string& url) { |
| ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( |
| browser(), https_server_.GetURL(url), 1); |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetLastCommittedEntry(); |
| ASSERT_TRUE(entry); |
| } |
| |
| void CreatePdfSpanFromResultData() { |
| const std::string& data = *result()->FindString("data"); |
| ASSERT_TRUE(base::Base64Decode(data, &pdf_data_)); |
| |
| pdf_span_ = base::as_byte_span(pdf_data_); |
| |
| ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span_, &pdf_num_pages_, nullptr)); |
| ASSERT_GE(pdf_num_pages_, 1); |
| } |
| |
| void CreatePdfSpanFromResultStream() { |
| std::string stream = *result()->FindString("stream"); |
| ASSERT_GT(stream.length(), 0ul); |
| |
| pdf_data_.clear(); |
| for (;;) { |
| base::Value::Dict params; |
| params.Set("handle", stream); |
| params.Set("offset", static_cast<int>(pdf_data_.size())); |
| const base::Value::Dict* result = |
| SendCommandSync("IO.read", std::move(params)); |
| std::string data = *result->FindString("data"); |
| if (result->FindBool("base64Encoded").value_or(false)) |
| ASSERT_TRUE(base::Base64Decode(data, &data)); |
| pdf_data_.append(std::move(data)); |
| if (result->FindBool("eof").value_or(false)) |
| break; |
| } |
| |
| pdf_span_ = base::span<const uint8_t>( |
| reinterpret_cast<const uint8_t*>(pdf_data_.data()), pdf_data_.size()); |
| |
| ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span_, &pdf_num_pages_, nullptr)); |
| ASSERT_GE(pdf_num_pages_, 1); |
| } |
| |
| void PrintToPdf(base::Value::Dict params) { |
| SendCommandSync("Page.printToPDF", std::move(params)); |
| CreatePdfSpanFromResultData(); |
| } |
| |
| void PrintToPdfAsStream(base::Value::Dict params) { |
| SendCommandSync("Page.printToPDF", std::move(params)); |
| CreatePdfSpanFromResultStream(); |
| } |
| |
| void PrintToPdfAndRenderPage(base::Value::Dict params, int page_index) { |
| SendCommandSync("Page.printToPDF", std::move(params)); |
| CreatePdfSpanFromResultData(); |
| ASSERT_TRUE(page_bitmap.Render(pdf_span_, page_index)); |
| } |
| |
| void PrintToPdfAsStreamAndRenderPage(base::Value::Dict params, |
| int page_index) { |
| SendCommandSync("Page.printToPDF", std::move(params)); |
| CreatePdfSpanFromResultStream(); |
| ASSERT_TRUE(page_bitmap.Render(pdf_span_, page_index)); |
| } |
| |
| uint32_t GetPixelRGB(int x, int y) { return page_bitmap.GetPixelRGB(x, y); } |
| |
| int bitmap_width() { return page_bitmap.width(); } |
| int bitmap_height() { return page_bitmap.height(); } |
| |
| net::EmbeddedTestServer https_server_; |
| |
| std::string pdf_data_; |
| base::raw_span<const uint8_t, DanglingUntriaged> pdf_span_; |
| int pdf_num_pages_ = 0; |
| |
| headless::PDFPageBitmap page_bitmap; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(HeadfulOrHeadless, |
| PrintToPdfProtocolTest, |
| testing::Bool()); |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, PrintToPdfBackground) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| // Expect top left pixel of background color |
| EXPECT_EQ(GetPixelRGB(0, 0), 0x123456u); |
| |
| // Expect midpoint pixel of red color |
| EXPECT_EQ(GetPixelRGB(bitmap_width() / 2, bitmap_height() / 2), 0xff0000u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, PrintToPdfMargins) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 1.0); |
| params.Set("marginLeft", 1.0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| // Expect top left pixel of white color |
| EXPECT_EQ(GetPixelRGB(0, 0), 0xffffffu); |
| |
| // Expect pixel at a quarter of diagonal of background color |
| EXPECT_EQ(GetPixelRGB(bitmap_width() / 4, bitmap_height() / 4), 0x123456u); |
| |
| // Expect midpoint pixel of red color |
| EXPECT_EQ(GetPixelRGB(bitmap_width() / 2, bitmap_height() / 2), 0xff0000u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, PrintToPdfHeaderFooter) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| constexpr double kHeaderMargin = 1.0; |
| constexpr double kFooterMargin = 1.0; |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", kHeaderMargin); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", kFooterMargin); |
| params.Set("marginRight", 0); |
| params.Set("displayHeaderFooter", true); |
| params.Set("headerTemplate", |
| "<div style='height: 1cm; width: 1cm; background: " |
| "#00ff00; -webkit-print-color-adjust: exact;'>"); |
| params.Set("footerTemplate", |
| "<div style='height: 1cm; width: 1cm; background: " |
| "#0000ff; -webkit-print-color-adjust: exact;'>"); |
| |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| // Expect top left pixel of white color |
| EXPECT_EQ(GetPixelRGB(0, 0), 0xffffffu); |
| |
| // Expect header left pixel of green color |
| EXPECT_EQ(GetPixelRGB(0, kDpi * kHeaderMargin / 2), 0x00ff00u); |
| |
| // Expect footer left pixel of blue color |
| EXPECT_EQ(GetPixelRGB(0, bitmap_height() - kDpi * kFooterMargin / 2), |
| 0x0000ffu); |
| |
| // Expect bottom left pixel of white color |
| EXPECT_EQ(GetPixelRGB(0, bitmap_height() - 1), 0xffffffu); |
| |
| // Expect midpoint pixel of red color |
| EXPECT_EQ(GetPixelRGB(bitmap_width() / 2, bitmap_height() / 2), 0xff0000u); |
| } |
| |
| class PrintToPdfScaleTest : public PrintToPdfProtocolTest { |
| protected: |
| int RenderAndReturnRedSquareWidth(double scale) { |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| params.Set("scale", scale); |
| |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| int y = bitmap_height() / 2; |
| uint32_t start_clr = GetPixelRGB(0, y); |
| EXPECT_EQ(start_clr, 0x123456u); |
| |
| int red_square_width = 0; |
| for (int x = 1; x < bitmap_width(); x++) { |
| uint32_t clr = GetPixelRGB(x, y); |
| if (clr != start_clr) { |
| EXPECT_EQ(clr, 0xff0000u); |
| ++red_square_width; |
| } |
| } |
| |
| return red_square_width; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(HeadfulOrHeadless, |
| PrintToPdfScaleTest, |
| testing::Bool()); |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfScaleTest, PrintToPdfScaleArea) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| constexpr double kScaleFactor = 2.0; |
| constexpr double kDefaultScaleFactor = 1.0; |
| int scaled_red_square_width = RenderAndReturnRedSquareWidth(kScaleFactor); |
| int unscaled_red_square_width = |
| RenderAndReturnRedSquareWidth(kDefaultScaleFactor); |
| |
| EXPECT_EQ(unscaled_red_square_width * kScaleFactor, scaled_red_square_width); |
| } |
| |
| class PrintToPdfPaperOrientationTest : public PrintToPdfProtocolTest { |
| protected: |
| std::optional<gfx::SizeF> PrintToPdfAndReturnPageSize( |
| bool landscape = false) { |
| base::Value::Dict params; |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("landscape", landscape); |
| |
| PrintToPdf(std::move(params)); |
| |
| return chrome_pdf::GetPDFPageSizeByIndex(pdf_span_, 0); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(HeadfulOrHeadless, |
| PrintToPdfPaperOrientationTest, |
| testing::Bool()); |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfPaperOrientationTest, |
| PrintToPdfPaperOrientation) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| std::optional<gfx::SizeF> portrait_page_size = PrintToPdfAndReturnPageSize(); |
| ASSERT_TRUE(portrait_page_size.has_value()); |
| EXPECT_GT(portrait_page_size->height(), portrait_page_size->width()); |
| |
| std::optional<gfx::SizeF> landscape_page_size = |
| PrintToPdfAndReturnPageSize(/*landscape=*/true); |
| ASSERT_TRUE(landscape_page_size.has_value()); |
| EXPECT_GT(landscape_page_size->width(), landscape_page_size->height()); |
| } |
| |
| class PrintToPdfPagesTest : public PrintToPdfProtocolTest { |
| protected: |
| static constexpr double kDocHeight = 50; |
| |
| void SetDocHeight() { |
| std::string height_expression = "document.body.style.height = '" + |
| base::NumberToString(kDocHeight) + "in'"; |
| base::Value::Dict params; |
| params.Set("expression", height_expression); |
| |
| SendCommandSync("Runtime.evaluate", std::move(params)); |
| } |
| |
| base::Value::Dict BuildPrintParams(const std::string& page_ranges) { |
| base::Value::Dict params; |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| params.Set("pageRanges", page_ranges); |
| return params; |
| } |
| |
| void PrintPageRanges(const std::string& page_ranges) { |
| PrintToPdf(BuildPrintParams(page_ranges)); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(HeadfulOrHeadless, |
| PrintToPdfPagesTest, |
| testing::Bool()); |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfPagesTest, PrintToPdfPageRanges) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| SetDocHeight(); |
| |
| const int kExpectedTotalPages = std::ceil(kDocHeight / kPaperHeight); |
| |
| // Empty page range prints all pages. |
| PrintPageRanges(""); |
| EXPECT_EQ(pdf_num_pages_, kExpectedTotalPages); |
| |
| // Print one page. |
| PrintPageRanges("1"); |
| EXPECT_EQ(pdf_num_pages_, 1); |
| |
| // Print list of pages. |
| PrintPageRanges("1,2"); |
| EXPECT_EQ(pdf_num_pages_, 2); |
| |
| // Print range of pages. |
| PrintPageRanges("1-3"); |
| EXPECT_EQ(pdf_num_pages_, 3); |
| |
| // Open page range is OK. |
| PrintPageRanges("-"); |
| EXPECT_EQ(pdf_num_pages_, kExpectedTotalPages); |
| |
| // End page beyond number of pages is OK. |
| PrintPageRanges("2-999"); |
| EXPECT_EQ(pdf_num_pages_, kExpectedTotalPages - 1); |
| |
| // Expect specific error for ranges beyond end of the document. |
| SendCommand("Page.printToPDF", BuildPrintParams("998-999")); |
| |
| EXPECT_THAT(*error()->FindString("message"), |
| testing::Eq("Page range exceeds page count")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfPagesTest, PrintToPdfCssPageSize) { |
| NavigateToURLBlockUntilNavigationsComplete( |
| "/print_to_pdf/css_page_size.html"); |
| |
| Attach(); |
| SetDocHeight(); |
| |
| base::Value::Dict params; |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("preferCSSPageSize", true); |
| |
| PrintToPdf(std::move(params)); |
| |
| // Css page size in /css_page_size.html is smaller than requested |
| // paper size, so expect greater page count. |
| const int kExpectedTotalPages = std::ceil(kDocHeight / kPaperHeight); |
| EXPECT_GT(pdf_num_pages_, kExpectedTotalPages); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, PrintToPdfAsStream) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| params.Set("transferMode", "ReturnAsStream"); |
| |
| PrintToPdfAsStreamAndRenderPage(std::move(params), 0); |
| |
| // Expect top left pixel of background color |
| EXPECT_EQ(GetPixelRGB(0, 0), 0x123456u); |
| |
| // Expect midpoint pixel of red color |
| EXPECT_EQ(GetPixelRGB(bitmap_width() / 2, bitmap_height() / 2), 0xff0000u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, HasDocumentOutline) { |
| NavigateToURLBlockUntilNavigationsComplete( |
| "/print_to_pdf/structured_doc.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| // generating a document outline at the moment requires a tagged pdf |
| params.Set("generateTaggedPDF", true); |
| params.Set("generateDocumentOutline", true); |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| params.Set("transferMode", "ReturnAsStream"); |
| |
| PrintToPdfAsStream(std::move(params)); |
| |
| std::optional<bool> has_outline = chrome_pdf::PDFDocHasOutline(pdf_span_); |
| EXPECT_THAT(has_outline, testing::Optional(true)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, Title) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/basic.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| params.Set("transferMode", "ReturnAsStream"); |
| |
| PrintToPdfAsStream(std::move(params)); |
| |
| std::optional<chrome_pdf::DocumentMetadata> metadata = |
| chrome_pdf::GetPDFDocMetadata(pdf_span_); |
| ASSERT_TRUE(metadata); |
| EXPECT_EQ(metadata->title, "PrintToPdf Basic Test"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, PrintToPdfOOPIF) { |
| NavigateToURLBlockUntilNavigationsComplete("/print_to_pdf/oopif.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| ASSERT_TRUE(printing::IsOopifEnabled()); |
| |
| // Expect red iframe pixel at 1 inch into the page. |
| EXPECT_EQ(GetPixelRGB(1 * kDpi, 1 * kDpi), 0xff0000u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(PrintToPdfProtocolTest, JpegCmykIccPrintToPdf) { |
| NavigateToURLBlockUntilNavigationsComplete( |
| "/print_to_pdf/red-cmyk-turned-green-via-icc_profile.html"); |
| |
| Attach(); |
| |
| base::Value::Dict params; |
| params.Set("printBackground", true); |
| params.Set("paperWidth", kPaperWidth); |
| params.Set("paperHeight", kPaperHeight); |
| params.Set("marginTop", 0); |
| params.Set("marginLeft", 0); |
| params.Set("marginBottom", 0); |
| params.Set("marginRight", 0); |
| PrintToPdfAndRenderPage(std::move(params), 0); |
| |
| ASSERT_TRUE(printing::IsOopifEnabled()); |
| |
| // These color values have been transformed from CMYK+icc to XYZ to sRGB+icc |
| // to displayRGB using several different color managers. Many approximations |
| // and rounding have been applied along the way. So carefully test for a |
| // green-ish rectangle. |
| constexpr SkColor background = SkColorSetARGB(0xff, 0xff, 0xff, 0xff); |
| |
| // Find the first non-background color pixel. |
| int x = 0; |
| int y = 0; |
| auto find_box = [&x, &y, this]() -> bool { |
| const int width = bitmap_width(); |
| const int height = bitmap_height(); |
| for (x = 0; x < width; ++x) { |
| for (y = 0; y < height; ++y) { |
| SkColor c = SkColorSetA(GetPixelRGB(x, y), 0xFF); |
| if (c != background) { |
| return true; |
| } |
| } |
| } |
| return false; |
| }; |
| ASSERT_TRUE(find_box()); |
| |
| // Sample a pixel color in the expected rectangle. |
| SkColor c = SkColorSetA(GetPixelRGB(x + 10, y + 10), 0xFF); |
| uint32_t r = SkColorGetR(c); |
| uint32_t g = SkColorGetG(c); |
| uint32_t b = SkColorGetB(c); |
| |
| // Expect that it is green-ish. |
| EXPECT_LT(r, 0x10u); |
| EXPECT_GT(g, 0xF0u); |
| EXPECT_LT(b, 0x10u); |
| |
| // Expect green rectangle on white background. |
| EXPECT_TRUE(page_bitmap.CheckColoredRect(c, background)); |
| } |
| |
| } // namespace |