001: /*
002: Copyright (C) Etymon Systems, Inc. <http://www.etymon.com/>
003: */
004:
005: package com.etymon.pjx;
006:
007: import java.io.*;
008: import java.util.*;
009:
010: /**
011: Manages the modification or creation of a PDF document. This class
012: is synchronized.
013: @author Nassib Nassar
014: */
015: public final class PdfManager {
016:
017: protected long _last_startxref;
018:
019: /**
020: @deprecated Do not use this method.
021: */
022: public long getStartxref() {
023: return _last_startxref;
024: }
025:
026: protected static final PdfName PDFNAME_PREV = new PdfName("Prev");
027:
028: protected static final PdfName PDFNAME_SIZE = new PdfName("Size");
029:
030: /**
031: The collection of PDF objects that have been modified. Each
032: PDF object is keyed by its associated object number (stored
033: as type Integer).
034: */
035: protected Map _modobj;
036:
037: /**
038: The total size of the cross-reference table, taking
039: together both the PDF input document and the modified
040: objects. This is also used as the next free object number.
041: */
042: protected int _modsize;
043:
044: /**
045: The PDF input document.
046: */
047: protected PdfReader _reader;
048:
049: /**
050: Returns the <code>PdfReader</code> instance associated with
051: this document.
052: */
053: public PdfReader getReader() {
054: synchronized (this ) {
055:
056: return _reader;
057:
058: }
059: }
060:
061: /**
062: The beginning offset of the cross-reference table in the
063: PDF input document.
064: */
065: protected long _startxref;
066:
067: /**
068: The document's trailer dictionary, in its current (possibly
069: modified) state.
070: */
071: protected PdfDictionary _trailer;
072:
073: /**
074: The cross-reference table and associated trailer read from
075: the PDF input document.
076: */
077: protected XrefTable _xt;
078:
079: /**
080: Constructs a <code>PdfManager</code> representing an empty
081: PDF document. The document in this initial state contains
082: no objects and therefore does not correspond to a valid PDF
083: file.
084: @param init if false, specifies that the document contain
085: no objects (and therefore will not correspond to a valid
086: PDF file); if true, that the document be initialized with
087: a minimal set of objects to create a single blank page.
088: */
089: public PdfManager(boolean init) {
090:
091: _reader = null;
092: _startxref = 0;
093: _xt = null;
094:
095: _trailer = new PdfDictionary(new HashMap());
096:
097: _modsize = 1;
098: construct();
099:
100: if (init) {
101: initialize();
102: }
103: }
104:
105: /**
106: Creates the minimal set of objects in a newly constructed
107: document in order to create a single blank page. This
108: method assumes that the document contains no objects.
109: (This method is adapted from
110: com.etymon.pj.Pdf.createEmpty().)
111: */
112: private void initialize() {
113:
114: List list;
115: Map map;
116: PdfName nameType = new PdfName("Type");
117:
118: // Make a ProcSet.
119: list = new ArrayList(2);
120: list.add(new PdfName("PDF"));
121: list.add(new PdfName("Text"));
122: PdfArray procSet = new PdfArray(list);
123: int procSetId = addObject(procSet);
124:
125: // Make a Resources dictionary.
126: map = new HashMap(1);
127: map.put(new PdfName("ProcSet"), new PdfReference(procSetId, 0));
128: PdfDictionary resources = new PdfDictionary(map);
129: int resourcesId = addObject(resources);
130:
131: // Make a MediaBox.
132: list = new ArrayList(4);
133: list.add(new PdfInteger(0));
134: list.add(new PdfInteger(0));
135: list.add(new PdfInteger(612));
136: list.add(new PdfInteger(792));
137: PdfArray mediaBox = new PdfArray(list);
138:
139: // Make a Page placeholder.
140: int pageId = addObject(new PdfDictionary());
141:
142: // Make a Kids array.
143: list = new ArrayList(1);
144: list.add(new PdfReference(pageId, 0));
145: PdfArray kids = new PdfArray(list);
146:
147: // Make the root Pages node.
148: map = new HashMap(5);
149: map.put(nameType, new PdfName("Pages"));
150: map.put(new PdfName("Resources"), new PdfReference(resourcesId,
151: 0));
152: map.put(new PdfName("MediaBox"), mediaBox);
153: map.put(new PdfName("Count"), new PdfInteger(1));
154: map.put(new PdfName("Kids"), kids);
155: int rootId = addObject(new PdfDictionary(map));
156:
157: // Go back and recreate the Page correctly, including
158: // the Parent pointer.
159: map = new HashMap(2);
160: map.put(nameType, new PdfName("Page"));
161: map.put(new PdfName("Parent"), new PdfReference(rootId, 0));
162: PdfDictionary page = new PdfDictionary(map);
163: setObject(page, pageId);
164:
165: // Make the Catalog.
166: map = new HashMap(2);
167: map.put(nameType, new PdfName("Catalog"));
168: map.put(new PdfName("Pages"), new PdfReference(rootId, 0));
169: int catalogId = addObject(new PdfDictionary(map));
170:
171: // Create Info dictionary with default fields. ?
172:
173: // Update trailer dictionary.
174: map = new HashMap(getTrailerDictionary().getMap());
175: map.put(new PdfName("Root"), new PdfReference(catalogId, 0));
176: setTrailerDictionary(new PdfDictionary(map));
177:
178: }
179:
180: /**
181: Constructs a <code>PdfManager</code> representing an
182: existing PDF document.
183: @param pdfReader specifies the existing PDF document to
184: read.
185: @throws IOException
186: @throws PdfFormatException
187: */
188: public PdfManager(PdfReader pdfReader) throws IOException,
189: PdfFormatException {
190:
191: _startxref = pdfReader.readStartxref();
192: _xt = pdfReader.readXrefTable(_startxref);
193: _trailer = _xt.getTrailerDictionary();
194: _reader = pdfReader;
195: _modsize = _xt.size();
196: construct();
197: }
198:
199: /**
200: Adds a PDF object to the document and assigns a new object
201: number to it.
202: @param obj the PDF object to be added.
203: @return the new object number assigned to the PDF object.
204: The generation number of the object will be 0.
205: */
206: public int addObject(PdfObject obj) {
207: synchronized (this ) {
208:
209: int id = _modsize++;
210:
211: setObject(obj, id);
212:
213: return id;
214: }
215: }
216:
217: /**
218: Retrieves the PDF object associated with a specified object
219: number.
220: @param objectNumber specifies the object number of the PDF
221: object to be retrieved.
222: @return the retrieved PDF object.
223: @throws IOException
224: @throws PdfFormatException
225: */
226: public PdfObject getObject(int objectNumber) throws IOException,
227: PdfFormatException {
228: synchronized (this ) {
229:
230: // first check hashmap
231: PdfObject obj = (PdfObject) _modobj.get(new Integer(
232: objectNumber));
233: if (obj != null) {
234: return obj;
235: }
236:
237: // next try the xref
238: if (_reader == null) {
239: return null;
240: }
241: if (_xt.unwrapUsageArray()[objectNumber] != XrefTable.ENTRY_IN_USE) {
242: return null;
243: }
244: long start = _xt.getIndex(objectNumber);
245: long end = _xt.estimateObjectEnd(objectNumber);
246: return _reader.readObject(start, end, true, _xt);
247: }
248:
249: }
250:
251: /**
252: Retrieves the PDF object referred to by a specified
253: indirect reference object. If the specified object is not
254: an indirect reference but a direct object, that object
255: itself is returned. If the specified object is
256: <code>null</code>, then <code>null</code> is returned.
257: @param obj the indirect reference object (or direct object,
258: or <code>null</code>).
259: @return the retrieved object (or direct object).
260: @throws IOException
261: @throws PdfFormatException
262: */
263: public PdfObject getObjectIndirect(PdfObject obj)
264: throws IOException, PdfFormatException {
265: synchronized (this ) {
266:
267: if (obj == null) {
268: return null;
269: }
270:
271: if (obj instanceof PdfReference) {
272: // this should be changed from
273: // recursive to iterative, for best
274: // efficiency
275: return getObjectIndirect(getObject(((PdfReference) obj)
276: .getObjectNumber()));
277: } else {
278: return obj;
279: }
280:
281: }
282: }
283:
284: /**
285: Returns the document's trailer dictionary.
286: @return the trailer dictionary.
287: */
288: public PdfDictionary getTrailerDictionary() {
289: synchronized (this ) {
290: return _trailer;
291: }
292: }
293:
294: /**
295: Returns the current size of the cross-reference table,
296: taking into account any modifications that have been made
297: to the document via this PdfManager.
298: @return the size of the cross-reference table.
299: */
300: public int getXrefTableSize() {
301: synchronized (this ) {
302: return _modsize;
303: }
304: }
305:
306: /**
307: Performs initialization common to multiple constructors of
308: this class. This method is only intended to be called from
309: the constructors.
310: */
311: protected void construct() {
312: _modobj = new HashMap();
313: }
314:
315: /**
316: Adds a PDF object to the document and assigns the specified
317: object number to it. If an object in the document is
318: already assigned to the specified object number, that
319: object is replaced with the new one.
320: @param obj the PDF object to be added.
321: @param objectNumber the object number to assign to the PDF
322: object.
323: */
324: public void setObject(PdfObject obj, int objectNumber) {
325: synchronized (this ) {
326: _modobj.put(new Integer(objectNumber), obj);
327: if (objectNumber >= _modsize) {
328: _modsize = objectNumber + 1;
329: }
330: }
331: }
332:
333: /**
334: Assigns the document a new trailer dictionary, replacing
335: the existing one (if any).
336: @param dictionary specifies the new trailer dictionary.
337: */
338: public void setTrailerDictionary(PdfDictionary dictionary) {
339: synchronized (this ) {
340: _trailer = dictionary;
341: }
342: }
343:
344: /**
345: Writes the document in PDF format, including all
346: modifications made through this <code>PdfManager</code>.
347: This method is equivalent to using {@link
348: #writeDocument(PdfWriter, boolean) writeDocument(...,
349: true)}; i.e. it uses incremental update if applicable.
350: Note that this means the resultant document will be larger
351: than the original document; otherwise use {@link
352: #writeDocument(PdfWriter, boolean) writeDocument(...,
353: false)} to get a smaller resultant file, although it will
354: usually take longer to generate. The
355: <code>PdfWriter</code> should be newly created (i.e. it
356: should not have been previously used for anything); and
357: after this method has been called, the
358: <code>PdfWriter</code> should be closed and discarded.
359: @param pdfWriter specifies the PDF document to write.
360: @return the number of bytes written.
361: @throws IOException
362: @throws PdfFormatException
363: */
364: public long writeDocument(PdfWriter pdfWriter) throws IOException,
365: PdfFormatException {
366: return writeDocument(pdfWriter, true);
367: }
368:
369: /**
370: Writes the document in PDF format, including all
371: modifications made through this <code>PdfManager</code>.
372: This method will optionally use PDF's incremental update
373: format, which often takes significantly less processing
374: time but creates a larger resultant PDF file and may be
375: slower for a reader to open (if the file has been updated
376: many times in this way). Incremental update should not be
377: used when the original document is a Linearized PDF file if
378: changes have been made that would invalidate its
379: correctness, unless the resultant PDF file will not be used
380: in an application that depends on its correct
381: Linearization. If this <code>PdfManager</code> represents
382: a new document rather than modifying an existing one, then
383: the incremental update option is not applicable and is
384: disregarded. The <code>PdfWriter</code> should be newly
385: created (i.e. it should not have been previously used for
386: anything); and after this method has been called, the
387: <code>PdfWriter</code> should be closed and discarded.
388: @param pdfWriter specifies the PDF document to write.
389: @param useIncrementalUpdate specifies whether incremental
390: update format should be used. A value of <code>true</code>
391: enables incremental update.
392: @return the number of bytes written.
393: @throws IOException
394: @throws PdfFormatException
395: */
396: public long writeDocument(PdfWriter pdfWriter,
397: boolean useIncrementalUpdate) throws IOException,
398: PdfFormatException {
399: synchronized (this ) {
400:
401: int modsize = _modsize;
402: XrefTable xt = _xt;
403: PdfReader reader = _reader;
404: Map modobj = _modobj;
405:
406: boolean incUpdate;
407: if (reader == null) {
408: incUpdate = false;
409: } else {
410: incUpdate = useIncrementalUpdate;
411: }
412:
413: PdfWriter w = pdfWriter;
414: long pos = 0;
415:
416: if (incUpdate) {
417: pos += w.writeCopy(reader);
418: } else {
419: pos += w.writeHeader();
420: }
421:
422: long[] nindex = new long[modsize];
423: int[] ngeneration = new int[modsize];
424: byte[] nusage = new byte[modsize];
425: // (maybe we should ideally clone the
426: // trailer instead of modifying the original)
427: HashMap ntrailerMap = new HashMap(_trailer.getMap());
428:
429: long[] index = null;
430: int[] generation = null;
431: byte[] usage = null;
432:
433: if (reader != null) {
434: index = xt.unwrapIndexArray();
435: generation = xt.unwrapGenerationArray();
436: usage = xt.unwrapUsageArray();
437: }
438:
439: int id;
440: // iterate through modified objects, adding
441: // them to the new XrefTrailer
442: for (Iterator it = modobj.keySet().iterator(); it.hasNext();) {
443: Integer key = (Integer) it.next();
444: id = key.intValue();
445: nusage[id] = XrefTable.ENTRY_IN_USE;
446: nindex[id] = pos;
447: PdfObject obj = (PdfObject) modobj.get(key);
448:
449: // determine generation number
450: if (reader == null) {
451: ngeneration[id] = 0;
452: } else {
453: if ((id < xt.size())
454: && (usage[id] != XrefTable.ENTRY_UNDEFINED)) {
455: ngeneration[id] = generation[id];
456: } else {
457: ngeneration[id] = 0;
458: }
459: }
460:
461: if ((incUpdate) || (reader == null)) {
462: pos += w.writeObjectIndirect(obj, id,
463: ngeneration[id]);
464: }
465: }
466:
467: // always keep entry 0
468: ngeneration[0] = 65535;
469: nusage[0] = XrefTable.ENTRY_FREE;
470:
471: if (!incUpdate) {
472: for (id = 1; id < modsize; id++) {
473: PdfObject obj = null;
474: if ((reader != null)
475: && (nusage[id] == XrefTable.ENTRY_UNDEFINED)) {
476: if ((id < xt.size())
477: && (usage[id] != XrefTable.ENTRY_UNDEFINED)) {
478: if (usage[id] == XrefTable.ENTRY_IN_USE) {
479: long start = index[id];
480: long end = xt.estimateObjectEnd(id);
481: obj = reader.readObject(start, end,
482: true, xt);
483: }
484: ngeneration[id] = generation[id];
485: nusage[id] = usage[id];
486: }
487: } else {
488: obj = (PdfObject) modobj.get(new Integer(id));
489: }
490: if (nusage[id] == XrefTable.ENTRY_IN_USE) {
491: nindex[id] = pos;
492: pos += w.writeObjectIndirect(obj, id,
493: ngeneration[id]);
494: }
495:
496: }
497: }
498:
499: if (incUpdate) {
500: ntrailerMap.put(PDFNAME_PREV, new PdfLong(_startxref));
501: } else {
502: ntrailerMap.remove(PDFNAME_PREV);
503: }
504: ntrailerMap.put(PDFNAME_SIZE, new PdfInteger(modsize));
505:
506: XrefTable nxt = XrefTable.wrap(nindex, ngeneration, nusage,
507: new PdfDictionary(ntrailerMap));
508:
509: _last_startxref = pos;
510:
511: pos += w.writeXrefTable(nxt, pos);
512:
513: return pos;
514: }
515: }
516:
517: }
|