Skip to content

Commit

Permalink
Support exporting tagged PDFs from Chrome Headless
Browse files Browse the repository at this point in the history
When the --export-tagged-pdf flag is enabled and
when exporting a PDF using Chrome Headless, generate
a tagged (accessible) PDF instead of an untagged one.

The details are described in the design doc:
https://docs.google.com/document/d/1ku6QNtAHEqVnRGqFzpmlMe1fowkjCX9miHj8Kx1cFlk

In a nutshell, PrintRenderFrameHelper captures a
snapshot of the accessibility tree for the page and
returns it in the metafile. This snapshot includes
blink DOMNodeIds. In addition, we ensure that all
text drawing commands captured for printing also
include DOMNodeIds (this was originally added to
support content capture). Finally,
printing::MakePdfDocument is modified to convert the
accessibility tree into a structure tree and pass
this through to Skia.

A follow-up change adds an end-to-end test of
generating a tagged PDF using Chrome Headless:
http://crrev.com/c/1970744

Bug: 607777

Change-Id: Ib2bfa2de94f622c766c4b4c73c013437e404bbde
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1970742
Commit-Queue: Dominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Reviewed-by: Eric Seckler <eseckler@chromium.org>
Cr-Commit-Position: refs/heads/master@{#729428}
  • Loading branch information
minorninth authored and Commit Bot committed Jan 8, 2020
1 parent 48d4e04 commit 0a39059
Show file tree
Hide file tree
Showing 27 changed files with 201 additions and 16 deletions.
3 changes: 3 additions & 0 deletions cc/paint/paint_op_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkRegion.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"
#include "third_party/skia/include/gpu/GrContext.h"

namespace cc {
Expand Down Expand Up @@ -1395,9 +1396,11 @@ void DrawTextBlobOp::RasterWithFlags(const DrawTextBlobOp* op,
const PaintFlags* flags,
SkCanvas* canvas,
const PlaybackParams& params) {
SkPDF::SetNodeId(canvas, op->node_id);
flags->DrawToSk(canvas, [op](SkCanvas* c, const SkPaint& p) {
c->drawTextBlob(op->blob.get(), op->x, op->y, p);
});
SkPDF::SetNodeId(canvas, 0);
}

void RestoreOp::Raster(const RestoreOp* op,
Expand Down
3 changes: 3 additions & 0 deletions cc/paint/skia_paint_canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "cc/paint/paint_recorder.h"
#include "cc/paint/scoped_raster_flags.h"
#include "third_party/skia/include/core/SkAnnotation.h"
#include "third_party/skia/include/docs/SkPDFDocument.h"

namespace cc {
SkiaPaintCanvas::ContextFlushes::ContextFlushes()
Expand Down Expand Up @@ -317,7 +318,9 @@ void SkiaPaintCanvas::drawTextBlob(sk_sp<SkTextBlob> blob,
SkScalar y,
NodeId node_id,
const PaintFlags& flags) {
SkPDF::SetNodeId(canvas_, node_id);
drawTextBlob(blob, x, y, flags);
SkPDF::SetNodeId(canvas_, 0);
}

void SkiaPaintCanvas::drawPicture(sk_sp<const PaintRecord> record) {
Expand Down
16 changes: 16 additions & 0 deletions components/printing/renderer/print_render_frame_helper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,10 @@ bool PrintRenderFrameHelper::Delegate::IsScriptedPrintEnabled() {
return true;
}

bool PrintRenderFrameHelper::Delegate::ShouldGenerateTaggedPDF() {
return false;
}

PrintRenderFrameHelper::PrintRenderFrameHelper(
content::RenderFrame* render_frame,
std::unique_ptr<Delegate> delegate)
Expand Down Expand Up @@ -1865,6 +1869,18 @@ bool PrintRenderFrameHelper::PrintPagesNative(blink::WebLocalFrame* frame,
print_params.document_cookie);
CHECK(metafile.Init());

// If tagged PDF exporting is enabled, we also need to capture an
// accessibility tree and store it in the metafile. AXTreeSnapshotter
// should stay alive through the end of this function, because text
// drawing commands are only annotated with a DOMNodeId if accessibility
// is enabled.
std::unique_ptr<content::AXTreeSnapshotter> snapshotter;
if (delegate_->ShouldGenerateTaggedPDF()) {
snapshotter = render_frame()->CreateAXTreeSnapshotter();
snapshotter->Snapshot(ui::kAXModeComplete, 0,
&metafile.accessibility_tree());
}

PrintHostMsg_DidPrintDocument_Params page_params;
gfx::Size* page_size_in_dpi;
gfx::Rect* content_area_in_dpi;
Expand Down
4 changes: 4 additions & 0 deletions components/printing/renderer/print_render_frame_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ class PrintRenderFrameHelper
// The default implementation returns |true|.
virtual bool IsScriptedPrintEnabled();

// Whether we should send extra metadata necessary to produce a tagged
// (accessible) PDF.
virtual bool ShouldGenerateTaggedPDF();

// Returns true if printing is overridden and the default behavior should be
// skipped for |frame|.
virtual bool OverridePrint(blink::WebLocalFrame* frame) = 0;
Expand Down
1 change: 1 addition & 0 deletions components/services/pdf_compositor/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ static_library("pdf_compositor") {
"//printing/common",
"//skia",
"//third_party/blink/public:blink_headers",
"//ui/accessibility",
]

if (is_win) {
Expand Down
1 change: 1 addition & 0 deletions components/services/pdf_compositor/DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ include_rules = [
"+third_party/skia",
"+third_party/blink/public/platform", # Test web sandbox support.
"+ui/base/ui_base_features.h", # UI features.
"+ui/accessibility", # Tagged PDF exporting.
]
6 changes: 4 additions & 2 deletions components/services/pdf_compositor/pdf_compositor_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "third_party/skia/include/core/SkGraphics.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/src/utils/SkMultiPictureDocument.h"
#include "ui/accessibility/ax_tree_update.h"

#if defined(OS_WIN)
#include "content/public/child/dwrite_font_proxy_init_win.h"
Expand Down Expand Up @@ -336,7 +337,8 @@ mojom::PdfCompositor::Status PdfCompositorImpl::CompositeToPdf(
}

SkDynamicMemoryWStream wstream;
sk_sp<SkDocument> doc = MakePdfDocument(creator_, &wstream);
sk_sp<SkDocument> doc =
MakePdfDocument(creator_, ui::AXTreeUpdate(), &wstream);

for (const auto& page : pages) {
SkCanvas* canvas = doc->beginPage(page.fSize.width(), page.fSize.height());
Expand Down Expand Up @@ -447,7 +449,7 @@ PdfCompositorImpl::FrameInfo::FrameInfo() = default;
PdfCompositorImpl::FrameInfo::~FrameInfo() = default;

PdfCompositorImpl::DocumentInfo::DocumentInfo(const std::string& creator)
: doc(MakePdfDocument(creator, &compositor_stream)) {}
: doc(MakePdfDocument(creator, ui::AXTreeUpdate(), &compositor_stream)) {}

PdfCompositorImpl::DocumentInfo::~DocumentInfo() = default;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ void AXTreeSnapshotterImpl::SnapshotContentTree(ui::AXMode ax_mode,

BlinkAXTreeSource tree_source(render_frame_, ax_mode);
tree_source.SetRoot(root);
tree_source.EnableDOMNodeIDs();
ScopedFreezeBlinkAXTreeSource freeze(&tree_source);

// The serializer returns an AXContentTreeUpdate, which can store a complete
Expand Down
5 changes: 5 additions & 0 deletions headless/app/headless_shell_switches.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const char kEnableBeginFrameControl[] = "enable-begin-frame-control";
// Enable crash reporter for headless.
const char kEnableCrashReporter[] = "enable-crash-reporter";

// If enabled, generate a tagged (accessible) file when printing to PDF.
// The plan is for this to go away once tagged PDFs become the default.
// See https://crbug.com/607777
const char kExportTaggedPDF[] = "export-tagged-pdf";

// Disable crash reporter for headless. It is enabled by default in official
// builds.
const char kDisableCrashReporter[] = "disable-crash-reporter";
Expand Down
1 change: 1 addition & 0 deletions headless/app/headless_shell_switches.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ HEADLESS_EXPORT extern const char kDiskCacheDir[];
HEADLESS_EXPORT extern const char kDumpDom[];
HEADLESS_EXPORT extern const char kEnableBeginFrameControl[];
HEADLESS_EXPORT extern const char kEnableCrashReporter[];
HEADLESS_EXPORT extern const char kExportTaggedPDF[];
HEADLESS_EXPORT extern const char kHideScrollbars[];
HEADLESS_EXPORT extern const char kPasswordStore[];
HEADLESS_EXPORT extern const char kPrintToPDF[];
Expand Down
3 changes: 3 additions & 0 deletions headless/lib/browser/headless_content_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ void HeadlessContentBrowserClient::AppendExtraCommandLineSwitches(
command_line->AppendSwitch(::switches::kEnableCrashReporter);
#endif // defined(HEADLESS_USE_BREAKPAD)

if (old_command_line.HasSwitch(switches::kExportTaggedPDF))
command_line->AppendSwitch(switches::kExportTaggedPDF);

// If we're spawning a renderer, then override the language switch.
std::string process_type =
command_line->GetSwitchValueASCII(::switches::kProcessType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// found in the LICENSE file.

#include "headless/lib/renderer/headless_print_render_frame_helper_delegate.h"
#include "base/command_line.h"
#include "headless/app/headless_shell_switches.h"

#include "third_party/blink/public/web/web_element.h"

Expand All @@ -23,6 +25,11 @@ bool HeadlessPrintRenderFrameHelperDelegate::IsPrintPreviewEnabled() {
return false;
}

bool HeadlessPrintRenderFrameHelperDelegate::ShouldGenerateTaggedPDF() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
headless::switches::kExportTaggedPDF);
}

bool HeadlessPrintRenderFrameHelperDelegate::OverridePrint(
blink::WebLocalFrame* frame) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class HeadlessPrintRenderFrameHelperDelegate
private:
// printing::PrintRenderFrameHelper::Delegate:
bool IsPrintPreviewEnabled() override;
bool ShouldGenerateTaggedPDF() override;
bool OverridePrint(blink::WebLocalFrame* frame) override;
blink::WebElement GetPdfElement(blink::WebLocalFrame* frame) override;

Expand Down
1 change: 1 addition & 0 deletions printing/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ component("printing") {
"//printing/common",
"//skia",
"//third_party/icu",
"//ui/accessibility",
"//ui/gfx",
"//ui/gfx/geometry",
"//url",
Expand Down
1 change: 1 addition & 0 deletions printing/DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ include_rules = [
"+third_party/icu/source/common/unicode",
"+third_party/icu/source/i18n/unicode",
"+third_party/skia",
"+ui/accessibility",
"+ui/android",
"+ui/aura",
"+ui/base/resource",
Expand Down
1 change: 1 addition & 0 deletions printing/common/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ source_set("common") {
"//base",
"//printing:printing_export",
"//skia",
"//ui/accessibility",
]

defines = [ "PRINTING_IMPLEMENTATION" ]
Expand Down
99 changes: 98 additions & 1 deletion printing/common/metafile_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
#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_tree.h"
#include "ui/accessibility/ax_tree_update.h"

namespace {

Expand Down Expand Up @@ -38,11 +42,94 @@ sk_sp<SkPicture> GetEmptyPicture() {
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->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:
case ax::mojom::Role::kRowHeader:
tag->fType = SkPDF::DocumentStructureType::kTH;
break;
case ax::mojom::Role::kCell:
tag->fType = SkPDF::DocumentStructureType::kTD;
break;
case ax::mojom::Role::kFigure:
case ax::mojom::Role::kImage:
tag->fType = SkPDF::DocumentStructureType::kFigure;
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;
}

tag->fChildCount = ax_node->GetUnignoredChildCount();
// Allocated here, cleaned up in DestroyStructureElementNodeTree().
SkPDF::StructureElementNode* children =
new SkPDF::StructureElementNode[tag->fChildCount];
tag->fChildren = children;
for (size_t i = 0; i < tag->fChildCount; i++) {
bool success = RecursiveBuildStructureTree(
ax_node->GetUnignoredChildAtIndex(i), &children[i]);
if (success)
valid = true;
}

return valid;
}

void DestroyStructureElementNodeTree(SkPDF::StructureElementNode* node) {
for (size_t i = 0; i < node->fChildCount; i++) {
DestroyStructureElementNodeTree(
const_cast<SkPDF::StructureElementNode*>(&node->fChildren[i]));
}
delete[] node->fChildren;
}

} // 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());
Expand All @@ -56,7 +143,17 @@ sk_sp<SkDocument> MakePdfDocument(const std::string& creator,
base::FeatureList::IsEnabled(printing::features::kHarfBuzzPDFSubsetter)
? SkPDF::Metadata::kHarfbuzz_Subsetter
: SkPDF::Metadata::kSfntly_Subsetter;
return SkPDF::MakeDocument(stream, metadata);

SkPDF::StructureElementNode tag_root = {};
if (!accessibility_tree.nodes.empty()) {
ui::AXTree tree(accessibility_tree);
if (RecursiveBuildStructureTree(tree.root(), &tag_root))
metadata.fStructureElementTreeRoot = &tag_root;
}

sk_sp<SkDocument> document = SkPDF::MakeDocument(stream, metadata);
DestroyStructureElementNodeTree(&tag_root);
return document;
}

sk_sp<SkData> SerializeOopPicture(SkPicture* pic, void* ctx) {
Expand Down
2 changes: 2 additions & 0 deletions printing/common/metafile_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/include/core/SkStream.h"
#include "ui/accessibility/ax_tree_update_forward.h"

namespace printing {

Expand All @@ -34,6 +35,7 @@ using DeserializationContext = base::flat_map<uint32_t, sk_sp<SkPicture>>;
using SerializationContext = ContentToProxyIdMap;

sk_sp<SkDocument> MakePdfDocument(const std::string& creator,
const ui::AXTreeUpdate& accessibility_tree,
SkWStream* stream);

SkSerialProcs SerializationProcs(SerializationContext* ctx);
Expand Down
2 changes: 1 addition & 1 deletion printing/metafile_skia.cc
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ bool MetafileSkia::FinishDocument() {
cc::PlaybackParams::CustomDataRasterCallback custom_callback;
switch (data_->type) {
case SkiaDocumentType::PDF:
doc = MakePdfDocument(printing::GetAgent(), &stream);
doc = MakePdfDocument(printing::GetAgent(), accessibility_tree_, &stream);
break;
case SkiaDocumentType::MSKP:
SkSerialProcs procs = SerializationProcs(&data_->subframe_content_info);
Expand Down
8 changes: 8 additions & 0 deletions printing/metafile_skia.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "printing/common/metafile_utils.h"
#include "printing/metafile.h"
#include "skia/ext/platform_canvas.h"
#include "ui/accessibility/ax_tree_update.h"

#if defined(OS_WIN)
#include <windows.h>
Expand Down Expand Up @@ -95,6 +96,11 @@ class PRINTING_EXPORT MetafileSkia : public Metafile {
int GetDocumentCookie() const;
const ContentToProxyIdMap& GetSubframeContentInfo() const;

const ui::AXTreeUpdate& accessibility_tree() const {
return accessibility_tree_;
}
ui::AXTreeUpdate& accessibility_tree() { return accessibility_tree_; }

private:
FRIEND_TEST_ALL_PREFIXES(MetafileSkiaTest, TestFrameContent);

Expand All @@ -111,6 +117,8 @@ class PRINTING_EXPORT MetafileSkia : public Metafile {

std::unique_ptr<MetafileSkiaData> data_;

ui::AXTreeUpdate accessibility_tree_;

DISALLOW_COPY_AND_ASSIGN(MetafileSkia);
};

Expand Down
Loading

0 comments on commit 0a39059

Please sign in to comment.