[PDF] Add a concept of a substitute object to the SkPDFCatalog class.

Code by Arthur Hsu, original code review: http://codereview.appspot.com/4650060/

Review URL: http://codereview.appspot.com/4639102

git-svn-id: http://skia.googlecode.com/svn/trunk@1812 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/include/pdf/SkPDFCatalog.h b/include/pdf/SkPDFCatalog.h
index e02ffa1..794bbb1 100644
--- a/include/pdf/SkPDFCatalog.h
+++ b/include/pdf/SkPDFCatalog.h
@@ -70,6 +70,26 @@
      */
     int32_t emitXrefTable(SkWStream* stream, bool firstPage);
 
+    /** Set substitute object for the passed object.
+     */
+    void setSubstitute(SkPDFObject* original, SkPDFObject* substitute);
+
+    /** Find and return any substitute object set for the passed object. If
+     *  there is none, return the passed object.
+     */
+    SkPDFObject* getSubstituteObject(SkPDFObject* object);
+
+    /** Set file offsets for the resources of substitute objects.
+     *  @param fileOffset Accumulated offset of current document.
+     *  @param firstPage  Indicate whether this is for the first page only.
+     *  @return           Accumulated offset of resources of substitute objects.
+     */
+    off_t setSubstituteResourcesOffsets(off_t fileOffset, bool firstPage);
+
+    /** Emit the resources of substitute objects.
+     */
+    void emitSubstituteResources(SkWStream* stream, bool firstPage);
+
 private:
     struct Rec {
         Rec(SkPDFObject* object, bool onFirstPage)
@@ -84,9 +104,22 @@
         bool fOnFirstPage;
     };
 
+    struct SubstituteMapping {
+        SubstituteMapping(SkPDFObject* original, SkPDFObject* substitute)
+            : fOriginal(original), fSubstitute(substitute) {
+        }
+        SkPDFObject* fOriginal;
+        SkPDFObject* fSubstitute;
+    };
+
     // TODO(vandebo) Make this a hash if it's a performance problem.
     SkTDArray<struct Rec> fCatalog;
 
+    // TODO(arthurhsu) Make this a hash if it's a performance problem.
+    SkTDArray<SubstituteMapping> fSubstituteMap;
+    SkTDArray<SkPDFObject*> fSubstituteResourcesFirstPage;
+    SkTDArray<SkPDFObject*> fSubstituteResourcesRemaining;
+
     // Number of objects on the first page.
     uint32_t fFirstPageCount;
     // Next object number to assign (on page > 1).
@@ -97,6 +130,8 @@
     int findObjectIndex(SkPDFObject* obj) const;
 
     int assignObjNum(SkPDFObject* obj);
+
+    SkTDArray<SkPDFObject*>* getSubstituteList(bool firstPage);
 };
 
 #endif
diff --git a/include/pdf/SkPDFTypes.h b/include/pdf/SkPDFTypes.h
index 6b5146a..a115802 100644
--- a/include/pdf/SkPDFTypes.h
+++ b/include/pdf/SkPDFTypes.h
@@ -39,15 +39,6 @@
     SkPDFObject();
     virtual ~SkPDFObject();
 
-    /** Subclasses must implement this method to print the object to the
-     *  PDF file.
-     *  @param catalog  The object catalog to use.
-     *  @param indirect If true, output an object identifier with the object.
-     *  @param stream   The writable output stream to send the output to.
-     */
-    virtual void emitObject(SkWStream* stream, SkPDFCatalog* catalog,
-                            bool indirect) = 0;
-
     /** Return the size (number of bytes) of this object in the final output
      *  file. Compound objects or objects that are computationally intensive
      *  to output should override this method.
@@ -65,6 +56,12 @@
      */
     virtual void getResources(SkTDArray<SkPDFObject*>* resourceList);
 
+    /** Emit this object unless the catalog has a substitute object, in which
+     *  case emit that.
+     *  @see emitObject
+     */
+    void emit(SkWStream* stream, SkPDFCatalog* catalog, bool indirect);
+
     /** Helper function to output an indirect object.
      *  @param catalog The object catalog to use.
      *  @param stream  The writable output stream to send the output to.
@@ -75,6 +72,16 @@
      *  @param catalog The object catalog to use.
      */
     size_t getIndirectOutputSize(SkPDFCatalog* catalog);
