High level pdf classes and pdf specific interface.

The guts of the implementation will be in SkPDFDevice and below.  This is a first implementation of everything above that point.

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

git-svn-id: http://skia.googlecode.com/svn/trunk@602 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/include/core/SkStream.h b/include/core/SkStream.h
index 3a82991..487c7d6 100644
--- a/include/core/SkStream.h
+++ b/include/core/SkStream.h
@@ -91,6 +91,7 @@
 
     bool    writeText(const char text[]);
     bool    writeDecAsText(int32_t);
+    bool    writeBigDecAsText(int64_t, int minDigits = 0);
     bool    writeHexAsText(uint32_t, int minDigits = 0);
     bool    writeScalarAsText(SkScalar);
     
@@ -308,4 +309,3 @@
 typedef SkFILEStream SkURLStream;
 
 #endif
-
diff --git a/include/core/SkString.h b/include/core/SkString.h
index 5ecfb1e..4498bba 100644
--- a/include/core/SkString.h
+++ b/include/core/SkString.h
@@ -28,6 +28,8 @@
 
 #define SkStrAppendS32_MaxSize  11
 char*   SkStrAppendS32(char buffer[], int32_t);
+#define SkStrAppendS64_MaxSize  20
+char*   SkStrAppendS64(char buffer[], int64_t, int minDigits);
 #define SkStrAppendScalar_MaxSize  11
 char*   SkStrAppendScalar(char buffer[], SkScalar);
 
@@ -93,6 +95,7 @@
     void    insert(size_t offset, const char text[], size_t len);
     void    insertUnichar(size_t offset, SkUnichar);
     void    insertS32(size_t offset, int32_t value);
+    void    insertS64(size_t offset, int64_t value, int minDigits = 0);
     void    insertHex(size_t offset, uint32_t value, int minDigits = 0);
     void    insertScalar(size_t offset, SkScalar);
 
@@ -101,6 +104,7 @@
     void    append(const char text[], size_t len) { this->insert((size_t)-1, text, len); }
     void    appendUnichar(SkUnichar uni) { this->insertUnichar((size_t)-1, uni); }
     void    appendS32(int32_t value) { this->insertS32((size_t)-1, value); }
+    void    appendS64(int64_t value, int minDigits = 0) { this->insertS64((size_t)-1, value, minDigits); }
     void    appendHex(uint32_t value, int minDigits = 0) { this->insertHex((size_t)-1, value, minDigits); }
     void    appendScalar(SkScalar value) { this->insertScalar((size_t)-1, value); }
 
@@ -109,6 +113,7 @@
     void    prepend(const char text[], size_t len) { this->insert(0, text, len); }
     void    prependUnichar(SkUnichar uni) { this->insertUnichar(0, uni); }
     void    prependS32(int32_t value) { this->insertS32(0, value); }
+    void    prependS64(int32_t value, int minDigits = 0) { this->insertS64(0, value, minDigits); }
     void    prependHex(uint32_t value, int minDigits = 0) { this->insertHex(0, value, minDigits); }
     void    prependScalar(SkScalar value) { this->insertScalar((size_t)-1, value); }
 
@@ -165,4 +170,3 @@
 };
 
 #endif
-
diff --git a/include/pdf/SkPDFCatalog.h b/include/pdf/SkPDFCatalog.h
index 932f1c8..6e530db 100644
--- a/include/pdf/SkPDFCatalog.h
+++ b/include/pdf/SkPDFCatalog.h
@@ -34,23 +34,39 @@
     ~SkPDFCatalog();
 
     /** Add the passed object to the catalog.
-     *  @param obj        The object to add.
-     *  @param onFirstPage  Is the object on the first page.
+     *  @param obj         The object to add.
+     *  @param onFirstPage Is the object on the first page.
      */
     void addObject(SkPDFObject* obj, bool onFirstPage);
 
+    /** Inform the catalog of the object's position in the final stream.
+     *  The object should already have been added to the catalog.  Returns
+     *  the object's size.
+     *  @param obj         The object to add.
+     *  @param offset      The byte offset in the output stream of this object.
+     */
+    size_t setFileOffset(SkPDFObject* obj, size_t offset);
+
     /** Output the object number for the passed object.
-     *  @param obj        The object of interest.
-     *  @param stream     The writable output stream to send the output to.
+     *  @param obj         The object of interest.
+     *  @param stream      The writable output stream to send the output to.
      */
     void emitObjectNumber(SkWStream* stream, SkPDFObject* obj);
 
     /** Return the number of bytes that would be emitted for the passed
      *  object's object number.
-     *  @param obj        The object of interest
+     *  @param obj         The object of interest
      */
     size_t getObjectNumberSize(SkPDFObject* obj);
 
