blob: 04015b41cb1209f28c34ad842b2ff40f53f4e6f5 [file] [log] [blame]
// Copyright 2017 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/390223051): Remove C-library calls to fix the errors.
#pragma allow_unsafe_libc_calls
#endif
#include "printing/common/metafile_utils.h"
#include <string_view>
#include <variant>
#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/containers/span_reader.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "printing/buildflags/buildflags.h"
#include "printing/mojom/print.mojom.h"
#include "skia/ext/codec_utils.h"
#include "skia/ext/font_utils.h"
#include "skia/ext/skia_utils_base.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkFontMgr.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkString.h"
#include "third_party/skia/include/core/SkTypeface.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"
#include "third_party/skia/include/private/chromium/SkImageChromium.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_update.h"
#include "ui/gfx/skia_span_util.h"
#if BUILDFLAG(IS_WIN)
// XpsObjectModel.h indirectly includes <wincrypt.h> which is
// incompatible with Chromium's OpenSSL. By including wincrypt_shim.h
// first, problems are avoided.
// clang-format off
#include "base/win/wincrypt_shim.h"
#include <XpsObjectModel.h>
#include <objbase.h>
// clang-format on
#include "third_party/skia/include/docs/SkXPSDocument.h"
#endif // BUILDFLAG(IS_WIN)
namespace {
// Table 364 in PDF 32000-2:2020 spec, section 14.8.4.3
const char kPDFStructureTypeDocument[] = "Document";
// Table 365 in PDF 32000-2:2020 spec, section 14.8.4.4
const char kPDFStructureTypeDiv[] = "Div";
const char kPDFStructureTypeAside[] = "Aside";
const char kPDFStructureTypeNonStruct[] = "NonStruct";
// Table 366 in PDF 32000-2:2020 spec, section 14.8.4.5
const char kPDFStructureTypeParagraph[] = "P";
const char kPDFStructureTypeHeading[] = "H";
// Table 368 in PDF 32000-2:2020 spec, section 14.8.4.7.2
const char kPDFStructureTypeListItemLabel[] = "Lbl";
const char kPDFStructureTypeEmphasis[] = "Em";
const char kPDFStructureTypeStrong[] = "Strong";
const char kPDFStructureTypeLink[] = "Link";
// Table 370 in PDF 32000-2:2020 spec, section 14.8.4.8.2
const char kPDFStructureTypeList[] = "L";
const char kPDFStructureTypeListItemBody[] = "LI";
// Table 371 in PDF 32000-2:2020 spec, section 14.8.4.8.3
const char kPDFStructureTypeTable[] = "Table";
const char kPDFStructureTypeTableRow[] = "TR";
const char kPDFStructureTypeTableHeader[] = "TH";
const char kPDFStructureTypeTableCell[] = "TD";
// Table 373 in PDF 32000-2:2020 spec, section 14.8.4.8.5
const char kPDFStructureTypeFigure[] = "Figure";
// Standard attribute owners from Table 376 PDF 32000-2:2020 spec,
// section 14.8.5.2 (Attribute owners are kind of like "categories"
// for structure node attributes.)
const char kPDFTableAttributeOwner[] = "Table";
// Table Attributes from tabl 384 in PDF 32000-2:2020 spec,
// section 14.8.5.7
const char kPDFTableCellColSpanAttribute[] = "ColSpan";
const char kPDFTableCellHeadersAttribute[] = "Headers";
const char kPDFTableCellRowSpanAttribute[] = "RowSpan";
const char kPDFTableHeaderScopeAttribute[] = "Scope";
const char kPDFTableHeaderScopeColumn[] = "Column";
const char kPDFTableHeaderScopeRow[] = "Row";
SkString GetHeadingStructureType(int heading_level) {
// From Table 366 in PDF 32000-2:2020 spec, section 14.8.4.5,
// "H1"..."H6" are valid structure types.
if (heading_level >= 1 && heading_level <= 6)
return SkString(base::StringPrintf("H%d", heading_level).c_str());
// If we don't have a valid heading level, use the generic heading role.
return SkString(kPDFStructureTypeHeading);
}
SkPDF::DateTime TimeToSkTime(base::Time time) {
base::Time::Exploded exploded;
time.UTCExplode(&exploded);
return SkPDF::DateTime{
.fTimeZoneMinutes = 0,
.fYear = static_cast<uint16_t>(exploded.year),
.fMonth = static_cast<uint8_t>(exploded.month),
.fDayOfWeek = static_cast<uint8_t>(exploded.day_of_week),
.fDay = static_cast<uint8_t>(exploded.day_of_month),
.fHour = static_cast<uint8_t>(exploded.hour),
.fMinute = static_cast<uint8_t>(exploded.minute),
.fSecond = static_cast<uint8_t>(exploded.second)};
}
sk_sp<SkPicture> GetEmptyPicture() {
SkPictureRecorder rec;
SkCanvas* canvas = rec.beginRecording(100, 100);
// Add some ops whose net effects equal to a noop.
canvas->save();
canvas->restore();
return rec.finishRecordingAsPicture();
}
// Convert an AXNode into a SkPDF::StructureElementNode in order to make a
// tagged (accessible) PDF. Returns true on success and false if we don't
// have enough data to build a valid tree.
bool RecursiveBuildStructureTree(const ui::AXNode* ax_node,
SkPDF::StructureElementNode* tag) {
bool valid = false;
tag->fNodeId = ax_node->data().GetDOMNodeId();
switch (ax_node->GetRole()) {
case ax::mojom::Role::kRootWebArea:
tag->fTypeString = kPDFStructureTypeDocument;
break;
case ax::mojom::Role::kParagraph:
tag->fTypeString = kPDFStructureTypeParagraph;
break;
case ax::mojom::Role::kGenericContainer:
tag->fTypeString = kPDFStructureTypeDiv;
break;
case ax::mojom::Role::kComplementary:
tag->fTypeString = kPDFStructureTypeAside;
break;
case ax::mojom::Role::kHeading:
tag->fTypeString = GetHeadingStructureType(ax_node->GetIntAttribute(
ax::mojom::IntAttribute::kHierarchicalLevel));
break;
case ax::mojom::Role::kLink:
tag->fTypeString = kPDFStructureTypeLink;
break;
case ax::mojom::Role::kEmphasis:
tag->fTypeString = kPDFStructureTypeEmphasis;
break;
case ax::mojom::Role::kStrong:
tag->fTypeString = kPDFStructureTypeStrong;
break;
case ax::mojom::Role::kList:
tag->fTypeString = kPDFStructureTypeList;
break;
case ax::mojom::Role::kListMarker:
tag->fTypeString = kPDFStructureTypeListItemLabel;
break;
case ax::mojom::Role::kListItem:
tag->fTypeString = kPDFStructureTypeListItemBody;
break;
case ax::mojom::Role::kGrid:
case ax::mojom::Role::kTable:
case ax::mojom::Role::kTreeGrid:
tag->fTypeString = kPDFStructureTypeTable;
break;
case ax::mojom::Role::kRow:
tag->fTypeString = kPDFStructureTypeTableRow;
break;
case ax::mojom::Role::kColumnHeader:
tag->fTypeString = kPDFStructureTypeTableHeader;
tag->fAttributes.appendName(kPDFTableAttributeOwner,
kPDFTableHeaderScopeAttribute,
kPDFTableHeaderScopeColumn);
break;
case ax::mojom::Role::kRowHeader:
tag->fTypeString = kPDFStructureTypeTableHeader;
tag->fAttributes.appendName(kPDFTableAttributeOwner,
kPDFTableHeaderScopeAttribute,
kPDFTableHeaderScopeRow);
break;
case ax::mojom::Role::kCell:
case ax::mojom::Role::kGridCell: {
tag->fTypeString = kPDFStructureTypeTableCell;
// Append an attribute consisting of the string IDs of all of the
// header cells that correspond to this table cell.
std::vector<ui::AXNode*> header_nodes;
ax_node->GetTableCellColHeaders(&header_nodes);
ax_node->GetTableCellRowHeaders(&header_nodes);
std::vector<int> header_ids;
header_ids.reserve(header_nodes.size());
for (ui::AXNode* header_node : header_nodes) {
header_ids.push_back(header_node->data().GetDOMNodeId());
}
tag->fAttributes.appendNodeIdArray(
kPDFTableAttributeOwner, kPDFTableCellHeadersAttribute, header_ids);
break;
}
case ax::mojom::Role::kImage:
// TODO(thestig): Figure out if the `ax::mojom::Role::kFigure` case should
// share code with the `ax::mojom::Role::kImage` case, and if `valid`
// should be set.
valid = true;
[[fallthrough]];
case ax::mojom::Role::kFigure: {
tag->fTypeString = kPDFStructureTypeFigure;
std::string alt =
ax_node->GetStringAttribute(ax::mojom::StringAttribute::kName);
tag->fAlt = SkString(alt.c_str());
break;
}
case ax::mojom::Role::kStaticText:
tag->fTypeString = kPDFStructureTypeNonStruct;
valid = true;
break;
default:
tag->fTypeString = kPDFStructureTypeNonStruct;
break;
}
if (ui::IsCellOrTableHeader(ax_node->GetRole())) {
std::optional<int> row_span = ax_node->GetTableCellRowSpan();
if (row_span.has_value()) {
tag->fAttributes.appendInt(kPDFTableAttributeOwner,
kPDFTableCellRowSpanAttribute,
row_span.value());
}
std::optional<int> col_span = ax_node->GetTableCellColSpan();
if (col_span.has_value()) {
tag->fAttributes.appendInt(kPDFTableAttributeOwner,
kPDFTableCellColSpanAttribute,
col_span.value());
}
}
std::string lang = ax_node->GetLanguage();
std::string parent_lang =
ax_node->parent() ? ax_node->parent()->GetLanguage() : "";
if (!lang.empty() && lang != parent_lang)
tag->fLang = lang.c_str();
tag->fChildVector.resize(ax_node->GetUnignoredChildCount());
for (size_t i = 0; i < tag->fChildVector.size(); i++) {
tag->fChildVector[i] = std::make_unique<SkPDF::StructureElementNode>();
valid |= RecursiveBuildStructureTree(ax_node->GetUnignoredChildAtIndex(i),
tag->fChildVector[i].get());
}
return valid;
}
sk_sp<SkData> GetImageData(SkImage* img) {
// Skip the encoding step if the image is already encoded
if (sk_sp<SkData> data = img->refEncodedData()) {
return data;
}
// TODO(crbug.com/40073326) Convert texture-backed images to raster
// *before* they get this far if possible.
if (img->isTextureBacked()) {
GrDirectContext* ctx = SkImages::GetContext(img);
return skia::EncodePngAsSkData(ctx, img);
}
return skia::EncodePngAsSkData(nullptr, img);
}
} // namespace
namespace printing {
sk_sp<SkDocument> MakePdfDocument(
std::string_view creator,
std::string_view title,
const ui::AXTreeUpdate& accessibility_tree,
mojom::GenerateDocumentOutline generate_document_outline,
SkWStream* stream) {
SkPDF::Metadata metadata;
SkPDF::DateTime now = TimeToSkTime(base::Time::Now());
metadata.fCreation = now;
metadata.fModified = now;
metadata.fCreator =
creator.empty() ? SkString("Chromium") : SkString(creator);
metadata.fTitle = SkString(title);
metadata.fRasterDPI = 300.0f;
SkPDF::StructureElementNode tag_root = {};
if (!accessibility_tree.nodes.empty()) {
ui::AXTree tree(accessibility_tree);
if (RecursiveBuildStructureTree(tree.root(), &tag_root)) {
metadata.fStructureElementTreeRoot = &tag_root;
metadata.fOutline =
generate_document_outline == mojom::GenerateDocumentOutline::kNone
? SkPDF::Metadata::Outline::None
: SkPDF::Metadata::Outline::StructureElementHeaders;
}
}
return SkPDF::MakeDocument(stream, metadata);
}
#if BUILDFLAG(IS_WIN)
sk_sp<SkDocument> MakeXpsDocument(SkWStream* stream) {
IXpsOMObjectFactory* factory = nullptr;
HRESULT hr = CoCreateInstance(CLSID_XpsOMObjectFactory, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
if (FAILED(hr) || !factory) {
DLOG(ERROR) << "Unable to create XPS object factory: "
<< logging::SystemErrorCodeToString(hr);
return nullptr;
}
return SkXPS::MakeDocument(stream, factory);
}
#endif
sk_sp<SkData> SerializeOopPicture(SkPicture* pic, void* ctx) {
const auto* context = reinterpret_cast<const ContentToProxyTokenMap*>(ctx);
uint32_t pic_id = pic->uniqueID();
auto iter = context->find(pic_id);
if (iter == context->end()) {
return nullptr;
}
return gfx::MakeSkDataFromSpanWithCopy(base::byte_span_from_ref(pic_id));
}
sk_sp<SkPicture> DeserializeOopPicture(const void* data,
size_t length,
void* ctx) {
uint32_t pic_id;
if (length < sizeof(pic_id)) {
NOTREACHED(); // Should not happen if the content is as written.
}
memcpy(&pic_id, data, sizeof(pic_id));
auto* context = reinterpret_cast<PictureDeserializationContext*>(ctx);
auto iter = context->find(pic_id);
if (iter == context->end() || !iter->second) {
// When we don't have the out-of-process picture available, we return
// an empty picture. Returning a nullptr will cause the deserialization
// crash.
return GetEmptyPicture();
}
return iter->second;
}
sk_sp<SkData> SerializeOopTypeface(SkTypeface* typeface, void* ctx) {
auto* context = reinterpret_cast<TypefaceSerializationContext*>(ctx);
SkTypefaceID typeface_id = typeface->uniqueID();
bool data_included = context->insert(typeface_id).second;
// Need the typeface ID to identify the desired typeface. Include an
// indicator for when typeface data actually follows vs. when the typeface
// should already exist in a cache when deserializing.
SkDynamicMemoryWStream stream;
stream.write32(typeface_id);
stream.writeBool(data_included);
if (data_included) {
typeface->serialize(&stream, SkTypeface::SerializeBehavior::kDoIncludeData);
}
return stream.detachAsData();
}
sk_sp<SkTypeface> DeserializeOopTypeface(const void* data,
size_t length,
void* ctx) {
SkStream* stream = *(reinterpret_cast<SkStream**>(const_cast<void*>(data)));
if (length < sizeof(stream)) {
NOTREACHED(); // Should not happen if the content is as written.
}
SkTypefaceID id;
if (!stream->readU32(&id)) {
return nullptr;
}
bool data_included;
if (!stream->readBool(&data_included)) {
return nullptr;
}
auto* context = reinterpret_cast<TypefaceDeserializationContext*>(ctx);
auto iter = context->find(id);
if (iter != context->end()) {
DCHECK(!data_included);
return iter->second;
}
// Typeface not encountered before, expect it to be present in the stream.
DCHECK(data_included);
sk_sp<SkTypeface> typeface =
SkTypeface::MakeDeserialize(stream, skia::DefaultFontMgr());
context->emplace(id, typeface);
return typeface;
}
sk_sp<SkData> SerializeRasterImage(SkImage* img, void* ctx) {
if (!img) {
return nullptr;
}
auto* context = reinterpret_cast<ImageSerializationContext*>(ctx);
uint32_t img_id = img->uniqueID();
if (context->contains(img_id)) {
return SkData::MakeWithCopy(&img_id, sizeof(img_id));
}
sk_sp<SkData> img_data = GetImageData(img);
if (!img_data) {
return nullptr;
}
// Store image id followed by the image data on the first occurrence
// of an image.
auto data = SkData::MakeUninitialized(
base::CheckAdd(img_data->size(), sizeof(img_id)).ValueOrDie());
// SAFETY: The span is used as a view to avoid direct pointer access.
auto [id_span, data_span] =
skia::as_writable_byte_span(*data).split_at<sizeof(img_id)>();
id_span.copy_from(base::byte_span_from_ref(img_id));
data_span.copy_from(gfx::SkDataToSpan(img_data));
context->insert(img_id);
return data;
}
sk_sp<SkImage> DeserializeRasterImage(const void* bytes,
size_t length,
void* ctx) {
auto* context = reinterpret_cast<ImageDeserializationContext*>(ctx);
// SAFETY: The caller must provide a valid pointer and length.
base::SpanReader reader{
UNSAFE_BUFFERS(base::span(static_cast<const uint8_t*>(bytes), length))};
uint32_t img_id;
if (!reader.ReadU32NativeEndian(img_id)) {
// If there is no room for id, there cannot be meaningful image data.
return nullptr;
}
auto iter = context->find(img_id);
if (iter != context->end() && iter->second) {
return iter->second;
}
if (!reader.remaining()) {
return nullptr;
}
// Copy the data to avoid `bytes` being freed before the image is decoded.
auto data_span = reader.remaining_span();
auto img_data = SkData::MakeWithCopy(data_span.data(), data_span.size());
// Need to explicitly decode here, as the data are prefixed with image id,
// invalidating the built-in Skia fallback.
auto image = SkImages::DeferredFromEncodedData(img_data);
if (!image) {
return nullptr;
}
(*context)[img_id] = image;
return image;
}
SkSerialProcs SerializationProcs(PictureSerializationContext* picture_ctx,
TypefaceSerializationContext* typeface_ctx,
ImageSerializationContext* image_ctx) {
SkSerialProcs procs;
procs.fImageProc = SerializeRasterImage;
procs.fImageCtx = image_ctx;
procs.fPictureProc = SerializeOopPicture;
procs.fPictureCtx = picture_ctx;
procs.fTypefaceProc = SerializeOopTypeface;
procs.fTypefaceCtx = typeface_ctx;
return procs;
}
SkDeserialProcs DeserializationProcs(
PictureDeserializationContext* picture_ctx,
TypefaceDeserializationContext* typeface_ctx,
ImageDeserializationContext* image_ctx) {
SkDeserialProcs procs;
procs.fImageProc = DeserializeRasterImage;
procs.fImageCtx = image_ctx;
procs.fPictureProc = DeserializeOopPicture;
procs.fPictureCtx = picture_ctx;
procs.fTypefaceProc = DeserializeOopTypeface;
procs.fTypefaceCtx = typeface_ctx;
return procs;
}
} // namespace printing