+
+protected:
+    /** Subclasses must implement this method to print the object to the
+     *  PDF file.
+     *  @param catalog  The object catalog to use.
+     *  @param indirect If true, output an object identifier with the object.
+     *  @param stream   The writable output stream to send the output to.
+     */
+    virtual void emitObject(SkWStream* stream, SkPDFCatalog* catalog,
+                            bool indirect) = 0;
 };
 
 /** \class SkPDFObjRef
diff --git a/src/pdf/SkPDFCatalog.cpp b/src/pdf/SkPDFCatalog.cpp
index afa9d9a..5e9d018 100644
--- a/src/pdf/SkPDFCatalog.cpp
+++ b/src/pdf/SkPDFCatalog.cpp
@@ -25,10 +25,15 @@
       fNextFirstPageObjNum(0) {
 }
 
-SkPDFCatalog::~SkPDFCatalog() {}
+SkPDFCatalog::~SkPDFCatalog() {
+    fSubstituteResourcesRemaining.safeUnrefAll();
+    fSubstituteResourcesFirstPage.safeUnrefAll();
+}
 
 SkPDFObject* SkPDFCatalog::addObject(SkPDFObject* obj, bool onFirstPage) {
-    SkASSERT(findObjectIndex(obj) == -1);
+    if (findObjectIndex(obj) != -1) {  // object already added
+        return obj;
+    }
     SkASSERT(fNextFirstPageObjNum == 0);
     if (onFirstPage)
         fFirstPageCount++;
@@ -63,6 +68,12 @@
         if (fCatalog[i].fObject == obj)
             return i;
     }
+    // If it's not in the main array, check if it's a substitute object.
+    for (int i = 0; i < fSubstituteMap.count(); ++i) {
+        if (fSubstituteMap[i].fSubstitute == obj) {
+            return findObjectIndex(fSubstituteMap[i].fOriginal);
+        }
+    }
     return -1;
 }
 
@@ -126,3 +137,73 @@
 
     return fCatalog.count() + 1;
 }
+
+void SkPDFCatalog::setSubstitute(SkPDFObject* original,
+                                 SkPDFObject* substitute) {
+#if defined(SK_DEBUG)
+    // Sanity check: is the original already in substitute list?
+    for (int i = 0; i < fSubstituteMap.count(); ++i) {
+        if (original == fSubstituteMap[i].fSubstitute ||
+            original == fSubstituteMap[i].fOriginal) {
+            SkASSERT(false);
+            return;
+        }
+    }
+#endif
+    // Check if the original is on first page.
+    bool onFirstPage = false;
+    for (int i = 0; i < fCatalog.count(); ++i) {
+        if (fCatalog[i].fObject == original) {
+            onFirstPage = fCatalog[i].fOnFirstPage;
+            break;
+        }
+#if defined(SK_DEBUG)
+        if (i == fCatalog.count() - 1) {
+            SkASSERT(false);  // original not in catalog
+            return;
+        }
+#endif
+    }
+
+    SubstituteMapping newMapping(original, substitute);
+    fSubstituteMap.append(1, &newMapping);
+
+    // Add resource objects of substitute object to catalog.
+    SkTDArray<SkPDFObject*>* targetList = getSubstituteList(onFirstPage);
+    int existingSize = targetList->count();
+    newMapping.fSubstitute->getResources(targetList);
+    for (int i = existingSize; i < targetList->count(); ++i) {
+        addObject((*targetList)[i], onFirstPage);
+    }
+}
+
+SkPDFObject* SkPDFCatalog::getSubstituteObject(SkPDFObject* object) {
+    for (int i = 0; i < fSubstituteMap.count(); ++i) {
+        if (object == fSubstituteMap[i].fOriginal) {
+            return fSubstituteMap[i].fSubstitute;
+        }
+    }
+    return object;
+}
+
+off_t SkPDFCatalog::setSubstituteResourcesOffsets(off_t fileOffset,
+                                                  bool firstPage) {
+    SkTDArray<SkPDFObject*>* targetList = getSubstituteList(firstPage);
+    off_t offsetSum = fileOffset;
+    for (int i = 0; i < targetList->count(); ++i) {
+        offsetSum += setFileOffset((*targetList)[i], offsetSum);
+    }
+    return offsetSum;
+}
+
+void SkPDFCatalog::emitSubstituteResources(SkWStream *stream, bool firstPage) {
+    SkTDArray<SkPDFObject*>* targetList = getSubstituteList(firstPage);
+    for (int i = 0; i < targetList->count(); ++i) {
+        (*targetList)[i]->emit(stream, this, true);
+    }
+}
+
+SkTDArray<SkPDFObject*>* SkPDFCatalog::getSubstituteList(bool firstPage) {
+    return firstPage ? &fSubstituteResourcesFirstPage :
+                       &fSubstituteResourcesRemaining;
+}
\ No newline at end of file
diff --git a/src/pdf/SkPDFDocument.cpp b/src/pdf/SkPDFDocument.cpp
index fce100b..faa9360 100644
--- a/src/pdf/SkPDFDocument.cpp
+++ b/src/pdf/SkPDFDocument.cpp
@@ -78,14 +78,14 @@
         fDocCatalog->insert("OutputIntent", intentArray.get());
         */
 