+    /** Output the cross reference table for objects in the catalog.
+     *  Returns the total number of objects.
+     *  @param stream      The writable output stream to send the output to.
+     *  @param firstPage   If true, include first page objects only, otherwise
+     *                     include all objects not on the first page.
+     */
+    int32_t emitXrefTable(SkWStream* stream, bool firstPage);
+
 private:
     struct Rec {
         Rec(SkPDFObject* object, bool onFirstPage)
@@ -68,9 +84,12 @@
     // TODO(vandebo) Make this a hash if it's a performance problem.
     SkTDArray<struct Rec> fCatalog;
 
+    // Number of objects on the first page.
+    uint32_t fFirstPageCount;
+    // Next object number to assign (on page > 1).
     uint32_t fNextObjNum;
-    bool fStartedAssigningObjNums;
-    bool fAssigningFirstPageObjNums;
+    // Next object number to assign on the first page.
+    uint32_t fNextFirstPageObjNum;
 
     int findObjectIndex(SkPDFObject* obj) const;
 
diff --git a/include/pdf/SkPDFDocument.h b/include/pdf/SkPDFDocument.h
new file mode 100644
index 0000000..91dd379
--- /dev/null
+++ b/include/pdf/SkPDFDocument.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SkPDFDocument_DEFINED
+#define SkPDFDocument_DEFINED
+
+#include "SkPDFCatalog.h"
+#include "SkPDFTypes.h"
+#include "SkRefCnt.h"
+#include "SkTDArray.h"
+
+class SkPDFDevice;
+class SkPDFPage;
+class SkWSteam;
+
+/** \class SkPDFDocument
+
+    A SkPDFDocument assembles pages together and generates the final PDF file.
+*/
+class SkPDFDocument {
+public:
+    /** Create a PDF document.
+     */
+    SkPDFDocument();
+    ~SkPDFDocument();
+
+    /** Output the PDF to the passed stream.
+     *  @param stream    The writable output stream to send the PDF to.
+     */
+    bool emitPDF(SkWStream* stream);
+
+    /** Append the passed pdf device to the document as a new page.  Returns
+     *  true if successful.  Will fail if the document has already been emitted.
+     *
+     *  @param pdfDevice The page to add to this document.
+     */
+    bool appendPage(const SkRefPtr<SkPDFDevice>& pdfDevice);
+
+private:
+    SkPDFCatalog fCatalog;
+    int64_t fXRefFileOffset;
+
+    SkTDArray<SkPDFPage*> fPages;
+    SkTDArray<SkPDFDict*> fPageTree;
+    SkRefPtr<SkPDFDict> fDocCatalog;
+    SkTDArray<SkPDFObject*> fPageResources;
+    int fSecondPageFirstResouceIndex;
+
+    SkRefPtr<SkPDFDict> fTrailerDict;
+
+    /** Output the PDF header to the passed stream.
+     *  @param stream    The writable output stream to send the header to.
+     */
+    void emitHeader(SkWStream* stream);
+
+    /** Get the size of the header.
+     */
+    size_t headerSize();
+
+    /** Output the PDF footer to the passed stream.
+     *  @param stream    The writable output stream to send the footer to.
+     *  @param objCount  The number of objects in the PDF.
+     */
+    void emitFooter(SkWStream* stream, int64_t objCount);
+};
+
+#endif
diff --git a/include/pdf/SkPDFPage.h b/include/pdf/SkPDFPage.h
new file mode 100644
index 0000000..7e96212
--- /dev/null
+++ b/include/pdf/SkPDFPage.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SkPDFPage_DEFINED
+#define SkPDFPage_DEFINED
+
+#include "SkPDFTypes.h"
+#include "SkPDFStream.h"
+#include "SkRefCnt.h"
+#include "SkTDArray.h"
+
+class SkPDFCatalog;
+class SkPDFDevice;
+class SkWStream;
+
+/** \class SkPDFPage
+
+    A SkPDFPage contains meta information about a page, is used in the page
+    tree and points to the content of the page.
+*/
+class SkPDFPage : public SkPDFDict {
+public:
+    /** Create a PDF page with the passed PDF device.  The device need not
+     *  have content on it yet.
+     *  @param content    The page content.
+     */
+    explicit SkPDFPage(const SkRefPtr<SkPDFDevice>& content);
+    ~SkPDFPage();
+
+    /** Before a page and its contents can be sized and emitted, it must
+     *  be finalized.  No changes to the PDFDevice will be honored after
+     *  finalizePage has been called.  This function adds the page content
+     *  to the passed catalog, so it must be called for each document
+     *  that the page is part of.
+     *  @param catalog        The catalog to add page content objects to.
+     *  @param firstPage      Indicate if this is the first page of a document.
+     *  @param resouceObjects The resource objects used on the page are added
+     *                        to this array.  This gives the caller a chance
+     *                        to deduplicate resources across pages.
+     */
+    void finalizePage(SkPDFCatalog* catalog, bool firstPage,
+                      SkTDArray<SkPDFObject*>* resourceObjects);
+
+    /** Determine the size of the page content and store to the catalog
+     *  the offsets of all nonresource-indirect objects that make up the page
+     *  content.  This must be called before emitPage(), but after finalizePage.
+     *  @param catalog    The catalog to add the object offsets to.
+     *  @param fileOffset The file offset where the page content will be
+     *                    emitted.
+     */
+    off_t getPageSize(SkPDFCatalog* catalog, off_t fileOffset);
+
+    /** Output the page content to the passed stream.
+     *  @param stream     The writable output stream to send the content to.
+     *  @param catalog    The active object catalog.
+     */
+    void emitPage(SkWStream* stream, SkPDFCatalog* catalog);
+
+    /** Generate a page tree for the passed vector of pages.  New objects are
+     *  added to the catalog.  The pageTree vector is populated with all of
+     *  the 'Pages' dictionaries as well as the 'Page' objects.  Page trees
+     *  have both parent and children links, creating reference cycles, so
+     *  it must be torn down explicitly.  The first page is not added to
+     *  the pageTree dictionary array so the caller can handle it specially.
+     *  @param pages      The ordered vector of page objects.
+     *  @param catalog    The catalog to add new objects into.
+     *  @param pageTree   An output vector with all of the internal and leaf
+     *                    nodes of the pageTree.
+     *  @param rootNode   An output parameter set to the root node.
+     */
+    static void generatePageTree(const SkTDArray<SkPDFPage*>& pages,
+                                 SkPDFCatalog* catalog,
+                                 SkTDArray<SkPDFDict*>* pageTree,
+                                 SkPDFDict** rootNode);
+
+private:
+    // Multiple pages may reference the content.
+    SkRefPtr<SkPDFDevice> fDevice;
+
+    SkString fContent;
+    // Once the content is finalized, put it into a stream for output.
+    SkRefPtr<SkPDFStream> fContentStream;
+};
+
+#endif
diff --git a/include/pdf/SkPDFStream.h b/include/pdf/SkPDFStream.h
index b83aa59..24a9642 100644
--- a/include/pdf/SkPDFStream.h
+++ b/include/pdf/SkPDFStream.h
@@ -44,11 +44,17 @@
     virtual size_t getOutputSize(SkPDFCatalog* catalog, bool indirect);
 
     /** Add the value to the stream dictionary with the given key.
-     *  @param index The index into the array to set.
-     *  @param value The value to add to the array.
+     *  @param key   The key for this dictionary entry.
+     *  @param value The value for this dictionary entry.
      */
     void insert(SkPDFName* key, SkPDFObject* value);
 
+    /** Add the value to the stream dictionary with the given key.
+     *  @param key   The text of the key for this dictionary entry.
+     *  @param value The value for this dictionary entry.
+     */
+    void insert(const char key[], SkPDFObject* value);
+
 private:
     SkPDFDict fDict;
     SkRefPtr<SkStream> fData;
diff --git a/include/pdf/SkPDFTypes.h b/include/pdf/SkPDFTypes.h
index 9324595..e0b1041 100644
--- a/include/pdf/SkPDFTypes.h
+++ b/include/pdf/SkPDFTypes.h
@@ -147,7 +147,7 @@
     virtual size_t getOutputSize(SkPDFCatalog* catalog, bool indirect);
 
 private:
-    static const uint32_t kMaxLen = 65535;
+    static const size_t kMaxLen = 65535;
 
     const SkString fValue;
 
@@ -173,7 +173,7 @@
     virtual size_t getOutputSize(SkPDFCatalog* catalog, bool indirect);
 
 private:
-    static const uint32_t kMaxLen = 127;
+    static const size_t kMaxLen = 127;
 
     const SkString fValue;
 
@@ -235,6 +235,12 @@
     /** Create a PDF dictionary. Maximum number of entries is 4095.
      */
     SkPDFDict();
+
+    /** Create a PDF dictionary with a Type entry.
+     *  @param type   The value of the Type entry.
+     */
+    explicit SkPDFDict(const char type[]);
+
     virtual ~SkPDFDict();
 
     // The SkPDFObject interface.
@@ -252,6 +258,17 @@
      */
     void insert(SkPDFName* key, SkPDFObject* value);
 
+    /** Add the value to the dictionary with the given key.  The method will
+     *  create the SkPDFName object.
+     *  @param key   The text of the key for this dictionary entry.
+     *  @param value The value for this dictionary entry.
+     */
+    void insert(const char key[], SkPDFObject* value);
+
+    /** Remove all entries from the dictionary.
+     */
+    void clear();
+
 private:
     static const int kMaxLen = 4095;
 
diff --git a/src/core/SkStream.cpp b/src/core/SkStream.cpp
index 6187f81..cbe3eb4 100644
--- a/src/core/SkStream.cpp
+++ b/src/core/SkStream.cpp
@@ -123,6 +123,13 @@
     return this->write(tmp.c_str(), tmp.size());
 }
 
