diff --git a/dm/DMSrcSink.cpp b/dm/DMSrcSink.cpp index 10cdd0965b..a4e240bbaa 100644 --- a/dm/DMSrcSink.cpp +++ b/dm/DMSrcSink.cpp @@ -930,6 +930,12 @@ Error PDFSink::draw(const Src& src, SkBitmap*, SkWStream* dst, SkString*) const if (!doc) { return "SkDocument::CreatePDF() returned nullptr"; } + SkTArray 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); } diff --git a/include/core/SkDocument.h b/include/core/SkDocument.h index a35f448208..316d15a253 100644 --- a/include/core/SkDocument.h +++ b/include/core/SkDocument.h @@ -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 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&, + const SkTime::DateTime* /* creationDate */, + const SkTime::DateTime* /* modifiedDate */) {} + protected: SkDocument(SkWStream*, void (*)(SkWStream*, bool aborted)); diff --git a/include/core/SkTArray.h b/include/core/SkTArray.h index d67956be32..401f7084d6 100644 --- a/include/core/SkTArray.h +++ b/include/core/SkTArray.h @@ -193,6 +193,14 @@ public: return *newT; } + /** + * Construct a new T at the back of this array. + */ + template T& emplace_back(Args&&... args) { + T* newT = reinterpret_cast(this->push_back_raw(1)); + return *new (newT) T(skstd::forward(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 diff --git a/src/doc/SkDocument_PDF.cpp b/src/doc/SkDocument_PDF.cpp index 4ea9d89dd7..fb22f18ec5 100644 --- a/src/doc/SkDocument_PDF.cpp +++ b/src/doc/SkDocument_PDF.cpp @@ -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& pages, } } +struct Metadata { + SkTArray fInfo; + SkAutoTDelete fCreation; + SkAutoTDelete 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(dt.fYear), static_cast(dt.fMonth), + static_cast(dt.fDay), static_cast(dt.fHour), + static_cast(dt.fMinute), + static_cast(dt.fSecond), timezoneSign, timeZoneHours, + timeZoneMinutes); +} + +SkPDFDict* create_document_information_dict(const Metadata& metadata) { + SkAutoTUnref 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& pageDevices, + const Metadata& metadata, SkWStream* stream) { if (pageDevices.isEmpty()) { return false; @@ -198,7 +246,12 @@ static bool emit_pdf_document(const SkTDArray& pageDevices, SkPDFSubstituteMap substitutes; perform_font_subsetting(pageDevices, &substitutes); + SkAutoTUnref 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& 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 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& 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 fPageDevices; SkAutoTUnref fCanvas; SkScalar fRasterDpi; + Metadata fMetadata; }; } // namespace /////////////////////////////////////////////////////////////////////////////// diff --git a/tests/PDFMetadataAttributeTest.cpp b/tests/PDFMetadataAttributeTest.cpp new file mode 100644 index 0000000000..e58146ba2b --- /dev/null +++ b/tests/PDFMetadataAttributeTest.cpp @@ -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 doc(SkDocument::CreatePDF(&pdf)); + SkTArray 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 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); + } + } +}