-        bool first_page = true;
+        bool firstPage = true;
         for (int i = 0; i < fPages.count(); i++) {
             int resourceCount = fPageResources.count();
-            fPages[i]->finalizePage(&fCatalog, first_page, &fPageResources);
-            addResourcesToCatalog(resourceCount, first_page, &fPageResources,
-                                 &fCatalog);
+            fPages[i]->finalizePage(&fCatalog, firstPage, &fPageResources);
+            addResourcesToCatalog(resourceCount, firstPage, &fPageResources,
+                                  &fCatalog);
             if (i == 0) {
-                first_page = false;
+                firstPage = false;
                 fSecondPageFirstResourceIndex = fPageResources.count();
             }
         }
@@ -97,6 +97,8 @@
         fileOffset += fPages[0]->getPageSize(&fCatalog, fileOffset);
         for (int i = 0; i < fSecondPageFirstResourceIndex; i++)
             fileOffset += fCatalog.setFileOffset(fPageResources[i], fileOffset);
+        // Add the size of resources of substitute objects used on page 1.
+        fileOffset += fCatalog.setSubstituteResourcesOffsets(fileOffset, true);
         if (fPages.count() > 1) {
             // TODO(vandebo) For linearized format, save the start of the
             // first page xref table and calculate the size.
@@ -113,6 +115,7 @@
                  i++)
             fileOffset += fCatalog.setFileOffset(fPageResources[i], fileOffset);
 
+        fileOffset += fCatalog.setSubstituteResourcesOffsets(fileOffset, false);
         fXRefFileOffset = fileOffset;
     }
 
@@ -121,7 +124,8 @@
     fPages[0]->emitObject(stream, &fCatalog, true);
     fPages[0]->emitPage(stream, &fCatalog);
     for (int i = 0; i < fSecondPageFirstResourceIndex; i++)
-        fPageResources[i]->emitObject(stream, &fCatalog, true);
+        fPageResources[i]->emit(stream, &fCatalog, true);
+    fCatalog.emitSubstituteResources(stream, true);
     // TODO(vandebo) support linearized format
     //if (fPages.size() > 1) {
     //    // TODO(vandebo) save the file offset for the first page xref table.
@@ -135,8 +139,9 @@
         fPages[i]->emitPage(stream, &fCatalog);
 
     for (int i = fSecondPageFirstResourceIndex; i < fPageResources.count(); i++)
-        fPageResources[i]->emitObject(stream, &fCatalog, true);
+        fPageResources[i]->emit(stream, &fCatalog, true);
 
+    fCatalog.emitSubstituteResources(stream, false);
     int64_t objCount = fCatalog.emitXrefTable(stream, fPages.count() > 1);
     emitFooter(stream, objCount);
     return true;
diff --git a/src/pdf/SkPDFTypes.cpp b/src/pdf/SkPDFTypes.cpp
index b9420eb..600145e 100644
--- a/src/pdf/SkPDFTypes.cpp
+++ b/src/pdf/SkPDFTypes.cpp
@@ -27,9 +27,15 @@
 SkPDFObject::SkPDFObject() {}
 SkPDFObject::~SkPDFObject() {}
 
+void SkPDFObject::emit(SkWStream* stream, SkPDFCatalog* catalog,
+                       bool indirect) {
+    SkPDFObject* realObject = catalog->getSubstituteObject(this);
+    return realObject->emitObject(stream, catalog, indirect);
+}
+
 size_t SkPDFObject::getOutputSize(SkPDFCatalog* catalog, bool indirect) {
     SkDynamicMemoryWStream buffer;
-    emitObject(&buffer, catalog, indirect);
+    emit(&buffer, catalog, indirect);
     return buffer.getOffset();
 }
 
@@ -38,7 +44,7 @@
 void SkPDFObject::emitIndirectObject(SkWStream* stream, SkPDFCatalog* catalog) {
     catalog->emitObjectNumber(stream, this);
     stream->writeText(" obj\n");
-    emitObject(stream, catalog, false);
+    emit(stream, catalog, false);
     stream->writeText("\nendobj\n");
 }
 
@@ -292,7 +298,7 @@
 
     stream->writeText("[");
     for (int i = 0; i < fValue.count(); i++) {
-        fValue[i]->emitObject(stream, catalog, false);
+        fValue[i]->emit(stream, catalog, false);
         if (i + 1 < fValue.count())
             stream->writeText(" ");
     }
@@ -350,7 +356,7 @@
     for (int i = 0; i < fValue.count(); i++) {
         fValue[i].key->emitObject(stream, catalog, false);
         stream->writeText(" ");
-        fValue[i].value->emitObject(stream, catalog, false);
+        fValue[i].value->emit(stream, catalog, false);
         stream->writeText("\n");
     }
     stream->writeText(">>");