+bool SkWStream::writeBigDecAsText(int64_t dec, int minDigits)
+{
+    SkString    tmp;
+    tmp.appendS64(dec, minDigits);
+    return this->write(tmp.c_str(), tmp.size());
+}
+
 bool SkWStream::writeHexAsText(uint32_t hex, int digits)
 {
     SkString    tmp;
@@ -725,4 +732,3 @@
 #endif
     return true;
 }
-
diff --git a/src/core/SkString.cpp b/src/core/SkString.cpp
index d595d51..cdce160 100644
--- a/src/core/SkString.cpp
+++ b/src/core/SkString.cpp
@@ -79,6 +79,39 @@
     return string;
 }
 
+char* SkStrAppendS64(char string[], int64_t dec, int minDigits)
+{
+    SkDEBUGCODE(char* start = string;)
+
+    char    buffer[SkStrAppendS64_MaxSize];
+    char*   p = buffer + sizeof(buffer);
+    bool    neg = false;
+
+    if (dec < 0) {
+        neg = true;
+        dec = -dec;
+    }
+    do {
+        *--p = SkToU8('0' + dec % 10);
+        dec /= 10;
+        minDigits--;
+    } while (dec != 0);
+    while (minDigits > 0) {
+        *--p = '0';
+        minDigits--;
+    }
+    if (neg)
+        *--p = '-';
+
+    SkASSERT(p >= buffer);
+    size_t cp_len = buffer + sizeof(buffer) - p;
+    memcpy(string, p, cp_len);
+    string += cp_len;
+
+    SkASSERT(string - start <= SkStrAppendS64_MaxSize);
+    return string;
+}
+
 char* SkStrAppendScalar(char string[], SkScalar value)
 {
     SkDEBUGCODE(char* start = string;)
@@ -440,6 +473,13 @@
     this->insert(offset, buffer, stop - buffer);
 }
 
