JPEG(JFIF only) directly embedded into PDF
R=reed@google.com, djsollen@google.com, mtklein@google.com
Author: halcanary@google.com
Review URL: https://codereview.chromium.org/515493003
diff --git a/gyp/tests.gypi b/gyp/tests.gypi
index 91dedb3..c3b559e 100644
--- a/gyp/tests.gypi
+++ b/gyp/tests.gypi
@@ -143,6 +143,7 @@
'../tests/OSPathTest.cpp',
'../tests/ObjectPoolTest.cpp',
'../tests/OnceTest.cpp',
+ '../tests/PDFJpegEmbedTest.cpp',
'../tests/PDFPrimitivesTest.cpp',
'../tests/PackBitsTest.cpp',
'../tests/PaintTest.cpp',
diff --git a/src/pdf/SkPDFDevice.cpp b/src/pdf/SkPDFDevice.cpp
index 5946b9a..ec07a8f 100644
--- a/src/pdf/SkPDFDevice.cpp
+++ b/src/pdf/SkPDFDevice.cpp
@@ -2182,8 +2182,8 @@
return;
}
- SkAutoTUnref<SkPDFImage> image(
- SkPDFImage::CreateImage(*bitmap, subset, fEncoder));
+ SkAutoTUnref<SkPDFObject> image(
+ SkPDFCreateImageObject(*bitmap, subset, fEncoder));
if (!image) {
return;
}
diff --git a/src/pdf/SkPDFImage.cpp b/src/pdf/SkPDFImage.cpp
index 715fb48..4946051 100644
--- a/src/pdf/SkPDFImage.cpp
+++ b/src/pdf/SkPDFImage.cpp
@@ -13,6 +13,7 @@
#include "SkData.h"
#include "SkFlate.h"
#include "SkPDFCatalog.h"
+#include "SkPixelRef.h"
#include "SkRect.h"
#include "SkStream.h"
#include "SkString.h"
@@ -628,3 +629,91 @@
}
return true;
}
+
+namespace {
+/**
+ * This PDFObject assumes that its constructor was handed
+ * Jpeg-encoded data that can be directly embedded into a PDF.
+ */
+class PDFJPEGImage : public SkPDFObject {
+ SkAutoTUnref<SkData> fData;
+ int fWidth;
+ int fHeight;
+public:
+ PDFJPEGImage(SkData* data, int width, int height)
+ : fData(SkRef(data)), fWidth(width), fHeight(height) {}
+ virtual void getResources(const SkTSet<SkPDFObject*>&,
+ SkTSet<SkPDFObject*>*) SK_OVERRIDE {}
+ virtual void emitObject(
+ SkWStream* stream,
+ SkPDFCatalog* catalog, bool indirect) SK_OVERRIDE {
+ if (indirect) {
+ this->emitIndirectObject(stream, catalog);
+ return;
+ }
+ SkASSERT(fData.get());
+ const char kPrefaceFormat[] =
+ "<<"
+ "/Type /XObject\n"
+ "/Subtype /Image\n"
+ "/Width %d\n"
+ "/Height %d\n"
+ "/ColorSpace /DeviceRGB\n"
+ "/BitsPerComponent 8\n"
+ "/Filter /DCTDecode\n"
+ "/ColorTransform 0\n"
+ "/Length " SK_SIZE_T_SPECIFIER "\n"
+ ">> stream\n";
+ SkString preface(
+ SkStringPrintf(kPrefaceFormat, fWidth, fHeight, fData->size()));
+ const char kPostface[] = "\nendstream";
+ stream->write(preface.c_str(), preface.size());
+ stream->write(fData->data(), fData->size());
+ stream->write(kPostface, sizeof(kPostface));
+ }
+};
+
+/**
+ * If the bitmap is not subsetted, return its encoded data, if
+ * availible.
+ */
+static inline SkData* ref_encoded_data(const SkBitmap& bm) {
+ if ((NULL == bm.pixelRef())
+ || !bm.pixelRefOrigin().isZero()
+ || (bm.info().dimensions() != bm.pixelRef()->info().dimensions())) {
+ return NULL;
+ }
+ return bm.pixelRef()->refEncodedData();
+}
+
+/*
+ * This functions may give false negatives but no false positives.
+ */
+static bool is_jfif_jpeg(SkData* data) {
+ if (!data || (data->size() < 11)) {
+ return false;
+ }
+ const uint8_t bytesZeroToThree[] = {0xFF, 0xD8, 0xFF, 0xE0};
+ const uint8_t bytesSixToTen[] = {'J', 'F', 'I', 'F', 0};
+ // 0 1 2 3 4 5 6 7 8 9 10
+ // FF D8 FF E0 ?? ?? 'J' 'F' 'I' 'F' 00 ...
+ return ((0 == memcmp(data->bytes(), bytesZeroToThree,
+ sizeof(bytesZeroToThree)))
+ && (0 == memcmp(data->bytes() + 6, bytesSixToTen,
+ sizeof(bytesSixToTen))));
+}
+} // namespace
+
+SkPDFObject* SkPDFCreateImageObject(
+ const SkBitmap& bitmap,
+ const SkIRect& subset,
+ SkPicture::EncodeBitmap encoder) {
+ if (SkIRect::MakeWH(bitmap.width(), bitmap.height()) == subset) {
+ SkAutoTUnref<SkData> encodedData(ref_encoded_data(bitmap));
+ if (is_jfif_jpeg(encodedData)) {
+ return SkNEW_ARGS(PDFJPEGImage,
+ (encodedData, bitmap.width(), bitmap.height()));
+ }
+ }
+ return SkPDFImage::CreateImage(bitmap, subset, encoder);
+}
diff --git a/src/pdf/SkPDFImage.h b/src/pdf/SkPDFImage.h
index 10f8be6..5c026da 100644
--- a/src/pdf/SkPDFImage.h
+++ b/src/pdf/SkPDFImage.h
@@ -17,9 +17,20 @@
#include "SkRefCnt.h"
class SkBitmap;
+class SkData;
class SkPDFCatalog;
struct SkIRect;
+/**
+ * Return the mose efficient availible encoding of the given bitmap.
+ *
+ * If the bitmap has encoded JPEG data and that data can be embedded
+ * into the PDF output stream directly, use that. Otherwise, fall
+ * back on SkPDFImage::CreateImage.
+ */
+SkPDFObject* SkPDFCreateImageObject(
+ const SkBitmap&, const SkIRect& subset, SkPicture::EncodeBitmap);
+
/** \class SkPDFImage
An image XObject.
diff --git a/tests/PDFJpegEmbedTest.cpp b/tests/PDFJpegEmbedTest.cpp
new file mode 100644
index 0000000..c1d0ea8
--- /dev/null
+++ b/tests/PDFJpegEmbedTest.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 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 "SkCanvas.h"
+#include "SkImageGenerator.h"
+#include "SkData.h"
+#include "SkStream.h"
+#include "SkDecodingImageGenerator.h"
+
+#include "Resources.h"
+#include "Test.h"
+
+// Returned bitmap is lazy. Only lazy bitmaps hold onto the original data.
+static SkBitmap bitmap_from_data(SkData* data) {
+ SkASSERT(data);
+ SkBitmap bm;
+ SkInstallDiscardablePixelRef(
+ SkDecodingImageGenerator::Create(
+ data, SkDecodingImageGenerator::Options()), &bm);
+ return bm;
+}
+
+static bool is_subset_of(SkData* smaller, SkData* larger) {
+ SkASSERT(smaller && larger);
+ if (smaller->size() > larger->size()) {
+ return false;
+ }
+ size_t size = smaller->size();
+ size_t size_diff = larger->size() - size;
+ for (size_t i = 0; i <= size_diff; ++i) {
+ if (0 == memcmp(larger->bytes() + i, smaller->bytes(), size)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+static SkData* load_resource(
+ skiatest::Reporter* r, const char* test, const char* filename) {
+ SkString path(GetResourcePath(filename));
+ SkData* data = SkData::NewFromFileName(path.c_str());
+ if (!data && r->verbose()) {
+ SkDebugf("\n%s: Resource '%s' can not be found.\n",
+ test, filename);
+ }
+ return data; // May return NULL.
+}
+
+/**
+ * Test that for Jpeg files that use the JFIF colorspace, they are
+ * directly embedded into the PDF (without re-encoding) when that
+ * makes sense.
+ */
+DEF_TEST(PDFJpegEmbedTest, r) {
+ const char test[] = "PDFJpegEmbedTest";
+ SkAutoTUnref<SkData> mandrillData(
+ load_resource(r, test, "mandrill_512_q075.jpg"));
+ SkAutoTUnref<SkData> cmykData(load_resource(r, test, "CMYK.jpg"));
+ if (!mandrillData || !cmykData) {
+ return;
+ }
+
+ SkDynamicMemoryWStream pdf;
+ SkAutoTUnref<SkDocument> document(SkDocument::CreatePDF(&pdf));
+ SkCanvas* canvas = document->beginPage(642, 1028);
+
+ canvas->clear(SK_ColorLTGRAY);
+
+ SkBitmap bm1(bitmap_from_data(mandrillData));
+ canvas->drawBitmap(bm1, 65.0, 0.0, NULL);
+ SkBitmap bm2(bitmap_from_data(cmykData));
+ canvas->drawBitmap(bm2, 0.0, 512.0, NULL);
+
+ canvas->flush();
+ document->endPage();
+ document->close();
+ SkAutoTUnref<SkData> pdfData(pdf.copyToData());
+ SkASSERT(pdfData);
+ pdf.reset();
+
+ REPORTER_ASSERT(r, is_subset_of(mandrillData, pdfData));
+
+ // This JPEG uses a nonstandard colorspace - it can not be
+ // embedded into the PDF directly.
+ REPORTER_ASSERT(r, !is_subset_of(cmykData, pdfData));
+
+ // The following is for debugging purposes only.
+ const char* outputPath = getenv("SKIA_TESTS_PDF_JPEG_EMBED_OUTPUT_PATH");
+ if (outputPath) {
+ SkFILEWStream output(outputPath);
+ if (output.isValid()) {
+ output.write(pdfData->data(), pdfData->size());
+ }
+ }
+}