/*
Copyright 2006-2010 Stefano Chizzolini. http://www.pdfclown.org
Contributors:
* Stefano Chizzolini (original code developer, http://www.stefanochizzolini.it)
* Haakan Aakerberg (bugfix contributor):
- [FIX:0.0.4:5]
This file should be part of the source code distribution of "PDF Clown library"
(the Program): see the accompanying README files for more info.
This Program is free software; you can redistribute it and/or modify it under the terms
of the GNU Lesser General Public License as published by the Free Software Foundation;
either version 3 of the License, or (at your option) any later version.
This Program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY,
either expressed or implied; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the License for more details.
You should have received a copy of the GNU Lesser General Public License along with this
Program (see README files); if not, go to the GNU website (http://www.gnu.org/licenses/).
Redistribution and use, with or without modification, are permitted provided that such
redistributions retain the above copyright notice, license and disclaimer, along with
this list of conditions.
*/
package org.pdfclown.tokens;
import java.text.DecimalFormat;
import java.util.Map;
import org.pdfclown.bytes.IOutputStream;
import org.pdfclown.files.File;
import org.pdfclown.files.IndirectObjects;
import org.pdfclown.objects.PdfDictionary;
import org.pdfclown.objects.PdfIndirectObject;
import org.pdfclown.objects.PdfInteger;
import org.pdfclown.objects.PdfName;
import org.pdfclown.objects.PdfReference;
import org.pdfclown.util.NotImplementedException;
/**
PDF file writer implementing classic cross-reference table [PDF:1.6:3.4.3].
@author Stefano Chizzolini (http://www.stefanochizzolini.it)
@version 0.1.0
*/
final class PlainWriter
extends Writer
{
// <class>
// <static>
// <fields>
private static final byte[] TrailerChunk = Encoding.encode(Keyword.Trailer + Symbol.LineFeed);
private static final String XRefChunk = Keyword.XRef + Symbol.LineFeed;
private static final String XRefEOLChunk = "" + Symbol.CarriageReturn + Symbol.LineFeed;
private static final DecimalFormat XRefGenerationFormatter = new DecimalFormat("00000");
private static final DecimalFormat XRefOffsetFormatter = new DecimalFormat("0000000000");
// </fields>
// </static>
// <dynamic>
// <constructors>
PlainWriter(
File file,
IOutputStream stream
)
{super(file, stream);}
// </constructors>
// <interface>
// <protected>
@Override
protected void writeIncremental(
)
{
// 1. Original content (head, body and previous trailer).
Parser parser = file.getReader().getParser();
stream.write(parser.getStream());
// 2. Body update (modified indirect objects insertion).
int xrefSize = file.getIndirectObjects().size();
StringBuilder xrefBuilder = new StringBuilder(XRefChunk);
{
/*
NOTE: Incremental xref table comprises multiple sections
each one composed by multiple subsections; this update
adds a new section.
*/
StringBuilder xrefSubBuilder = new StringBuilder(); // Xref-table subsection builder.
int xrefSubCount = 0; // Xref-table subsection counter.
int prevKey = 0; // Previous-entry object number.
for(
Map.Entry<Integer,PdfIndirectObject> indirectObjectEntry
: file.getIndirectObjects().getModifiedObjects().entrySet()
)
{
// Is the object in the current subsection?
/*
NOTE: To belong to the current subsection, the object entry MUST be contiguous with the
previous (condition 1) or the iteration has to have been just started (condition 2).
*/
if(indirectObjectEntry.getKey() - prevKey == 1
|| prevKey == 0) // Current subsection continues.
{xrefSubCount++;}
else // Current subsection terminates.
{
// End current subsection!
appendXRefSubsection(
xrefBuilder,
prevKey - xrefSubCount + 1,
xrefSubCount,
xrefSubBuilder
);
// Begin next subsection!
xrefSubBuilder.setLength(0);
xrefSubCount = 1;
}
prevKey = indirectObjectEntry.getKey();
// Current entry insertion.
if(indirectObjectEntry.getValue().isInUse()) // In-use entry.
{
// Add in-use entry!
appendXRefEntry(
xrefSubBuilder,
indirectObjectEntry.getValue().getReference(),
stream.getLength()
);
// Add in-use entry content!
indirectObjectEntry.getValue().writeTo(stream);
}
else // Free entry.
{
// Add free entry!
/*
NOTE: We purposely neglect the linked list of free entries (see IndirectObjects.remove(int)),
so that this entry links directly back to object number 0, having a generation number of 65535
(not reusable) [PDF:1.6:3.4.3].
*/
appendXRefEntry(
xrefSubBuilder,
indirectObjectEntry.getValue().getReference(),
0
);
}
}
// End last subsection!
appendXRefSubsection(
xrefBuilder,
prevKey - xrefSubCount + 1,
xrefSubCount,
xrefSubBuilder
);
}
// 3. XRef-table last section.
long startxref = stream.getLength();
stream.write(xrefBuilder.toString());
// 4. Trailer.
writeTrailer(startxref, xrefSize, parser);
}
@Override
protected void writeLinearized(
)
{throw new NotImplementedException();}
@Override
protected void writeStandard(
)
{
// 1. Header [PDF:1.6:3.4.1].
writeHeader();
// 2. Body [PDF:1.6:3.4.2].
int xrefSize = file.getIndirectObjects().size();
StringBuilder xrefBuilder = new StringBuilder(XRefChunk);
{
/*
NOTE: A standard xref table comprises just one section composed by just one subsection.
NOTE: As xref-table free entries MUST be arrayed as a linked list,
it's needed to cache intermingled in-use entries in order to properly render
the object number of the next free entry inside the previous one.
*/
appendXRefSubsectionIndexer(xrefBuilder, 0, xrefSize);
StringBuilder xrefInUseBlockBuilder = new StringBuilder();
IndirectObjects indirectObjects = file.getIndirectObjects();
PdfReference freeReference = indirectObjects.get(0).getReference(); // Initialized to the first free entry.
for(
int index = 1;
index < xrefSize;
index++
)
{
// Current entry insertion.
PdfIndirectObject indirectObject = indirectObjects.get(index);
if(indirectObject.isInUse()) // In-use entry.
{
// Add in-use entry!
appendXRefEntry(
xrefInUseBlockBuilder,
indirectObject.getReference(),
stream.getLength()
);
// Add in-use entry content!
indirectObject.writeTo(stream);
}
else // Free entry.
{
// Add free entry!
appendXRefEntry(
xrefBuilder,
freeReference,
index
);
// End current block!
xrefBuilder.append(xrefInUseBlockBuilder);
// Initialize next block!
xrefInUseBlockBuilder.setLength(0);
freeReference = indirectObject.getReference();
}
}
// Add last free entry!
appendXRefEntry(
xrefBuilder,
freeReference,
0
);
// End last block!
xrefBuilder.append(xrefInUseBlockBuilder);
}
// 3. XRef table (unique section) [PDF:1.6:3.4.3].
long startxref = stream.getLength();
stream.write(xrefBuilder.toString());
// 4. Trailer [PDF:1.6:3.4.4].
writeTrailer(startxref, xrefSize, null);
}
// </protected>
// <private>
private StringBuilder appendXRefEntry(
StringBuilder xrefBuilder,
PdfReference reference,
long offset
)
{
String usage;
switch(reference.getIndirectObject().getXrefEntry().getUsage())
{
case Free:
usage = Keyword.FreeXrefEntry;
break;
case InUse:
usage = Keyword.InUseXrefEntry;
break;
default: // Should NEVER happen.
throw new UnsupportedOperationException();
}
return xrefBuilder.append(XRefOffsetFormatter.format(offset)).append(Symbol.Space)
.append(XRefGenerationFormatter.format(reference.getGenerationNumber())).append(Symbol.Space)
.append(usage).append(XRefEOLChunk);
}
/**
Appends the cross-reference subsection to the specified builder.
@param xrefBuilder Target builder.
@param firstObjectNumber Object number of the first object in the subsection.
@param entryCount Number of entries in the subsection.
@param xrefSubBuilder Cross-reference subsection entries.
*/
private StringBuilder appendXRefSubsection(
StringBuilder xrefBuilder,
int firstObjectNumber,
int entryCount,
StringBuilder xrefSubBuilder
)
{return appendXRefSubsectionIndexer(xrefBuilder, firstObjectNumber, entryCount).append(xrefSubBuilder);}
/**
Appends the cross-reference subsection indexer to the specified builder.
@param xrefBuilder Target builder.
@param firstObjectNumber Object number of the first object in the subsection.
@param entryCount Number of entries in the subsection.
*/
private StringBuilder appendXRefSubsectionIndexer(
StringBuilder xrefBuilder,
int firstObjectNumber,
int entryCount
)
{return xrefBuilder.append(firstObjectNumber).append(Symbol.Space).append(entryCount).append(Symbol.LineFeed);}
/**
Serializes the file trailer [PDF:1.6:3.4.4].
@param startxref Byte offset from the beginning of the file to the beginning
of the last cross-reference section.
@param xrefSize Total number of entries in the file's cross-reference table,
as defined by the combination of the original section and all update sections.
@param parser File parser.
*/
private void writeTrailer(
long startxref,
int xrefSize,
Parser parser
)
{
try
{
// 1. Header.
stream.write(TrailerChunk);
// 2. Body.
// Update its entries:
PdfDictionary trailer = file.getTrailer();
// * Size
trailer.put(PdfName.Size,new PdfInteger(xrefSize));
// * Prev
if(parser == null)
{trailer.remove(PdfName.Prev);} // [FIX:0.0.4:5] It (wrongly) kept the 'Prev' entry of multiple-section xref tables.
else
{trailer.put(PdfName.Prev,new PdfInteger((int)parser.retrieveXRefOffset()));}
// Serialize its contents!
trailer.writeTo(stream); stream.write(Chunk.LineFeed);
// 3. Tail.
writeTail(startxref);
}
catch(Exception e)
{throw new RuntimeException(e);}
}
// </private>
// </interface>
// </dynamic>
// </class>
}