+void SkString::insertS64(size_t offset, int64_t dec, int minDigits)
+{
+    char    buffer[SkStrAppendS64_MaxSize];
+    char*   stop = SkStrAppendS64(buffer, dec, minDigits);
+    this->insert(offset, buffer, stop - buffer);
+}
+
 void SkString::insertHex(size_t offset, uint32_t hex, int minDigits)
 {
     minDigits = SkPin32(minDigits, 0, 8);
@@ -580,4 +620,3 @@
 {
     delete[] fUCS2;
 }
-
diff --git a/src/pdf/SkPDFCatalog.cpp b/src/pdf/SkPDFCatalog.cpp
index 1349da1..7979b70 100644
--- a/src/pdf/SkPDFCatalog.cpp
+++ b/src/pdf/SkPDFCatalog.cpp
@@ -17,23 +17,35 @@
 #include "SkPDFCatalog.h"
 #include "SkPDFTypes.h"
 #include "SkStream.h"
+#include "SkTypes.h"
 
 SkPDFCatalog::SkPDFCatalog()
-    : fNextObjNum(1),
-      fStartedAssigningObjNums(false),
-      fAssigningFirstPageObjNums(false) {
+    : fFirstPageCount(0),
+      fNextObjNum(1),
+      fNextFirstPageObjNum(0) {
 }
 
 SkPDFCatalog::~SkPDFCatalog() {}
 
 void SkPDFCatalog::addObject(SkPDFObject* obj, bool onFirstPage) {
     SkASSERT(findObjectIndex(obj) == -1);
-    SkASSERT(!fStartedAssigningObjNums);
+    SkASSERT(fNextFirstPageObjNum == 0);
+    if (onFirstPage)
+        fFirstPageCount++;
 
     struct Rec newEntry(obj, onFirstPage);
     fCatalog.append(1, &newEntry);
 }
 
+size_t SkPDFCatalog::setFileOffset(SkPDFObject* obj, size_t offset) {
+    int objIndex = assignObjNum(obj) - 1;
+    SkASSERT(fCatalog[objIndex].fObjNumAssigned);
+    SkASSERT(fCatalog[objIndex].fFileOffset == 0);
+    fCatalog[objIndex].fFileOffset = offset;
+
+    return obj->getOutputSize(this, true);
+}
+
 void SkPDFCatalog::emitObjectNumber(SkWStream* stream, SkPDFObject* obj) {
     stream->writeDecAsText(assignObjNum(obj));
     stream->writeText(" 0");  // Generation number is always 0.
@@ -60,21 +72,54 @@
     if (fCatalog[currentIndex].fObjNumAssigned)
         return currentIndex + 1;
 
-    fStartedAssigningObjNums = true;
+    // First assignment.
+    if (fNextFirstPageObjNum == 0)
+        fNextFirstPageObjNum = fCatalog.count() - fFirstPageCount + 1;
+
+    uint32_t objNum;
     if (fCatalog[currentIndex].fOnFirstPage) {
-        fAssigningFirstPageObjNums = true;
+        objNum = fNextFirstPageObjNum;
+        fNextFirstPageObjNum++;
     } else {
-        SkASSERT(!fAssigningFirstPageObjNums);
+        objNum = fNextObjNum;
+        fNextObjNum++;
     }
 
     // When we assign an object an object number, we put it in that array
     // offset (minus 1 because object number 0 is reserved).
-    if (fNextObjNum - 1 != currentIndex) {
-        Rec other = fCatalog[fNextObjNum - 1];
-        fCatalog[fNextObjNum - 1] = fCatalog[currentIndex];
-        fCatalog[currentIndex] = other;
+    SkASSERT(!fCatalog[objNum - 1].fObjNumAssigned);
+    if (objNum - 1 != currentIndex)
+        SkTSwap(fCatalog[objNum - 1], fCatalog[currentIndex]);
+    fCatalog[objNum - 1].fObjNumAssigned = true;
+    return objNum;
+}
+
+int32_t SkPDFCatalog::emitXrefTable(SkWStream* stream, bool firstPage) {
+    int first = -1;
+    int last = fCatalog.count() - 1;
+    // TODO(vandebo) support linearized format.
+    //int last = fCatalog.count() - fFirstPageCount - 1;
+    //if (firstPage) {
+    //    first = fCatalog.count() - fFirstPageCount;
+    //    last = fCatalog.count() - 1;
+    //}
+
+    stream->writeText("xref\n");
+    stream->writeDecAsText(first + 1);
+    stream->writeText(" ");
+    stream->writeDecAsText(last - first + 1);
+    stream->writeText("\n");
+
+    if (first == -1) {
+        stream->writeText("0000000000 65535 f \n");
+        first++;
     }
-    fCatalog[fNextObjNum - 1].fObjNumAssigned = true;
-    fNextObjNum++;
-    return fNextObjNum - 1;
+    for (int i = first; i <= last; i++) {
+        SkASSERT(fCatalog[i].fFileOffset > 0);
+        SkASSERT(fCatalog[i].fFileOffset <= 9999999999LL);
+        stream->writeBigDecAsText(fCatalog[i].fFileOffset, 10);
+        stream->writeText(" 00000 n \n");
+    }
+
+    return fCatalog.count() + 1;
 }
diff --git a/src/pdf/SkPDFDocument.cpp b/src/pdf/SkPDFDocument.cpp
new file mode 100644
index 0000000..8597d7d
--- /dev/null
+++ b/src/pdf/SkPDFDocument.cpp
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SkPDFDevice.h"
+#include "SkPDFDocument.h"
+#include "SkPDFPage.h"
+#include "SkStream.h"
+
+// Add the resouces, starting at firstIndex to the catalog, removing any dupes.
+// A hash table would be really nice here.
+void addResoucesToCatalog(int firstIndex, bool firstPage,
+                          SkTDArray<SkPDFObject*>* resouceList,
+                          SkPDFCatalog* catalog) {
+    for (int i = firstIndex; i < resouceList->count(); i++) {
+        int index = resouceList->find((*resouceList)[i]);
+        if (index != i) {
+            // The resouce lists themselves should already be unique, so the
+            // first page resouces shouldn't have any dups (assuming the first
+            // page resouces are handled first).
+            SkASSERT(!firstPage);
+            (*resouceList)[i]->unref();
+            resouceList->removeShuffle(i);
+        } else {
+            catalog->addObject((*resouceList)[i], firstPage);
+        }
+    }
+}
+
+SkPDFDocument::SkPDFDocument() : fXRefFileOffset(0) {
+    fDocCatalog = new SkPDFDict("Catalog");
+    fDocCatalog->unref();  // SkRefPtr and new both took a reference.
+    fCatalog.addObject(fDocCatalog.get(), true);
+}
+
+SkPDFDocument::~SkPDFDocument() {
+    fPages.safeUnrefAll();
+
+    // The page tree has both child and parent pointers, so it creates a
+    // reference cycle.  We must clear that cycle to properly reclaim memory.
+    for (int i = 0; i < fPageTree.count(); i++)
+        fPageTree[i]->clear();
+    fPageTree.safeUnrefAll();
+    fPageResources.safeUnrefAll();
+}
+
+bool SkPDFDocument::emitPDF(SkWStream* stream) {
+    if (fPages.isEmpty())
+        return false;
+
+    // We haven't emitted the document before if fPageTree is empty.
+    if (fPageTree.count() == 0) {
+        SkPDFDict* pageTreeRoot;
+        SkPDFPage::generatePageTree(fPages, &fCatalog, &fPageTree,
+                                    &pageTreeRoot);
+        SkRefPtr<SkPDFObjRef> pageTreeRootRef = new SkPDFObjRef(pageTreeRoot);
+        pageTreeRootRef->unref();  // SkRefPtr and new both took a reference.
+        fDocCatalog->insert("Pages", pageTreeRootRef.get());
+        bool first_page = true;
+        for (int i = 0; i < fPages.count(); i++) {
+            int resourceCount = fPageResources.count();
+            fPages[i]->finalizePage(&fCatalog, first_page, &fPageResources);
+            addResoucesToCatalog(resourceCount, first_page, &fPageResources,
+                                 &fCatalog);
+            if (i == 0) {
+                first_page = false;
+                fSecondPageFirstResouceIndex = fPageResources.count();
+            }
+        }
+
+        // Figure out the size of things and inform the catalog of file offsets.
+        off_t fileOffset = headerSize();
+        fileOffset += fCatalog.setFileOffset(fDocCatalog.get(), fileOffset);
+        fileOffset += fCatalog.setFileOffset(fPages[0], fileOffset);
+        fileOffset += fPages[0]->getPageSize(&fCatalog, fileOffset);
+        for (int i = 0; i < fSecondPageFirstResouceIndex; i++)
+            fileOffset += fCatalog.setFileOffset(fPageResources[i], fileOffset);
+        if (fPages.count() > 1) {
+            // TODO(vandebo) For linearized format, save the start of the
+            // first page xref table and calculate the size.
+        }
+
+        for (int i = 0; i < fPageTree.count(); i++)
+            fileOffset += fCatalog.setFileOffset(fPageTree[i], fileOffset);
+
+        for (int i = 1; i < fPages.count(); i++)
+            fileOffset += fPages[i]->getPageSize(&fCatalog, fileOffset);
+
+        for (int i = fSecondPageFirstResouceIndex;
+                 i < fPageResources.count();
+                 i++)
+            fileOffset += fCatalog.setFileOffset(fPageResources[i], fileOffset);
+
+        fXRefFileOffset = fileOffset;
+    }
+
+    emitHeader(stream);
+    fDocCatalog->emitObject(stream, &fCatalog, true);
+    fPages[0]->emitObject(stream, &fCatalog, true);
+    fPages[0]->emitPage(stream, &fCatalog);
+    for (int i = 0; i < fSecondPageFirstResouceIndex; i++)
+        fPageResources[i]->emitObject(stream, &fCatalog, true);
+    // TODO(vandebo) support linearized format
+    //if (fPages.size() > 1) {
+    //    // TODO(vandebo) save the file offset for the first page xref table.
+    //    fCatalog.emitXrefTable(stream, true);
+    //}
+
+    for (int i = 0; i < fPageTree.count(); i++)
+        fPageTree[i]->emitObject(stream, &fCatalog, true);
+
+    for (int i = 1; i < fPages.count(); i++)
+        fPages[i]->emitPage(stream, &fCatalog);
+
+    for (int i = fSecondPageFirstResouceIndex; i < fPageResources.count(); i++)
+        fPageResources[i]->emitObject(stream, &fCatalog, true);
+
+    int64_t objCount = fCatalog.emitXrefTable(stream, fPages.count() > 1);
+    emitFooter(stream, objCount);
+    return true;
+}
+
+bool SkPDFDocument::appendPage(const SkRefPtr<SkPDFDevice>& pdfDevice) {
+    if (fPageTree.count() != 0)
+        return false;
+
+    SkPDFPage* page = new SkPDFPage(pdfDevice);
+    fPages.push(page);  // Reference from new passed to fPages.
+    // The rest of the pages will be added to the catalog along with the rest
+    // of the page tree.  But the first page has to be marked as such, so we
+    // handle it here.
+    if (fPages.count() == 1)
+        fCatalog.addObject(page, true);
+    return true;
+}
+
+void SkPDFDocument::emitHeader(SkWStream* stream) {
+    stream->writeText("%PDF-1.4\n%");
+    // The PDF spec recommends including a comment with four bytes, all
+    // with their high bits set.  This is "Skia" with the high bits set.
+    stream->write32(0xD3EBE9E1);
+    stream->writeText("\n");
+}
+
+size_t SkPDFDocument::headerSize() {
+    SkDynamicMemoryWStream buffer;
+    emitHeader(&buffer);
+    return buffer.getOffset();
+}
+
+void SkPDFDocument::emitFooter(SkWStream* stream, int64_t objCount) {
+    if (fTrailerDict.get() == NULL) {
+        fTrailerDict = new SkPDFDict();
+        fTrailerDict->unref();  // SkRefPtr and new both took a reference.
+
+        SkPDFInt* objCountInt = new SkPDFInt(objCount);
+        fTrailerDict->insert("Size", objCountInt);
+        objCountInt->unref();  // insert took a ref and we're done with it.
+
+        // TODO(vandebo) Linearized format will take a Prev entry too.
+
+        SkPDFObjRef* docCatalogRef = new SkPDFObjRef(fDocCatalog.get());
+        fTrailerDict->insert("Root", docCatalogRef);
+        docCatalogRef->unref();  // insert took a ref and we're done with it.
+
+        // TODO(vandebo) PDF/A requires an ID entry.
+    }
+
+    stream->writeText("trailer\n");
+    fTrailerDict->emitObject(stream, &fCatalog, false);
+    stream->writeText("\nstartxref\n");
+    stream->writeBigDecAsText(fXRefFileOffset);
+    stream->writeText("\n%%EOF");
+}
diff --git a/src/pdf/SkPDFPage.cpp b/src/pdf/SkPDFPage.cpp
new file mode 100644
index 0000000..4183387
--- /dev/null
+++ b/src/pdf/SkPDFPage.cpp
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SkPDFCatalog.h"
+#include "SkPDFDevice.h"
+#include "SkPDFPage.h"
+#include "SkStream.h"
+
+SkPDFPage::SkPDFPage(const SkRefPtr<SkPDFDevice>& content)
+    : SkPDFDict("Page"),
+      fDevice(content) {
+}
+
+SkPDFPage::~SkPDFPage() {}
+
+void SkPDFPage::finalizePage(SkPDFCatalog* catalog, bool firstPage,
+                             SkTDArray<SkPDFObject*>* resourceObjects) {
+    if (fContentStream.get() == NULL) {
+        insert("Resources", fDevice->getResourceDict().get());
+        insert("MediaBox", fDevice->getMediaBox().get());
+
+        fContent = fDevice->content();
+        SkRefPtr<SkMemoryStream> contentStream = new SkMemoryStream(
+                fContent.c_str(), fContent.size());
+        contentStream->unref();  // SkRefPtr and new both took a reference.
+        fContentStream = new SkPDFStream(contentStream.get());
+        fContentStream->unref();  // SkRefPtr and new both took a reference.
+        SkRefPtr<SkPDFObjRef> contentRef =
+            new SkPDFObjRef(fContentStream.get());
+        contentRef->unref();  // SkRefPtr and new both took a reference.
+        insert("Contents", contentRef.get());
+    }
+    catalog->addObject(fContentStream.get(), firstPage);
+    fDevice->getResouces(resourceObjects);
+}
+
+off_t SkPDFPage::getPageSize(SkPDFCatalog* catalog, off_t fileOffset) {
+    SkASSERT(fContentStream.get() != NULL);
+    catalog->setFileOffset(fContentStream.get(), fileOffset);
+    return fContentStream->getOutputSize(catalog, true);
+}
+
+void SkPDFPage::emitPage(SkWStream* stream, SkPDFCatalog* catalog) {
+    SkASSERT(fContentStream.get() != NULL);
+    fContentStream->emitObject(stream, catalog, true);
+}
+
+// static
+void SkPDFPage::generatePageTree(const SkTDArray<SkPDFPage*>& pages,
+                                 SkPDFCatalog* catalog,
+                                 SkTDArray<SkPDFDict*>* pageTree,
+                                 SkPDFDict** rootNode) {
+    static const int kNodeSize = 8;
+
+    SkRefPtr<SkPDFName> kidsName = new SkPDFName("Kids");
+    kidsName->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFName> countName = new SkPDFName("Count");
+    countName->unref();  // SkRefPtr and new both took a reference.
+    SkRefPtr<SkPDFName> parentName = new SkPDFName("Parent");
+    parentName->unref();  // SkRefPtr and new both took a reference.
+
+    // curNodes takes a reference to its items, which it passes to pageTree.
+    SkTDArray<SkPDFDict*> curNodes;
+    curNodes.setReserve(pages.count());
+    for (int i = 0; i < pages.count(); i++) {
+        pages[i]->safeRef();
+        curNodes.push(pages[i]);
+    }
+
+    // nextRoundNodes passes its references to nodes on to curNodes.
+    SkTDArray<SkPDFDict*> nextRoundNodes;
+    nextRoundNodes.setReserve((pages.count() + kNodeSize - 1)/kNodeSize);
+
+    do {
+        for (int i = 0; i < curNodes.count(); ) {
+            if (i > 0 && i + 1 == curNodes.count()) {
+                nextRoundNodes.push(curNodes[i]);
+                break;
+            }
+
+            SkPDFDict* newNode = new SkPDFDict("Pages");
+            SkRefPtr<SkPDFObjRef> newNodeRef = new SkPDFObjRef(newNode);
+            newNodeRef->unref();  // SkRefPtr and new both took a reference.
+
+            SkRefPtr<SkPDFArray> kids = new SkPDFArray;
+            kids->unref();  // SkRefPtr and new both took a reference.
+            kids->reserve(kNodeSize);
+
+            int count = 0;
+            for (; i < curNodes.count() && count < kNodeSize; i++, count++) {
+                curNodes[i]->insert(parentName.get(), newNodeRef.get());
+                SkRefPtr<SkPDFObjRef> nodeRef = new SkPDFObjRef(curNodes[i]);
+                nodeRef->unref();  // SkRefPtr and new both took a reference.
+                kids->append(nodeRef.get());
+
+                // TODO(vandebo) put the objects in strict access order.
+                // Probably doesn't matter because they are so small.
+                if (curNodes[i] != pages[0]) {
+                    pageTree->push(curNodes[i]); // Transfer reference.
+                    catalog->addObject(curNodes[i], false);
+                } else {
+                    curNodes[i]->safeUnref();
+                }
+            }
+
+            newNode->insert(kidsName.get(), kids.get());
+            SkRefPtr<SkPDFInt> countVal = new SkPDFInt(count);
+            countVal->unref();  // SkRefPtr and new both took a reference.
+            newNode->insert(countName.get(), countVal.get());
+            nextRoundNodes.push(newNode);  // Transfer reference.
+        }
+
+        curNodes = nextRoundNodes;
+        nextRoundNodes.rewind();
+    } while(curNodes.count() > 1);
+
+    pageTree->push(curNodes[0]); // Transfer reference.
+    catalog->addObject(curNodes[0], false);
+    if (rootNode)
+        *rootNode = curNodes[0];
+}
diff --git a/src/pdf/SkPDFStream.cpp b/src/pdf/SkPDFStream.cpp
index a7fbc62..b880d4a 100644
--- a/src/pdf/SkPDFStream.cpp
+++ b/src/pdf/SkPDFStream.cpp
@@ -19,11 +19,9 @@
 #include "SkStream.h"
 
 SkPDFStream::SkPDFStream(SkStream* stream) : fData(stream) {
-    SkRefPtr<SkPDFName> lenKey = new SkPDFName("Length");
-    lenKey->unref();  // SkRefPtr and new both took a reference.
     SkRefPtr<SkPDFInt> lenValue = new SkPDFInt(fData->read(NULL, 0));
     lenValue->unref();  // SkRefPtr and new both took a reference.
-    fDict.insert(lenKey.get(), lenValue.get());
+    fDict.insert("Length", lenValue.get());
 }
 
 SkPDFStream::~SkPDFStream() {
@@ -37,7 +35,7 @@
     fDict.emitObject(stream, catalog, false);
     stream->writeText(" stream\n");
     stream->write(fData->getMemoryBase(), fData->read(NULL, 0));
-    stream->writeText("endstream\n");
+    stream->writeText("endstream");
 }
 
 size_t SkPDFStream::getOutputSize(SkPDFCatalog* catalog, bool indirect) {
@@ -45,9 +43,13 @@
         return getIndirectOutputSize(catalog);
 
     return fDict.getOutputSize(catalog, false) +
-        strlen(" stream\nendstream\n") + fData->read(NULL, 0);
+        strlen(" stream\nendstream") + fData->read(NULL, 0);
 }
 
 void SkPDFStream::insert(SkPDFName* key, SkPDFObject* value) {
     fDict.insert(key, value);
 }
+
+void SkPDFStream::insert(const char key[], SkPDFObject* value) {
+    fDict.insert(key, value);
+}
diff --git a/src/pdf/SkPDFTypes.cpp b/src/pdf/SkPDFTypes.cpp
index f5f23ca..a01fcba 100644
--- a/src/pdf/SkPDFTypes.cpp
+++ b/src/pdf/SkPDFTypes.cpp
@@ -211,11 +211,15 @@
 }
 
 SkPDFDict::SkPDFDict() {}
+
+SkPDFDict::SkPDFDict(const char type[]) {
+    SkRefPtr<SkPDFName> typeName = new SkPDFName(type);
+    typeName->unref();  // SkRefPtr and new both took a reference.
+    insert("Type", typeName.get());
+}
+
 SkPDFDict::~SkPDFDict() {
-    for (int i = 0; i < fValue.count(); i++) {
-        SkSafeUnref(fValue[i].key);
-        SkSafeUnref(fValue[i].value);
-    }
+    clear();
 }
 
 void SkPDFDict::emitObject(SkWStream* stream, SkPDFCatalog* catalog,
@@ -252,3 +256,17 @@
     newEntry->value = value;
     SkSafeRef(newEntry->value);
 }
+
+void SkPDFDict::insert(const char key[], SkPDFObject* value) {
+    SkRefPtr<SkPDFName> keyName = new SkPDFName(key);
+    keyName->unref();  // SkRefPtr and new both took a reference.
+    insert(keyName.get(), value);
+}
+
+void SkPDFDict::clear() {
+    for (int i = 0; i < fValue.count(); i++) {
+        fValue[i].key->safeUnref();
+        fValue[i].value->safeUnref();
+    }
+    fValue.reset();
+}
diff --git a/src/pdf/pdf_files.mk b/src/pdf/pdf_files.mk
index 5e41eea..a28957f 100644
--- a/src/pdf/pdf_files.mk
+++ b/src/pdf/pdf_files.mk
@@ -1,5 +1,6 @@
 SOURCE := \
     SkPDFCatalog.cpp \
+    SkPDFDocument.cpp \
+    SkPDFPage.cpp \
     SkPDFStream.cpp \
     SkPDFTypes.cpp \
-
diff --git a/tests/PDFPrimitivesTest.cpp b/tests/PDFPrimitivesTest.cpp
index 7e4465b..5dcb73c 100644
--- a/tests/PDFPrimitivesTest.cpp
+++ b/tests/PDFPrimitivesTest.cpp
@@ -175,12 +175,12 @@
     SkRefPtr<SkPDFStream> stream = new SkPDFStream(streamData.get());
     stream->unref();  // SkRefPtr and new both took a reference.
     CheckObjectOutput(reporter, stream.get(),
-                      "<</Length 12\n>> stream\nTest\nFoo\tBarendstream\n",
+                      "<</Length 12\n>> stream\nTest\nFoo\tBarendstream",
                       true);
     stream->insert(n1.get(), int42.get());
     CheckObjectOutput(reporter, stream.get(),
                       "<</Length 12\n/n1 42\n>> stream\nTest\nFoo\tBar"
-                      "endstream\n",
+                      "endstream",
                       true);
 
     TestCatalog(reporter);
diff --git a/tests/StringTest.cpp b/tests/StringTest.cpp
index 344c752..2e691f4 100644
--- a/tests/StringTest.cpp
+++ b/tests/StringTest.cpp
@@ -48,6 +48,22 @@
     a.set("ab");
     a.set("abc");
     a.set("abcd");
+
+    a.set("");
+    a.appendS64(72036854775808LL, 0);
+    REPORTER_ASSERT(reporter, a.equals("72036854775808"));
+
+    a.set("");
+    a.appendS64(-1844674407370LL, 0);
+    REPORTER_ASSERT(reporter, a.equals("-1844674407370"));
+
+    a.set("");
+    a.appendS64(73709551616LL, 15);
+    REPORTER_ASSERT(reporter, a.equals("000073709551616"));
+
+    a.set("");
+    a.appendS64(-429496729612LL, 15);
+    REPORTER_ASSERT(reporter, a.equals("-000429496729612"));
 }
 
 #include "TestClassDef.h"