diff --git a/tests/PDFPrimitivesTest.cpp b/tests/PDFPrimitivesTest.cpp
index a5feba8..d4d1eea 100644
--- a/tests/PDFPrimitivesTest.cpp
+++ b/tests/PDFPrimitivesTest.cpp
@@ -24,6 +24,24 @@
 #include "SkScalar.h"
 #include "SkStream.h"
 
+class SkPDFTestDict : public SkPDFDict {
+public:
+    void getResources(SkTDArray<SkPDFObject*>* resourceList) {
+        resourceList->setReserve(resourceList->count() + fResources.count());
+        for (int i = 0; i < fResources.count(); i++) {
+            resourceList->push(fResources[i]);
+            fResources[i]->ref();
+        }
+    }
+
+    void addResource(SkPDFObject* object) {
+        fResources.append(1, &object);
+    }
+
+private:
+    SkTDArray<SkPDFObject*> fResources;
+};
+
 static bool stream_equals(const SkDynamicMemoryWStream& stream, size_t offset,
                           const void* buffer, size_t len) {
     SkAutoDataUnref data(stream.copyToData());
@@ -36,11 +54,12 @@
 static void CheckObjectOutput(skiatest::Reporter* reporter, SkPDFObject* obj,
                               const std::string& representation,
                               bool indirect) {
-    size_t directSize = obj->getOutputSize(NULL, false);
+    SkPDFCatalog catalog;
+    size_t directSize = obj->getOutputSize(&catalog, false);
     REPORTER_ASSERT(reporter, directSize == representation.size());
 
     SkDynamicMemoryWStream buffer;
-    obj->emitObject(&buffer, NULL, false);
+    obj->emit(&buffer, &catalog, false);
     REPORTER_ASSERT(reporter, directSize == buffer.getOffset());
     REPORTER_ASSERT(reporter, stream_equals(buffer, 0, representation.c_str(),
                                             directSize));
@@ -52,7 +71,6 @@
         static char footer[] = "\nendobj\n";
         static size_t footerLen = strlen(footer);
 
-        SkPDFCatalog catalog;
         catalog.addObject(obj, false);
 
         size_t indirectSize = obj->getOutputSize(&catalog, true);
@@ -60,7 +78,7 @@
                         indirectSize == directSize + headerLen + footerLen);
 
         buffer.reset();
-        obj->emitObject(&buffer, &catalog, true);
+        obj->emit(&buffer, &catalog, true);
         REPORTER_ASSERT(reporter, indirectSize == buffer.getOffset());
         REPORTER_ASSERT(reporter, stream_equals(buffer, 0, header, headerLen));
         REPORTER_ASSERT(reporter, stream_equals(buffer, headerLen,
@@ -121,6 +139,37 @@
                                             buffer.getOffset()));
 }
 
+static void TestSubstitute(skiatest::Reporter* reporter) {
+    SkRefPtr<SkPDFTestDict> proxy = new SkPDFTestDict();
+    proxy->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFTestDict> stub = new SkPDFTestDict();
+    stub->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFInt> int33 = new SkPDFInt(33);
+    int33->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFDict> stubResource = new SkPDFDict();
+    stubResource->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFInt> int44 = new SkPDFInt(44);
+    int44->unref();  // SkRefPtr and new both took a reference.
+
+    stub->insert("Value", int33.get());
+    stubResource->insert("InnerValue", int44.get());
+    stub->addResource(stubResource.get());
+
+    SkPDFCatalog catalog;
+    catalog.addObject(proxy.get(), false);
+    catalog.setSubstitute(proxy.get(), stub.get());
+
+    SkDynamicMemoryWStream buffer;
+    proxy->emit(&buffer, &catalog, false);
+    catalog.emitSubstituteResources(&buffer, false);
+
+    char expectedResult[] =
+        "<</Value 33\n>>1 0 obj\n<</InnerValue 44\n>>\nendobj\n";
+    REPORTER_ASSERT(reporter, buffer.getOffset() == strlen(expectedResult));
+    REPORTER_ASSERT(reporter, stream_equals(buffer, 0, expectedResult,
+                                            buffer.getOffset()));
+}
+
 static void TestPDFPrimitives(skiatest::Reporter* reporter) {
     SkRefPtr<SkPDFInt> int42 = new SkPDFInt(42);
     int42->unref();  // SkRefPtr and new both took a reference.
@@ -212,6 +261,8 @@
     TestCatalog(reporter);
 
     TestObjectRef(reporter);
+
+    TestSubstitute(reporter);
 }
 
 #include "TestClassDef.h"