SkPDF: add basic metadata support

Motivation: I want too finalize this API before working on the more
complex problem of adding XMP metadata for PDF/A.

BUG=skia:3110

Review URL: https://codereview.chromium.org/1359943003
This commit is contained in:
halcanary 2015-09-23 12:45:49 -07:00 committed by Commit bot
parent ab26a9b427
commit f12a1673f0
5 changed files with 162 additions and 3 deletions

View File

@ -930,6 +930,12 @@ Error PDFSink::draw(const Src& src, SkBitmap*, SkWStream* dst, SkString*) const
if (!doc) {
return "SkDocument::CreatePDF() returned nullptr";
}
SkTArray<SkDocument::Attribute> info;
info.emplace_back(SkString("Title"), src.name());
info.emplace_back(SkString("Subject"),
SkString("rendering correctness test"));
info.emplace_back(SkString("Creator"), SkString("Skia/DM"));
doc->setMetadata(info, nullptr, nullptr);
return draw_skdocument(src, doc.get(), dst);
}

View File

@ -12,6 +12,8 @@
#include "SkPicture.h"
#include "SkRect.h"
#include "SkRefCnt.h"
#include "SkString.h"
#include "SkTime.h"
class SkCanvas;
class SkWStream;
@ -104,6 +106,33 @@ public:
*/
void abort();
/**
* Set the document's metadata, if supported by the document
* type. The creationDate and modifiedDate parameters can be
* nullptr. For example:
*
* SkDocument* make_doc(SkWStream* output) {
* SkTArray<SkDocument::Attribute> info;
* info.emplace_back(SkString("Title"), SkString("..."));
* info.emplace_back(SkString("Author"), SkString("..."));
* info.emplace_back(SkString("Subject"), SkString("..."));
* info.emplace_back(SkString("Keywords"), SkString("..."));
* info.emplace_back(SkString("Creator"), SkString("..."));
* SkTime::DateTime now;
* SkTime::GetDateTime(&now);
* SkDocument* doc = SkDocument::CreatePDF(output);
* doc->setMetadata(info, &now, &now);
* return doc;
* }
*/
struct Attribute {
SkString fKey, fValue;
Attribute(const SkString& k, const SkString& v) : fKey(k), fValue(v) {}
};
virtual void setMetadata(const SkTArray<SkDocument::Attribute>&,
const SkTime::DateTime* /* creationDate */,
const SkTime::DateTime* /* modifiedDate */) {}
protected:
SkDocument(SkWStream*, void (*)(SkWStream*, bool aborted));

View File

