OLD | NEW |
1 | 1 |
2 /* | 2 /* |
3 * Copyright 2011 Google Inc. | 3 * Copyright 2011 Google Inc. |
4 * | 4 * |
5 * Use of this source code is governed by a BSD-style license that can be | 5 * Use of this source code is governed by a BSD-style license that can be |
6 * found in the LICENSE file. | 6 * found in the LICENSE file. |
7 */ | 7 */ |
8 | 8 |
9 | 9 |
10 #include "SkPDFCatalog.h" | 10 #include "SkPDFCatalog.h" |
11 #include "SkPDFDevice.h" | 11 #include "SkPDFDevice.h" |
12 #include "SkPDFDocument.h" | 12 #include "SkPDFDocument.h" |
13 #include "SkPDFFont.h" | 13 #include "SkPDFFont.h" |
14 #include "SkPDFPage.h" | 14 #include "SkPDFPage.h" |
15 #include "SkPDFTypes.h" | 15 #include "SkPDFTypes.h" |
16 #include "SkStream.h" | 16 #include "SkStream.h" |
17 | 17 |
18 static void addResourcesToCatalog(bool firstPage, | |
19 SkTSet<SkPDFObject*>* resourceSet, | |
20 SkPDFCatalog* catalog) { | |
21 for (int i = 0; i < resourceSet->count(); i++) { | |
22 catalog->addObject((*resourceSet)[i], firstPage); | |
23 } | |
24 } | |
25 | 18 |
26 static void perform_font_subsetting(SkPDFCatalog* catalog, | 19 static void perform_font_subsetting(SkPDFCatalog* catalog, |
27 const SkTDArray<SkPDFPage*>& pages, | 20 const SkTDArray<SkPDFPage*>& pages, |
28 SkTDArray<SkPDFObject*>* substitutes) { | 21 SkTDArray<SkPDFObject*>* substitutes) { |
29 SkASSERT(catalog); | 22 SkASSERT(catalog); |
30 SkASSERT(substitutes); | 23 SkASSERT(substitutes); |
31 | 24 |
32 SkPDFGlyphSetMap usage; | 25 SkPDFGlyphSetMap usage; |
33 for (int i = 0; i < pages.count(); ++i) { | 26 for (int i = 0; i < pages.count(); ++i) { |
34 usage.merge(pages[i]->getFontGlyphUsage()); | 27 usage.merge(pages[i]->getFontGlyphUsage()); |
35 } | 28 } |
36 SkPDFGlyphSetMap::F2BIter iterator(usage); | 29 SkPDFGlyphSetMap::F2BIter iterator(usage); |
37 const SkPDFGlyphSetMap::FontGlyphSetPair* entry = iterator.next(); | 30 const SkPDFGlyphSetMap::FontGlyphSetPair* entry = iterator.next(); |
38 while (entry) { | 31 while (entry) { |
39 SkPDFFont* subsetFont = | 32 SkPDFFont* subsetFont = |
40 entry->fFont->getFontSubset(entry->fGlyphSet); | 33 entry->fFont->getFontSubset(entry->fGlyphSet); |
41 if (subsetFont) { | 34 if (subsetFont) { |
42 catalog->setSubstitute(entry->fFont, subsetFont); | 35 catalog->setSubstitute(entry->fFont, subsetFont); |
43 substitutes->push(subsetFont); // Transfer ownership to substitutes | 36 substitutes->push(subsetFont); // Transfer ownership to substitutes |
44 } | 37 } |
45 entry = iterator.next(); | 38 entry = iterator.next(); |
46 } | 39 } |
47 } | 40 } |
48 | 41 |
49 SkPDFDocument::SkPDFDocument() | 42 SkPDFDocument::SkPDFDocument() {} |
50 : fXRefFileOffset(0), | 43 |
51 fTrailerDict(NULL) { | 44 SkPDFDocument::~SkPDFDocument() { fPageDevices.unrefAll(); } |
52 fCatalog.reset(SkNEW(SkPDFCatalog)); | 45 |
53 fDocCatalog = SkNEW_ARGS(SkPDFDict, ("Catalog")); | 46 static void emit_pdf_header(SkWStream* stream) { |
54 fCatalog->addObject(fDocCatalog, true); | 47 stream->writeText("%PDF-1.4\n%"); |
55 fFirstPageResources = NULL; | 48 // The PDF spec recommends including a comment with four bytes, all |
56 fOtherPageResources = NULL; | 49 // with their high bits set. This is "Skia" with the high bits set. |
| 50 stream->write32(0xD3EBE9E1); |
| 51 stream->writeText("\n"); |
57 } | 52 } |
58 | 53 |
59 SkPDFDocument::~SkPDFDocument() { | 54 static void emit_pdf_footer(SkWStream* stream, |
60 fPages.safeUnrefAll(); | 55 SkPDFCatalog* catalog, |
| 56 SkPDFObject* docCatalog, |
| 57 int64_t objCount, |
| 58 int32_t xRefFileOffset) { |
| 59 SkPDFDict trailerDict; |
| 60 // TODO(vandebo): Linearized format will take a Prev entry too. |
| 61 // TODO(vandebo): PDF/A requires an ID entry. |
| 62 trailerDict.insertInt("Size", int(objCount)); |
| 63 trailerDict.insert("Root", new SkPDFObjRef(docCatalog))->unref(); |
| 64 |
| 65 stream->writeText("trailer\n"); |
| 66 trailerDict.emitObject(stream, catalog); |
| 67 stream->writeText("\nstartxref\n"); |
| 68 stream->writeBigDecAsText(xRefFileOffset); |
| 69 stream->writeText("\n%%EOF"); |
| 70 } |
| 71 |
| 72 bool SkPDFDocument::emitPDF(SkWStream* stream) { |
| 73 // SkTDArray<SkPDFDevice*> fPageDevices; |
| 74 if (fPageDevices.isEmpty()) { |
| 75 return false; |
| 76 } |
| 77 SkTDArray<SkPDFPage*> pages; |
| 78 for (int i = 0; i < fPageDevices.count(); i++) { |
| 79 // Reference from new passed to pages. |
| 80 pages.push(SkNEW_ARGS(SkPDFPage, (fPageDevices[i]))); |
| 81 } |
| 82 SkPDFCatalog catalog; |
| 83 |
| 84 SkTDArray<SkPDFDict*> pageTree; |
| 85 SkAutoTUnref<SkPDFDict> docCatalog(SkNEW_ARGS(SkPDFDict, ("Catalog"))); |
| 86 SkTSet<SkPDFObject*> firstPageResources; |
| 87 SkTSet<SkPDFObject*> otherPageResources; |
| 88 SkTDArray<SkPDFObject*> substitutes; |
| 89 catalog.addObject(docCatalog.get(), true); |
| 90 |
| 91 SkPDFDict* pageTreeRoot; |
| 92 SkPDFPage::GeneratePageTree(pages, &catalog, &pageTree, &pageTreeRoot); |
| 93 docCatalog->insert("Pages", new SkPDFObjRef(pageTreeRoot))->unref(); |
| 94 |
| 95 /* TODO(vandebo): output intent |
| 96 SkAutoTUnref<SkPDFDict> outputIntent = new SkPDFDict("OutputIntent"); |
| 97 outputIntent->insert("S", new SkPDFName("GTS_PDFA1"))->unref(); |
| 98 outputIntent->insert("OutputConditionIdentifier", |
| 99 new SkPDFString("sRGB"))->unref(); |
| 100 SkAutoTUnref<SkPDFArray> intentArray = new SkPDFArray; |
| 101 intentArray->append(outputIntent.get()); |
| 102 docCatalog->insert("OutputIntent", intentArray.get()); |
| 103 */ |
| 104 |
| 105 SkAutoTUnref<SkPDFDict> dests(SkNEW(SkPDFDict)); |
| 106 |
| 107 bool firstPage = true; |
| 108 /* The references returned in newResources are transfered to |
| 109 * firstPageResources or otherPageResources depending on firstPage and |
| 110 * knownResources doesn't have a reference but just relies on the other |
| 111 * two sets to maintain a reference. |
| 112 */ |
| 113 SkTSet<SkPDFObject*> knownResources; |
| 114 |
| 115 // mergeInto returns the number of duplicates. |
| 116 // If there are duplicates, there is a bug and we mess ref counting. |
| 117 SkDEBUGCODE(int duplicates = ) knownResources.mergeInto(firstPageResources); |
| 118 SkASSERT(duplicates == 0); |
| 119 |
| 120 for (int i = 0; i < pages.count(); i++) { |
| 121 if (i == 1) { |
| 122 firstPage = false; |
| 123 SkDEBUGCODE(duplicates = ) |
| 124 knownResources.mergeInto(otherPageResources); |
| 125 } |
| 126 SkTSet<SkPDFObject*> newResources; |
| 127 pages[i]->finalizePage(&catalog, firstPage, knownResources, |
| 128 &newResources); |
| 129 for (int j = 0; j < newResources.count(); j++) { |
| 130 catalog.addObject(newResources[i], firstPage); |
| 131 } |
| 132 if (firstPage) { |
| 133 SkDEBUGCODE(duplicates = ) |
| 134 firstPageResources.mergeInto(newResources); |
| 135 } else { |
| 136 SkDEBUGCODE(duplicates = ) |
| 137 otherPageResources.mergeInto(newResources); |
| 138 } |
| 139 SkASSERT(duplicates == 0); |
| 140 |
| 141 SkDEBUGCODE(duplicates = ) knownResources.mergeInto(newResources); |
| 142 SkASSERT(duplicates == 0); |
| 143 |
| 144 pages[i]->appendDestinations(dests); |
| 145 } |
| 146 |
| 147 if (dests->size() > 0) { |
| 148 SkPDFDict* raw_dests = dests.get(); |
| 149 firstPageResources.add(dests.detach()); // Transfer ownership. |
| 150 catalog.addObject(raw_dests, true /* onFirstPage */); |
| 151 docCatalog->insert("Dests", SkNEW_ARGS(SkPDFObjRef, (raw_dests))) |
| 152 ->unref(); |
| 153 } |
| 154 |
| 155 // Build font subsetting info before proceeding. |
| 156 perform_font_subsetting(&catalog, pages, &substitutes); |
| 157 |
| 158 SkTSet<SkPDFObject*> resourceSet; |
| 159 if (resourceSet.add(docCatalog.get())) { |
| 160 docCatalog->addResources(&resourceSet, &catalog); |
| 161 } |
| 162 size_t baseOffset = SkToOffT(stream->bytesWritten()); |
| 163 emit_pdf_header(stream); |
| 164 for (int i = 0; i < resourceSet.count(); ++i) { |
| 165 SkPDFObject* object = resourceSet[i]; |
| 166 catalog.setFileOffset(object, |
| 167 SkToOffT(stream->bytesWritten() - baseOffset)); |
| 168 SkASSERT(object == catalog.getSubstituteObject(object)); |
| 169 stream->writeDecAsText(catalog.getObjectNumber(object)); |
| 170 stream->writeText(" 0 obj\n"); // Generation number is always 0. |
| 171 object->emitObject(stream, &catalog); |
| 172 stream->writeText("\nendobj\n"); |
| 173 } |
| 174 int32_t xRefFileOffset = SkToS32(stream->bytesWritten() - baseOffset); |
| 175 int64_t objCount = catalog.emitXrefTable(stream, pages.count() > 1); |
| 176 |
| 177 emit_pdf_footer(stream, &catalog, docCatalog.get(), objCount, |
| 178 xRefFileOffset); |
61 | 179 |
62 // The page tree has both child and parent pointers, so it creates a | 180 // The page tree has both child and parent pointers, so it creates a |
63 // reference cycle. We must clear that cycle to properly reclaim memory. | 181 // reference cycle. We must clear that cycle to properly reclaim memory. |
64 for (int i = 0; i < fPageTree.count(); i++) { | 182 for (int i = 0; i < pageTree.count(); i++) { |
65 fPageTree[i]->clear(); | 183 pageTree[i]->clear(); |
66 } | 184 } |
67 fPageTree.safeUnrefAll(); | 185 pageTree.safeUnrefAll(); |
| 186 pages.unrefAll(); |
68 | 187 |
69 if (fFirstPageResources) { | 188 firstPageResources.safeUnrefAll(); |
70 fFirstPageResources->safeUnrefAll(); | 189 otherPageResources.safeUnrefAll(); |
71 } | |
72 if (fOtherPageResources) { | |
73 fOtherPageResources->safeUnrefAll(); | |
74 } | |
75 | 190 |
76 fSubstitutes.safeUnrefAll(); | 191 substitutes.unrefAll(); |
77 | 192 docCatalog.reset(NULL); |
78 fDocCatalog->unref(); | |
79 SkSafeUnref(fTrailerDict); | |
80 SkDELETE(fFirstPageResources); | |
81 SkDELETE(fOtherPageResources); | |
82 } | |
83 | |
84 bool SkPDFDocument::emitPDF(SkWStream* stream) { | |
85 if (fPages.isEmpty()) { | |
86 return false; | |
87 } | |
88 for (int i = 0; i < fPages.count(); i++) { | |
89 if (fPages[i] == NULL) { | |
90 return false; | |
91 } | |
92 } | |
93 | |
94 fFirstPageResources = SkNEW(SkTSet<SkPDFObject*>); | |
95 fOtherPageResources = SkNEW(SkTSet<SkPDFObject*>); | |
96 | |
97 // We haven't emitted the document before if fPageTree is empty. | |
98 if (fPageTree.isEmpty()) { | |
99 SkPDFDict* pageTreeRoot; | |
100 SkPDFPage::GeneratePageTree(fPages, fCatalog.get(), &fPageTree, | |
101 &pageTreeRoot); | |
102 fDocCatalog->insert("Pages", new SkPDFObjRef(pageTreeRoot))->unref(); | |
103 | |
104 /* TODO(vandebo): output intent | |
105 SkAutoTUnref<SkPDFDict> outputIntent = new SkPDFDict("OutputIntent"); | |
106 outputIntent->insert("S", new SkPDFName("GTS_PDFA1"))->unref(); | |
107 outputIntent->insert("OutputConditionIdentifier", | |
108 new SkPDFString("sRGB"))->unref(); | |
109 SkAutoTUnref<SkPDFArray> intentArray = new SkPDFArray; | |
110 intentArray->append(outputIntent.get()); | |
111 fDocCatalog->insert("OutputIntent", intentArray.get()); | |
112 */ | |
113 | |
114 SkAutoTUnref<SkPDFDict> dests(SkNEW(SkPDFDict)); | |
115 | |
116 bool firstPage = true; | |
117 /* The references returned in newResources are transfered to | |
118 * fFirstPageResources or fOtherPageResources depending on firstPage and | |
119 * knownResources doesn't have a reference but just relies on the other | |
120 * two sets to maintain a reference. | |
121 */ | |
122 SkTSet<SkPDFObject*> knownResources; | |
123 | |
124 // mergeInto returns the number of duplicates. | |
125 // If there are duplicates, there is a bug and we mess ref counting. | |
126 SkDEBUGCODE(int duplicates =) knownResources.mergeInto(*fFirstPageResour
ces); | |
127 SkASSERT(duplicates == 0); | |
128 | |
129 for (int i = 0; i < fPages.count(); i++) { | |
130 if (i == 1) { | |
131 firstPage = false; | |
132 SkDEBUGCODE(duplicates =) knownResources.mergeInto(*fOtherPageRe
sources); | |
133 } | |
134 SkTSet<SkPDFObject*> newResources; | |
135 fPages[i]->finalizePage( | |
136 fCatalog.get(), firstPage, knownResources, &newResources); | |
137 addResourcesToCatalog(firstPage, &newResources, fCatalog.get()); | |
138 if (firstPage) { | |
139 SkDEBUGCODE(duplicates =) fFirstPageResources->mergeInto(newReso
urces); | |
140 } else { | |
141 SkDEBUGCODE(duplicates =) fOtherPageResources->mergeInto(newReso
urces); | |
142 } | |
143 SkASSERT(duplicates == 0); | |
144 | |
145 SkDEBUGCODE(duplicates =) knownResources.mergeInto(newResources); | |
146 SkASSERT(duplicates == 0); | |
147 | |
148 fPages[i]->appendDestinations(dests); | |
149 } | |
150 | |
151 if (dests->size() > 0) { | |
152 SkPDFDict* raw_dests = dests.get(); | |
153 fFirstPageResources->add(dests.detach()); // Transfer ownership. | |
154 fCatalog->addObject(raw_dests, true /* onFirstPage */); | |
155 fDocCatalog->insert("Dests", SkNEW_ARGS(SkPDFObjRef, (raw_dests)))->
unref(); | |
156 } | |
157 | |
158 // Build font subsetting info before proceeding. | |
159 perform_font_subsetting(fCatalog.get(), fPages, &fSubstitutes); | |
160 } | |
161 | |
162 SkTSet<SkPDFObject*> resourceSet; | |
163 if (resourceSet.add(fDocCatalog)) { | |
164 fDocCatalog->addResources(&resourceSet, fCatalog); | |
165 } | |
166 off_t baseOffset = SkToOffT(stream->bytesWritten()); | |
167 emitHeader(stream); | |
168 for (int i = 0; i < resourceSet.count(); ++i) { | |
169 SkPDFObject* object = resourceSet[i]; | |
170 fCatalog->setFileOffset(object, | |
171 SkToOffT(stream->bytesWritten()) - baseOffset); | |
172 SkASSERT(object == fCatalog->getSubstituteObject(object)); | |
173 stream->writeDecAsText(fCatalog->getObjectNumber(object)); | |
174 stream->writeText(" 0 obj\n"); // Generation number is always 0. | |
175 object->emitObject(stream, fCatalog); | |
176 stream->writeText("\nendobj\n"); | |
177 } | |
178 fXRefFileOffset = SkToOffT(stream->bytesWritten()) - baseOffset; | |
179 int64_t objCount = fCatalog->emitXrefTable(stream, fPages.count() > 1); | |
180 emitFooter(stream, objCount); | |
181 return true; | 193 return true; |
182 } | 194 } |
183 | 195 |
184 // TODO(halcanary): remove this method, since it is unused. | |
185 bool SkPDFDocument::setPage(int pageNumber, SkPDFDevice* pdfDevice) { | |
186 if (!fPageTree.isEmpty()) { | |
187 return false; | |
188 } | |
189 | |
190 pageNumber--; | |
191 SkASSERT(pageNumber >= 0); | |
192 | |
193 if (pageNumber >= fPages.count()) { | |
194 int oldSize = fPages.count(); | |
195 fPages.setCount(pageNumber + 1); | |
196 for (int i = oldSize; i <= pageNumber; i++) { | |
197 fPages[i] = NULL; | |
198 } | |
199 } | |
200 | |
201 SkPDFPage* page = new SkPDFPage(pdfDevice); | |
202 SkSafeUnref(fPages[pageNumber]); | |
203 fPages[pageNumber] = page; // Reference from new passed to fPages. | |
204 return true; | |
205 } | |
206 | |
207 bool SkPDFDocument::appendPage(SkPDFDevice* pdfDevice) { | |
208 if (!fPageTree.isEmpty()) { | |
209 return false; | |
210 } | |
211 | |
212 SkPDFPage* page = new SkPDFPage(pdfDevice); | |
213 fPages.push(page); // Reference from new passed to fPages. | |
214 return true; | |
215 } | |
216 | |
217 // Deprecated. | |
218 // TODO(halcanary): remove | |
219 void SkPDFDocument::getCountOfFontTypes( | |
220 int counts[SkAdvancedTypefaceMetrics::kOther_Font + 2]) const { | |
221 sk_bzero(counts, sizeof(int) * | |
222 (SkAdvancedTypefaceMetrics::kOther_Font + 2)); | |
223 SkTDArray<SkFontID> seenFonts; | |
224 int notEmbeddable = 0; | |
225 | |
226 for (int pageNumber = 0; pageNumber < fPages.count(); pageNumber++) { | |
227 const SkTDArray<SkPDFFont*>& fontResources = | |
228 fPages[pageNumber]->getFontResources(); | |
229 for (int font = 0; font < fontResources.count(); font++) { | |
230 SkFontID fontID = fontResources[font]->typeface()->uniqueID(); | |
231 if (seenFonts.find(fontID) == -1) { | |
232 counts[fontResources[font]->getType()]++; | |
233 seenFonts.push(fontID); | |
234 if (!fontResources[font]->canEmbed()) { | |
235 notEmbeddable++; | |
236 } | |
237 } | |
238 } | |
239 } | |
240 counts[SkAdvancedTypefaceMetrics::kOther_Font + 1] = notEmbeddable; | |
241 } | |
242 | |
243 // TODO(halcanary): expose notEmbeddableCount in SkDocument | 196 // TODO(halcanary): expose notEmbeddableCount in SkDocument |
244 void SkPDFDocument::getCountOfFontTypes( | 197 void SkPDFDocument::getCountOfFontTypes( |
245 int counts[SkAdvancedTypefaceMetrics::kOther_Font + 1], | 198 int counts[SkAdvancedTypefaceMetrics::kOther_Font + 1], |
246 int* notSubsettableCount, | 199 int* notSubsettableCount, |
247 int* notEmbeddableCount) const { | 200 int* notEmbeddableCount) const { |
248 sk_bzero(counts, sizeof(int) * | 201 sk_bzero(counts, sizeof(int) * |
249 (SkAdvancedTypefaceMetrics::kOther_Font + 1)); | 202 (SkAdvancedTypefaceMetrics::kOther_Font + 1)); |
250 SkTDArray<SkFontID> seenFonts; | 203 SkTDArray<SkFontID> seenFonts; |
251 int notSubsettable = 0; | 204 int notSubsettable = 0; |
252 int notEmbeddable = 0; | 205 int notEmbeddable = 0; |
253 | 206 |
254 for (int pageNumber = 0; pageNumber < fPages.count(); pageNumber++) { | 207 for (int pageNumber = 0; pageNumber < fPageDevices.count(); pageNumber++) { |
255 const SkTDArray<SkPDFFont*>& fontResources = | 208 const SkTDArray<SkPDFFont*>& fontResources = |
256 fPages[pageNumber]->getFontResources(); | 209 fPageDevices[pageNumber]->getFontResources(); |
257 for (int font = 0; font < fontResources.count(); font++) { | 210 for (int font = 0; font < fontResources.count(); font++) { |
258 SkFontID fontID = fontResources[font]->typeface()->uniqueID(); | 211 SkFontID fontID = fontResources[font]->typeface()->uniqueID(); |
259 if (seenFonts.find(fontID) == -1) { | 212 if (seenFonts.find(fontID) == -1) { |
260 counts[fontResources[font]->getType()]++; | 213 counts[fontResources[font]->getType()]++; |
261 seenFonts.push(fontID); | 214 seenFonts.push(fontID); |
262 if (!fontResources[font]->canSubset()) { | 215 if (!fontResources[font]->canSubset()) { |
263 notSubsettable++; | 216 notSubsettable++; |
264 } | 217 } |
265 if (!fontResources[font]->canEmbed()) { | 218 if (!fontResources[font]->canEmbed()) { |
266 notEmbeddable++; | 219 notEmbeddable++; |
267 } | 220 } |
268 } | 221 } |
269 } | 222 } |
270 } | 223 } |
271 if (notSubsettableCount) { | 224 if (notSubsettableCount) { |
272 *notSubsettableCount = notSubsettable; | 225 *notSubsettableCount = notSubsettable; |
273 | 226 |
274 } | 227 } |
275 if (notEmbeddableCount) { | 228 if (notEmbeddableCount) { |
276 *notEmbeddableCount = notEmbeddable; | 229 *notEmbeddableCount = notEmbeddable; |
277 } | 230 } |
278 } | 231 } |
279 | |
280 void SkPDFDocument::emitHeader(SkWStream* stream) { | |
281 stream->writeText("%PDF-1.4\n%"); | |
282 // The PDF spec recommends including a comment with four bytes, all | |
283 // with their high bits set. This is "Skia" with the high bits set. | |
284 stream->write32(0xD3EBE9E1); | |
285 stream->writeText("\n"); | |
286 } | |
287 | |
288 //TODO(halcanary): remove this function | |
289 size_t SkPDFDocument::headerSize() { | |
290 SkDynamicMemoryWStream buffer; | |
291 emitHeader(&buffer); | |
292 return buffer.getOffset(); | |
293 } | |
294 | |
295 void SkPDFDocument::emitFooter(SkWStream* stream, int64_t objCount) { | |
296 if (NULL == fTrailerDict) { | |
297 fTrailerDict = SkNEW(SkPDFDict); | |
298 | |
299 // TODO(vandebo): Linearized format will take a Prev entry too. | |
300 // TODO(vandebo): PDF/A requires an ID entry. | |
301 fTrailerDict->insertInt("Size", int(objCount)); | |
302 fTrailerDict->insert("Root", new SkPDFObjRef(fDocCatalog))->unref(); | |
303 } | |
304 | |
305 stream->writeText("trailer\n"); | |
306 fTrailerDict->emitObject(stream, fCatalog.get()); | |
307 stream->writeText("\nstartxref\n"); | |
308 stream->writeBigDecAsText(fXRefFileOffset); | |
309 stream->writeText("\n%%EOF"); | |
310 } | |
OLD | NEW |