blob: 0929c003967feb5a85e081f62a1bec9e7afbf560 [file] [log] [blame]
// Copyright (c) 2012 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 "printing/metafile_skia.h"
#include <algorithm>
#include <map>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file.h"
#include "base/numerics/safe_conversions.h"
#include "base/stl_util.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "cc/paint/paint_record.h"
#include "cc/paint/paint_recorder.h"
#include "cc/paint/skia_paint_canvas.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_settings.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/include/core/SkStream.h"
// Note that headers in third_party/skia/src are fragile. This is
// an experimental, fragile, and diagnostic-only document type.
#include "third_party/skia/src/utils/SkMultiPictureDocument.h"
#include "ui/gfx/skia_util.h"
#if defined(OS_MAC)
#include "printing/pdf_metafile_cg_mac.h"
#endif
#if defined(OS_POSIX)
#include "base/file_descriptor_posix.h"
#endif
#if defined(OS_ANDROID)
#include "base/files/file_util.h"
#endif // defined(OS_ANDROID)
namespace {
bool WriteAssetToBuffer(const SkStreamAsset* asset, void* buffer, size_t size) {
// Calling duplicate() keeps original asset state unchanged.
std::unique_ptr<SkStreamAsset> assetCopy(asset->duplicate());
size_t length = assetCopy->getLength();
return length <= size && length == assetCopy->read(buffer, length);
}
} // namespace
namespace printing {
struct Page {
Page(const SkSize& s, sk_sp<cc::PaintRecord> c)
: size(s), content(std::move(c)) {}
Page(Page&& that) : size(that.size), content(std::move(that.content)) {}
Page(const Page&) = default;
Page& operator=(const Page&) = default;
Page& operator=(Page&& that) {
size = that.size;
content = std::move(that.content);
return *this;
}
SkSize size;
sk_sp<cc::PaintRecord> content;
};
struct MetafileSkiaData {
cc::PaintRecorder recorder; // Current recording
std::vector<Page> pages;
std::unique_ptr<SkStreamAsset> data_stream;
ContentToProxyTokenMap subframe_content_info;
std::map<uint32_t, sk_sp<SkPicture>> subframe_pics;
int document_cookie = 0;
ContentProxySet* typeface_content_info = nullptr;
// The scale factor is used because Blink occasionally calls
// PaintCanvas::getTotalMatrix() even though the total matrix is not as
// meaningful for a vector canvas as for a raster canvas.
float scale_factor;
SkSize size;
mojom::SkiaDocumentType type;
#if defined(OS_MAC)
PdfMetafileCg pdf_cg;
#endif
};
MetafileSkia::MetafileSkia() : data_(std::make_unique<MetafileSkiaData>()) {
data_->type = mojom::SkiaDocumentType::kPDF;
}
MetafileSkia::MetafileSkia(mojom::SkiaDocumentType type, int document_cookie)
: data_(std::make_unique<MetafileSkiaData>()) {
data_->type = type;
data_->document_cookie = document_cookie;
}
MetafileSkia::~MetafileSkia() = default;
bool MetafileSkia::Init() {
return true;
}
void MetafileSkia::UtilizeTypefaceContext(
ContentProxySet* typeface_content_info) {
data_->typeface_content_info = typeface_content_info;
}
// TODO(halcanary): Create a Metafile class that only stores data.
// Metafile::InitFromData is orthogonal to what the rest of
// MetafileSkia does.
bool MetafileSkia::InitFromData(base::span<const uint8_t> data) {
data_->data_stream = std::make_unique<SkMemoryStream>(
data.data(), data.size(), /*copy_data=*/true);
return true;
}
void MetafileSkia::StartPage(const gfx::Size& page_size,
const gfx::Rect& content_area,
float scale_factor,
mojom::PageOrientation page_orientation) {
gfx::Size physical_page_size = page_size;
if (page_orientation != mojom::PageOrientation::kUpright)
physical_page_size.SetSize(page_size.height(), page_size.width());
DCHECK_GT(page_size.width(), 0);
DCHECK_GT(page_size.height(), 0);
DCHECK_GT(scale_factor, 0.0f);
if (data_->recorder.getRecordingCanvas())
FinishPage();
DCHECK(!data_->recorder.getRecordingCanvas());
float inverse_scale = 1.0 / scale_factor;
cc::PaintCanvas* canvas = data_->recorder.beginRecording(
inverse_scale * physical_page_size.width(),
inverse_scale * physical_page_size.height());
// Recording canvas is owned by the |data_->recorder|. No ref() necessary.
if (content_area != gfx::Rect(page_size) ||
page_orientation != mojom::PageOrientation::kUpright) {
canvas->scale(inverse_scale, inverse_scale);
if (page_orientation == mojom::PageOrientation::kRotateLeft) {
canvas->translate(0, physical_page_size.height());
canvas->rotate(-90);
} else if (page_orientation == mojom::PageOrientation::kRotateRight) {
canvas->translate(physical_page_size.width(), 0);
canvas->rotate(90);
}
SkRect sk_content_area = gfx::RectToSkRect(content_area);
canvas->clipRect(sk_content_area);
canvas->translate(sk_content_area.x(), sk_content_area.y());
canvas->scale(scale_factor, scale_factor);
}
data_->size = gfx::SizeFToSkSize(gfx::SizeF(physical_page_size));
data_->scale_factor = scale_factor;
// We scale the recording canvas's size so that
// canvas->getTotalMatrix() returns a value that ignores the scale
// factor. We store the scale factor and re-apply it later.
// http://crbug.com/469656
}
cc::PaintCanvas* MetafileSkia::GetVectorCanvasForNewPage(
const gfx::Size& page_size,
const gfx::Rect& content_area,
float scale_factor,
mojom::PageOrientation page_orientation) {
StartPage(page_size, content_area, scale_factor, page_orientation);
return data_->recorder.getRecordingCanvas();
}
bool MetafileSkia::FinishPage() {
if (!data_->recorder.getRecordingCanvas())
return false;
sk_sp<cc::PaintRecord> pic = data_->recorder.finishRecordingAsPicture();
if (data_->scale_factor != 1.0f) {
cc::PaintCanvas* canvas = data_->recorder.beginRecording(
data_->size.width(), data_->size.height());
canvas->scale(data_->scale_factor, data_->scale_factor);
canvas->drawPicture(pic);
pic = data_->recorder.finishRecordingAsPicture();
}
data_->pages.emplace_back(data_->size, std::move(pic));
return true;
}
bool MetafileSkia::FinishDocument() {
// If we've already set the data in InitFromData, leave it be.
if (data_->data_stream)
return false;
if (data_->recorder.getRecordingCanvas())
FinishPage();
SkDynamicMemoryWStream stream;
sk_sp<SkDocument> doc;
cc::PlaybackParams::CustomDataRasterCallback custom_callback;
switch (data_->type) {
case mojom::SkiaDocumentType::kPDF:
doc = MakePdfDocument(printing::GetAgent(), accessibility_tree_, &stream);
break;
case mojom::SkiaDocumentType::kMSKP:
SkSerialProcs procs = SerializationProcs(&data_->subframe_content_info,
data_->typeface_content_info);
doc = SkMakeMultiPictureDocument(&stream, &procs);
// It is safe to use base::Unretained(this) because the callback
// is only used by |canvas| in the following loop which has shorter
// lifetime than |this|.
custom_callback = base::BindRepeating(
&MetafileSkia::CustomDataToSkPictureCallback, base::Unretained(this));
break;
}
for (const Page& page : data_->pages) {
cc::SkiaPaintCanvas canvas(
doc->beginPage(page.size.width(), page.size.height()));
canvas.drawPicture(page.content, custom_callback);
doc->endPage();
}
doc->close();
data_->data_stream = stream.detachAsStream();
return true;
}
void MetafileSkia::FinishFrameContent() {
// Sanity check to make sure we print the entire frame as a single page
// content.
DCHECK_EQ(data_->pages.size(), 1u);
// Also make sure it is in skia multi-picture document format.
DCHECK_EQ(data_->type, mojom::SkiaDocumentType::kMSKP);
DCHECK(!data_->data_stream);
cc::PlaybackParams::CustomDataRasterCallback custom_callback =
base::BindRepeating(&MetafileSkia::CustomDataToSkPictureCallback,
base::Unretained(this));
sk_sp<SkPicture> pic = ToSkPicture(data_->pages[0].content,
SkRect::MakeSize(data_->pages[0].size),
nullptr, custom_callback);
SkSerialProcs procs = SerializationProcs(&data_->subframe_content_info,
data_->typeface_content_info);
SkDynamicMemoryWStream stream;
pic->serialize(&stream, &procs);
data_->data_stream = stream.detachAsStream();
}
uint32_t MetafileSkia::GetDataSize() const {
if (!data_->data_stream)
return 0;
return base::checked_cast<uint32_t>(data_->data_stream->getLength());
}
bool MetafileSkia::GetData(void* dst_buffer, uint32_t dst_buffer_size) const {
if (!data_->data_stream)
return false;
return WriteAssetToBuffer(data_->data_stream.get(), dst_buffer,
base::checked_cast<size_t>(dst_buffer_size));
}
gfx::Rect MetafileSkia::GetPageBounds(unsigned int page_number) const {
if (page_number < data_->pages.size()) {
SkSize size = data_->pages[page_number].size;
return gfx::Rect(base::ClampRound(size.width()),
base::ClampRound(size.height()));
}
return gfx::Rect();
}
unsigned int MetafileSkia::GetPageCount() const {
return base::checked_cast<unsigned int>(data_->pages.size());
}
printing::NativeDrawingContext MetafileSkia::context() const {
NOTREACHED();
return nullptr;
}
#if defined(OS_WIN)
bool MetafileSkia::Playback(printing::NativeDrawingContext hdc,
const RECT* rect) const {
NOTREACHED();
return false;
}
bool MetafileSkia::SafePlayback(printing::NativeDrawingContext hdc) const {
NOTREACHED();
return false;
}
#elif defined(OS_MAC)
/* TODO(caryclark): The set up of PluginInstance::PrintPDFOutput may result in
rasterized output. Even if that flow uses PdfMetafileCg::RenderPage,
the drawing of the PDF into the canvas may result in a rasterized output.
PDFMetafileSkia::RenderPage should be not implemented as shown and instead
should do something like the following CL in PluginInstance::PrintPDFOutput:
http://codereview.chromium.org/7200040/diff/1/webkit/plugins/ppapi/ppapi_plugin_instance.cc
*/
bool MetafileSkia::RenderPage(unsigned int page_number,
CGContextRef context,
const CGRect& rect,
bool autorotate,
bool fit_to_page) const {
DCHECK_GT(GetDataSize(), 0U);
if (data_->pdf_cg.GetDataSize() == 0) {
if (GetDataSize() == 0)
return false;
size_t length = data_->data_stream->getLength();
std::vector<uint8_t> buffer(length);
(void)WriteAssetToBuffer(data_->data_stream.get(), &buffer[0], length);
data_->pdf_cg.InitFromData(buffer);
}
return data_->pdf_cg.RenderPage(page_number, context, rect, autorotate,
fit_to_page);
}
#endif
#if defined(OS_ANDROID)
bool MetafileSkia::SaveToFileDescriptor(int fd) const {
if (GetDataSize() == 0u)
return false;
std::unique_ptr<SkStreamAsset> asset(data_->data_stream->duplicate());
static constexpr size_t kMaximumBufferSize = 1024 * 1024;
std::vector<uint8_t> buffer(std::min(kMaximumBufferSize, asset->getLength()));
do {
size_t read_size = asset->read(&buffer[0], buffer.size());
if (read_size == 0u)
break;
DCHECK_GE(buffer.size(), read_size);
if (!base::WriteFileDescriptor(
fd, reinterpret_cast<const char*>(buffer.data()), read_size)) {
return false;
}
} while (!asset->isAtEnd());
return true;
}
#else
bool MetafileSkia::SaveTo(base::File* file) const {
if (GetDataSize() == 0U)
return false;
// Calling duplicate() keeps original asset state unchanged.
std::unique_ptr<SkStreamAsset> asset(data_->data_stream->duplicate());
static constexpr size_t kMaximumBufferSize = 1024 * 1024;
std::vector<uint8_t> buffer(std::min(kMaximumBufferSize, asset->getLength()));
do {
size_t read_size = asset->read(&buffer[0], buffer.size());
if (read_size == 0)
break;
DCHECK_GE(buffer.size(), read_size);
if (!file->WriteAtCurrentPosAndCheck(
base::make_span(&buffer[0], read_size))) {
return false;
}
} while (!asset->isAtEnd());
return true;
}
#endif // defined(OS_ANDROID)
std::unique_ptr<MetafileSkia> MetafileSkia::GetMetafileForCurrentPage(
mojom::SkiaDocumentType type) {
// If we only ever need the metafile for the last page, should we
// only keep a handle on one PaintRecord?
auto metafile = std::make_unique<MetafileSkia>(type, data_->document_cookie);
if (data_->pages.size() == 0)
return metafile;
if (data_->recorder.getRecordingCanvas()) // page outstanding
return metafile;
metafile->data_->pages.push_back(data_->pages.back());
metafile->data_->subframe_content_info = data_->subframe_content_info;
metafile->data_->subframe_pics = data_->subframe_pics;
metafile->data_->typeface_content_info = data_->typeface_content_info;
if (!metafile->FinishDocument()) // Generate PDF.
metafile.reset();
return metafile;
}
uint32_t MetafileSkia::CreateContentForRemoteFrame(
const gfx::Rect& rect,
const base::UnguessableToken& render_proxy_token) {
// Create a place holder picture.
sk_sp<SkPicture> pic = SkPicture::MakePlaceholder(
SkRect::MakeXYWH(rect.x(), rect.y(), rect.width(), rect.height()));
// Store the map between content id and the proxy id.
uint32_t content_id = pic->uniqueID();
DCHECK(!base::Contains(data_->subframe_content_info, content_id));
data_->subframe_content_info[content_id] = render_proxy_token;
// Store the picture content.
data_->subframe_pics[content_id] = pic;
return content_id;
}
int MetafileSkia::GetDocumentCookie() const {
return data_->document_cookie;
}
const ContentToProxyTokenMap& MetafileSkia::GetSubframeContentInfo() const {
return data_->subframe_content_info;
}
void MetafileSkia::AppendPage(const SkSize& page_size,
sk_sp<cc::PaintRecord> record) {
data_->pages.emplace_back(page_size, std::move(record));
}
void MetafileSkia::AppendSubframeInfo(uint32_t content_id,
const base::UnguessableToken& proxy_token,
sk_sp<SkPicture> pic_holder) {
data_->subframe_content_info[content_id] = proxy_token;
data_->subframe_pics[content_id] = pic_holder;
}
SkStreamAsset* MetafileSkia::GetPdfData() const {
return data_->data_stream.get();
}
void MetafileSkia::CustomDataToSkPictureCallback(SkCanvas* canvas,
uint32_t content_id) {
// Check whether this is the one we need to handle.
if (!base::Contains(data_->subframe_content_info, content_id))
return;
auto it = data_->subframe_pics.find(content_id);
DCHECK(it != data_->subframe_pics.end());
// Found the picture, draw it on canvas.
sk_sp<SkPicture> pic = it->second;
SkRect rect = pic->cullRect();
SkMatrix matrix = SkMatrix::Translate(rect.x(), rect.y());
canvas->drawPicture(it->second, &matrix, nullptr);
}
} // namespace printing