@ -193,6 +193,14 @@ public:
return *newT;
}
/**
* Construct a new T at the back of this array.
*/
template<class... Args> T& emplace_back(Args&&... args) {
T* newT = reinterpret_cast<T*>(this->push_back_raw(1));
return *new (newT) T(skstd::forward<Args>(args)...);
}
/**
* Allocates n more default-initialized T values, and returns the address of
* the start of that new range. Note: this address is only valid until the

View File

@ -11,8 +11,11 @@
#include "SkPDFFont.h"
#include "SkPDFStream.h"
#include "SkPDFTypes.h"
#include "SkPDFUtils.h"
#include "SkStream.h"
class SkPDFDict;
static void emit_pdf_header(SkWStream* stream) {
stream->writeText("%PDF-1.4\n%");
// The PDF spec recommends including a comment with four bytes, all
@ -26,12 +29,15 @@ static void emit_pdf_footer(SkWStream* stream,
const SkPDFSubstituteMap& substitutes,
SkPDFObject* docCatalog,
int64_t objCount,
int32_t xRefFileOffset) {
int32_t xRefFileOffset,
SkPDFDict* info) {
SkPDFDict trailerDict;
// TODO(vandebo): Linearized format will take a Prev entry too.
// TODO(vandebo): PDF/A requires an ID entry.
trailerDict.insertInt("Size", int(objCount));
trailerDict.insertObjRef("Root", SkRef(docCatalog));
SkASSERT(info);
trailerDict.insertObjRef("Info", SkRef(info));
stream->writeText("trailer\n");
trailerDict.emitObject(stream, objNumMap, substitutes);
@ -156,7 +162,49 @@ static void generate_page_tree(const SkTDArray<SkPDFDict*>& pages,
}
}
struct Metadata {
SkTArray<SkDocument::Attribute> fInfo;
SkAutoTDelete<const SkTime::DateTime> fCreation;
SkAutoTDelete<const SkTime::DateTime> fModified;
};
static SkString pdf_date(const SkTime::DateTime& dt) {
int timeZoneMinutes = SkToInt(dt.fTimeZoneMinutes);
char timezoneSign = timeZoneMinutes >= 0 ? '+' : '-';
int timeZoneHours = SkTAbs(timeZoneMinutes) / 60;
timeZoneMinutes = SkTAbs(timeZoneMinutes) % 60;
return SkStringPrintf(
"D:%04u%02u%02u%02u%02u%02u%c%02d'%02d'",
static_cast<unsigned>(dt.fYear), static_cast<unsigned>(dt.fMonth),
static_cast<unsigned>(dt.fDay), static_cast<unsigned>(dt.fHour),
static_cast<unsigned>(dt.fMinute),
static_cast<unsigned>(dt.fSecond), timezoneSign, timeZoneHours,
timeZoneMinutes);
}
SkPDFDict* create_document_information_dict(const Metadata& metadata) {
SkAutoTUnref<SkPDFDict> dict(new SkPDFDict);
static const char* keys[] = {
"Title", "Author", "Subject", "Keywords", "Creator" };
for (const char* key : keys) {
for (const SkDocument::Attribute& keyValue : metadata.fInfo) {
if (keyValue.fKey.equals(key)) {
dict->insertString(key, keyValue.fValue);
}
}
}
dict->insertString("Producer", "Skia/PDF");
if (metadata.fCreation) {
dict->insertString("CreationDate", pdf_date(*metadata.fCreation.get()));
}
if (metadata.fModified) {
dict->insertString("ModDate", pdf_date(*metadata.fModified.get()));
}
return dict.detach();
}
static bool emit_pdf_document(const SkTDArray<const SkPDFDevice*>& pageDevices,
const Metadata& metadata,
SkWStream* stream) {
if (pageDevices.isEmpty()) {
return false;
@ -198,7 +246,12 @@ static bool emit_pdf_document(const SkTDArray<const SkPDFDevice*>& pageDevices,
SkPDFSubstituteMap substitutes;
perform_font_subsetting(pageDevices, &substitutes);
SkAutoTUnref<SkPDFDict> infoDict(
create_document_information_dict(metadata));
SkPDFObjNumMap objNumMap;
if (objNumMap.addObject(infoDict)) {
infoDict->addResources(&objNumMap, substitutes);
}
if (objNumMap.addObject(docCatalog.get())) {
docCatalog->addResources(&objNumMap, substitutes);
}
@ -233,7 +286,7 @@ static bool emit_pdf_document(const SkTDArray<const SkPDFDevice*>& pageDevices,
stream->writeText(" 00000 n \n");
}
emit_pdf_footer(stream, objNumMap, substitutes, docCatalog.get(), objCount,
xRefFileOffset);
xRefFileOffset, infoDict);
// The page tree has both child and parent pointers, so it creates a
// reference cycle. We must clear that cycle to properly reclaim memory.
@ -284,6 +337,8 @@ void GetCountOfFontTypes(
}
}
#endif
template <typename T> static T* clone(const T* o) { return o ? new T(*o) : nullptr; }
////////////////////////////////////////////////////////////////////////////////
namespace {
@ -325,7 +380,7 @@ protected:
bool onClose(SkWStream* stream) override {
SkASSERT(!fCanvas.get());
bool success = emit_pdf_document(fPageDevices, stream);
bool success = emit_pdf_document(fPageDevices, fMetadata, stream);
fPageDevices.unrefAll();
fCanon.reset();
return success;
@ -336,11 +391,20 @@ protected:
fCanon.reset();
}
void setMetadata(const SkTArray<SkDocument::Attribute>& info,
const SkTime::DateTime* creationDate,
const SkTime::DateTime* modifiedDate) override {
fMetadata.fInfo = info;
fMetadata.fCreation.reset(clone(creationDate));
fMetadata.fModified.reset(clone(modifiedDate));
}
private:
SkPDFCanon fCanon;
SkTDArray<const SkPDFDevice*> fPageDevices;
SkAutoTUnref<SkCanvas> fCanvas;
SkScalar fRasterDpi;
Metadata fMetadata;
};
} // namespace
///////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,52 @@
/*
* Copyright 2015 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkDocument.h"
#include "SkStream.h"
#include "SkData.h"
#include "Test.h"
DEF_TEST(SkPDF_MetadataAttribute, r) {
REQUIRE_PDF_DOCUMENT(SkPDF_MetadataAttribute, r);
SkDynamicMemoryWStream pdf;
SkAutoTUnref<SkDocument> doc(SkDocument::CreatePDF(&pdf));
SkTArray<SkDocument::Attribute> info;
info.emplace_back(SkString("Title"), SkString("A1"));
info.emplace_back(SkString("Author"), SkString("A2"));
info.emplace_back(SkString("Subject"), SkString("A3"));
info.emplace_back(SkString("Keywords"), SkString("A4"));
info.emplace_back(SkString("Creator"), SkString("A5"));
SkTime::DateTime now;
SkTime::GetDateTime(&now);
doc->setMetadata(info, &now, &now);
doc->beginPage(612.0f, 792.0f);
doc->close();
SkAutoTUnref<SkData> data(pdf.copyToData());
static const char* expectations[] = {
"/Title (A1)",
"/Author (A2)",
"/Subject (A3)",
"/Keywords (A4)",
"/Creator (A5)",
"/Producer (Skia/PDF)",
"/CreationDate (D:",
"/ModDate (D:"
};
for (const char* expectation : expectations) {
bool found = false;
size_t N = 1 + data->size() - strlen(expectation);
for (size_t i = 0; i < N; ++i) {
if (0 == memcmp(data->bytes() + i,
expectation, strlen(expectation))) {
found = true;
break;
}
}
if (!found) {
ERRORF(r, "expectation missing: '%s'.", expectation);
}
}
}