blob: 9dd15e588272f31a69340edf892a58b74650027c [file] [log] [blame]
// Copyright 2017 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/common/metafile_utils.h"
#include "base/time/time.h"
#include "printing/buildflags/buildflags.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPicture.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "third_party/skia/include/core/SkTime.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_update.h"
namespace {
#if BUILDFLAG(ENABLE_TAGGED_PDF)
// Standard attribute owners from PDF 32000-1:2008 spec, section 14.8.5.2
// (Attribute owners are kind of like "categories" for structure node
// attributes.)
const char kPDFTableAttributeOwner[] = "Table";
// Table Attributes from PDF 32000-1:2008 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";
#endif // BUILDFLAG(ENABLE_TAGGED_PDF)
SkTime::DateTime TimeToSkTime(base::Time time) {
base::Time::Exploded exploded;
time.UTCExplode(&exploded);
SkTime::DateTime skdate;
skdate.fTimeZoneMinutes = 0;
skdate.fYear = exploded.year;
skdate.fMonth = exploded.month;
skdate.fDayOfWeek = exploded.day_of_week;
skdate.fDay = exploded.day_of_month;
skdate.fHour = exploded.hour;
skdate.fMinute = exploded.minute;
skdate.fSecond = exploded.second;
return skdate;
}
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) {
#if BUILDFLAG(ENABLE_TAGGED_PDF)
bool valid = false;
tag->fNodeId = ax_node->GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId);
switch (ax_node->data().role) {
case ax::mojom::Role::kRootWebArea:
tag->fType = SkPDF::DocumentStructureType::kDocument;
break;
case ax::mojom::Role::kParagraph:
tag->fType = SkPDF::DocumentStructureType::kP;
break;
case ax::mojom::Role::kGenericContainer:
tag->fType = SkPDF::DocumentStructureType::kDiv;
break;
case ax::mojom::Role::kHeading:
// TODO(dmazzoni): heading levels. https://crbug.com/1039816
tag->fType = SkPDF::DocumentStructureType::kH;
break;
case ax::mojom::Role::kList:
tag->fType = SkPDF::DocumentStructureType::kL;
break;
case ax::mojom::Role::kListMarker:
tag->fType = SkPDF::DocumentStructureType::kLbl;
break;
case ax::mojom::Role::kListItem:
tag->fType = SkPDF::DocumentStructureType::kLI;
break;
case ax::mojom::Role::kTable:
tag->fType = SkPDF::DocumentStructureType::kTable;
break;
case ax::mojom::Role::kRow:
tag->fType = SkPDF::DocumentStructureType::kTR;
break;
case ax::mojom::Role::kColumnHeader:
tag->fType = SkPDF::DocumentStructureType::kTH;
tag->fAttributes.appendString(kPDFTableAttributeOwner,
kPDFTableHeaderScopeAttribute,
kPDFTableHeaderScopeColumn);
break;
case ax::mojom::Role::kRowHeader:
tag->fType = SkPDF::DocumentStructureType::kTH;
tag->fAttributes.appendString(kPDFTableAttributeOwner,
kPDFTableHeaderScopeAttribute,
kPDFTableHeaderScopeRow);
break;
case ax::mojom::Role::kCell: {
tag->fType = SkPDF::DocumentStructureType::kTD;
// 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<SkString> header_id_strs;
header_id_strs.reserve(header_nodes.size());
for (ui::AXNode* header_node : header_nodes) {
int node_id =
header_node->GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId);
header_id_strs.push_back(
SkString(base::NumberToString(node_id).c_str()));
}
tag->fAttributes.appendStringArray(kPDFTableAttributeOwner,
kPDFTableCellHeadersAttribute,
header_id_strs);
break;
}
case ax::mojom::Role::kFigure:
case ax::mojom::Role::kImage: {
tag->fType = SkPDF::DocumentStructureType::kFigure;
std::string alt =
ax_node->GetStringAttribute(ax::mojom::StringAttribute::kName);
tag->fAlt = SkString(alt.c_str());
break;
}
case ax::mojom::Role::kStaticText:
// Currently we're only marking text content, so we can't generate
// a nonempty structure tree unless we have at least one kStaticText
// node in the tree.
tag->fType = SkPDF::DocumentStructureType::kNonStruct;
valid = true;
break;
default:
tag->fType = SkPDF::DocumentStructureType::kNonStruct;
}
if (ui::IsCellOrTableHeader(ax_node->data().role)) {
base::Optional<int> row_span = ax_node->GetTableCellRowSpan();
if (row_span.has_value()) {
tag->fAttributes.appendInt(kPDFTableAttributeOwner,
kPDFTableCellRowSpanAttribute,
row_span.value());
}
base::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();
size_t children_count = ax_node->GetUnignoredChildCount();
tag->fChildVector.resize(children_count);
for (size_t i = 0; i < children_count; i++) {
tag->fChildVector[i] = std::make_unique<SkPDF::StructureElementNode>();
bool success = RecursiveBuildStructureTree(
ax_node->GetUnignoredChildAtIndex(i), tag->fChildVector[i].get());
if (success)
valid = true;
}
return valid;
#else // BUILDFLAG(ENABLE_TAGGED_PDF)
return false;
#endif
}
} // namespace
namespace printing {
sk_sp<SkDocument> MakePdfDocument(const std::string& creator,
const ui::AXTreeUpdate& accessibility_tree,
SkWStream* stream) {
SkPDF::Metadata metadata;
SkTime::DateTime now = TimeToSkTime(base::Time::Now());
metadata.fCreation = now;
metadata.fModified = now;
metadata.fCreator = creator.empty()
? SkString("Chromium")
: SkString(creator.c_str(), creator.size());
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;
}
return SkPDF::MakeDocument(stream, metadata);
}
sk_sp<SkData> SerializeOopPicture(SkPicture* pic, void* ctx) {
const ContentToProxyIdMap* context =
reinterpret_cast<const ContentToProxyIdMap*>(ctx);
uint32_t pic_id = pic->uniqueID();
auto iter = context->find(pic_id);
if (iter == context->end())
return nullptr;
return SkData::MakeWithCopy(&pic_id, sizeof(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.
return GetEmptyPicture();
}
memcpy(&pic_id, data, sizeof(pic_id));
auto* context = reinterpret_cast<DeserializationContext*>(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;
}
SkSerialProcs SerializationProcs(SerializationContext* ctx) {
SkSerialProcs procs;
procs.fPictureProc = SerializeOopPicture;
procs.fPictureCtx = ctx;
return procs;
}
SkDeserialProcs DeserializationProcs(DeserializationContext* ctx) {
SkDeserialProcs procs;
procs.fPictureProc = DeserializeOopPicture;
procs.fPictureCtx = ctx;
return procs;
}
} // namespace printing