// 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/strings/stringprintf.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) // Table 333 in PDF 32000-1:2008 spec, section 14.8.4.2 const char kPDFStructureTypeDocument[] = "Document"; const char kPDFStructureTypeParagraph[] = "P"; const char kPDFStructureTypeDiv[] = "Div"; const char kPDFStructureTypeHeading[] = "H"; const char kPDFStructureTypeLink[] = "Link"; const char kPDFStructureTypeList[] = "L"; const char kPDFStructureTypeListItemLabel[] = "Lbl"; const char kPDFStructureTypeListItemBody[] = "LI"; const char kPDFStructureTypeTable[] = "Table"; const char kPDFStructureTypeTableRow[] = "TR"; const char kPDFStructureTypeTableHeader[] = "TH"; const char kPDFStructureTypeTableCell[] = "TD"; const char kPDFStructureTypeFigure[] = "Figure"; const char kPDFStructureTypeNonStruct[] = "NonStruct"; // 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"; SkString GetHeadingStructureType(int heading_level) { // From Table 333 in PDF 32000-1:2008 spec, section 14.8.4.2, // "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); } #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 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->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::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::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::kTable: 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: { 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 header_nodes; ax_node->GetTableCellColHeaders(&header_nodes); ax_node->GetTableCellRowHeaders(&header_nodes); std::vector header_ids; header_ids.reserve(header_nodes.size()); for (ui::AXNode* header_node : header_nodes) { header_ids.push_back(header_node->GetIntAttribute( ax::mojom::IntAttribute::kDOMNodeId)); } tag->fAttributes.appendNodeIdArray( kPDFTableAttributeOwner, kPDFTableCellHeadersAttribute, header_ids); break; } case ax::mojom::Role::kFigure: case ax::mojom::Role::kImage: { 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: // 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->fTypeString = kPDFStructureTypeNonStruct; valid = true; break; default: tag->fTypeString = kPDFStructureTypeNonStruct; } if (ui::IsCellOrTableHeader(ax_node->data().role)) { base::Optional row_span = ax_node->GetTableCellRowSpan(); if (row_span.has_value()) { tag->fAttributes.appendInt(kPDFTableAttributeOwner, kPDFTableCellRowSpanAttribute, row_span.value()); } base::Optional 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(); 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 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 SerializeOopPicture(SkPicture* pic, void* ctx) { const auto* context = reinterpret_cast(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 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(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 SerializeOopTypeface(SkTypeface* typeface, void* ctx) { auto* context = reinterpret_cast(ctx); SkFontID 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 DeserializeOopTypeface(const void* data, size_t length, void* ctx) { SkStream* stream = *(reinterpret_cast(const_cast(data))); if (length < sizeof(stream)) { NOTREACHED(); // Should not happen if the content is as written. return nullptr; } SkFontID id; if (!stream->readU32(&id)) { return nullptr; } bool data_included; if (!stream->readBool(&data_included)) { return nullptr; } auto* context = reinterpret_cast(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 typeface = SkTypeface::MakeDeserialize(stream); context->emplace(id, typeface); return typeface; } SkSerialProcs SerializationProcs(PictureSerializationContext* picture_ctx, TypefaceSerializationContext* typeface_ctx) { SkSerialProcs procs; procs.fPictureProc = SerializeOopPicture; procs.fPictureCtx = picture_ctx; procs.fTypefaceProc = SerializeOopTypeface; procs.fTypefaceCtx = typeface_ctx; return procs; } SkDeserialProcs DeserializationProcs( PictureDeserializationContext* picture_ctx, TypefaceDeserializationContext* typeface_ctx) { SkDeserialProcs procs; procs.fPictureProc = DeserializeOopPicture; procs.fPictureCtx = picture_ctx; procs.fTypefaceProc = DeserializeOopTypeface; procs.fTypefaceCtx = typeface_ctx; return procs; } } // namespace printing