/**
*
* Copyright
* 2009-2015 Jayway Products AB
* 2016-2017 Föreningen Sambruk
*
* Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt
*
* 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.
*/
package se.streamsource.streamflow.web.application.pdf;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.COSStreamArray;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectForm;
import org.apache.pdfbox.util.MapUtil;
import java.io.*;
import java.util.*;
/**
* Underlay one document with a template, mainly used to apply header/footer to an existing document
* How it (should) work:<br>
* If the document has 10 pages, and the layout 2 the following is the result:<br>
* <pre>
* Document: 1234567890
* Layout : 1212121212
* </pre>
* <br>
* <p/>
* This is based on the org.apache.pdfbox.Overlay class but has been modified to work properly by
*
* @author Henrik Reinhold (henrik.reinhold@jayway.com)
* <p/>
* Original authors:
* @author Mario Ivankovits (mario@ops.co.at)
* @author <a href="ben@benlitchfield.com">Ben Litchfield</a>
*/
public class Underlay
{
private List layoutPages = new ArrayList( 10 );
private PDDocument pdfUnderlay;
private PDDocument pdfDocument;
private int pageCount = 0;
private COSStream saveGraphicsStateStream;
private COSStream restoreGraphicsStateStream;
private static PDDocument getDocument( String filename ) throws IOException
{
FileInputStream input = null;
PDFParser parser = null;
PDDocument result = null;
try
{
input = new FileInputStream( filename );
parser = new PDFParser( input );
parser.parse();
result = parser.getPDDocument();
}
finally
{
if (input != null)
{
input.close();
}
}
return result;
}
/**
* Private class.
*/
private static class LayoutPage
{
private final COSBase contents;
private final COSDictionary res;
private final Map objectNameMap;
/**
* Constructor.
*
* @param contentsValue The contents.
* @param resValue The resource dictionary
* @param objectNameMapValue The map
*/
public LayoutPage( COSBase contentsValue, COSDictionary resValue, Map objectNameMapValue )
{
contents = contentsValue;
res = resValue;
objectNameMap = objectNameMapValue;
}
}
/**
* This will underlay two documents onto each other. The underlay document is
* repeatedly underlayed under the destination document for every page in the
* destination.
*/
public PDDocument underlay(PDDocument destination, InputStream templateStream) throws IOException {
pdfDocument = destination;
try {
PDFParser parser = new PDFParser(templateStream);
parser.parse();
pdfUnderlay = parser.getPDDocument();
overlayWithDarkenBlendMode(pdfDocument, pdfUnderlay);
} finally {
if (templateStream != null) {
templateStream.close();
}
}
return pdfDocument;
}
private void overlayWithDarkenBlendMode(PDDocument document, PDDocument overlay) throws IOException {
PDXObjectForm xobject = importAsXObject(document, (PDPage) overlay.getDocumentCatalog().getAllPages().get(0));
PDExtendedGraphicsState darken = new PDExtendedGraphicsState();
darken.getCOSDictionary().setName("BM", "Darken");
List<PDPage> pages = document.getDocumentCatalog().getAllPages();
for (PDPage page : pages) {
Map<String, PDExtendedGraphicsState> states = page.getResources().getGraphicsStates();
if (states == null)
states = new HashMap<>();
String darkenKey = MapUtil.getNextUniqueKey(states, "Dkn");
states.put(darkenKey, darken);
page.getResources().setGraphicsStates(states);
PDPageContentStream stream = new PDPageContentStream(document, page, true, false, true);
stream.appendRawCommands(String.format("/%s gs ", darkenKey));
stream.drawXObject(xobject, 0, 0, 1, 1);
stream.close();
}
}
private PDXObjectForm importAsXObject(PDDocument target, PDPage page) throws IOException {
final PDStream xobjectStream = new PDStream(target, page.getContents().createInputStream(), false);
final PDXObjectForm xobject = new PDXObjectForm(xobjectStream);
xobject.setResources(page.findResources());
xobject.setBBox(page.findCropBox());
COSDictionary group = new COSDictionary();
group.setName("S", "Transparency");
group.setBoolean(COSName.getPDFName("K"), true);
xobject.getCOSStream().setItem(COSName.getPDFName("Group"), group);
return xobject;
}
private COSStream makeUniqObjectNames( Map objectNameMap, COSStream stream ) throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream( 10240 );
byte[] buf = new byte[10240];
int read;
InputStream is = stream.getUnfilteredStream();
while ((read = is.read( buf )) > -1)
{
baos.write( buf, 0, read );
}
buf = baos.toByteArray();
baos = new ByteArrayOutputStream( buf.length + 100 );
StringBuffer sbObjectName = new StringBuffer( 10 );
boolean bInObjectIdent = false;
boolean bInText = false;
boolean bInEscape = false;
for (int i = 0; i < buf.length; i++)
{
byte b = buf[i];
if (!bInEscape)
{
if (!bInText && b == '(')
{
bInText = true;
}
if (bInText && b == ')')
{
bInText = false;
}
if (b == '\\')
{
bInEscape = true;
}
if (!bInText && !bInEscape)
{
if (b == '/')
{
bInObjectIdent = true;
} else if (bInObjectIdent && Character.isWhitespace( (char) b ))
{
bInObjectIdent = false;
String objectName = sbObjectName.toString().substring( 1 );
String newObjectName = objectName + "overlay";
baos.write( '/' );
baos.write( newObjectName.getBytes() );
objectNameMap.put( objectName, COSName.getPDFName( newObjectName ) );
sbObjectName.delete( 0, sbObjectName.length() );
}
}
if (bInObjectIdent)
{
sbObjectName.append( (char) b );
continue;
}
} else
{
bInEscape = false;
}
baos.write( b );
}
COSDictionary streamDict = new COSDictionary();
streamDict.setInt( COSName.LENGTH, baos.size() );
COSStream output = new COSStream( streamDict, pdfDocument.getDocument().getScratchFile() );
output.setFilters( stream.getFilters() );
OutputStream os = output.createUnfilteredStream();
baos.writeTo( os );
os.close();
return output;
}
private void processPages( List pages ) throws IOException
{
Iterator pageIter = pages.iterator();
while (pageIter.hasNext())
{
PDPage page = (PDPage) pageIter.next();
COSDictionary pageDictionary = page.getCOSDictionary();
COSBase contents = pageDictionary.getDictionaryObject( COSName.CONTENTS );
if (contents instanceof COSStreamArray)
{
COSStreamArray cosStreamArray = (COSStreamArray) contents;
COSArray array = new COSArray();
for (int i = 0; i < cosStreamArray.getStreamCount(); i++)
{
array.add(cosStreamArray.get( i ));
}
mergePage( array, page );
pageDictionary.setItem( COSName.CONTENTS, array );
} else if (contents instanceof COSStream)
{
COSStream contentsStream = (COSStream) contents;
COSArray array = new COSArray();
array.add( contentsStream );
mergePage( array, page );
pageDictionary.setItem( COSName.CONTENTS, array );
} else if (contents instanceof COSArray)
{
COSArray contentsArray = (COSArray) contents;
mergePage( contentsArray, page );
} else
{
throw new IOException( "Contents are unknown type:" + contents.getClass().getName() );
}
pageCount++;
}
}
private void mergePage( COSArray array, PDPage page )
{
int layoutPageNum = pageCount % layoutPages.size();
LayoutPage layoutPage = (LayoutPage) layoutPages.get( layoutPageNum );
PDResources resources = page.findResources();
if (resources == null)
{
resources = new PDResources();
page.setResources( resources );
}
COSDictionary docResDict = resources.getCOSDictionary();
COSDictionary layoutResDict = layoutPage.res;
mergeArray( COSName.PROC_SET, docResDict, layoutResDict );
mergeDictionary( COSName.COLORSPACE, docResDict, layoutResDict, layoutPage.objectNameMap );
mergeDictionary( COSName.FONT, docResDict, layoutResDict, layoutPage.objectNameMap );
mergeDictionary( COSName.XOBJECT, docResDict, layoutResDict, layoutPage.objectNameMap );
mergeDictionary( COSName.EXT_G_STATE, docResDict, layoutResDict, layoutPage.objectNameMap );
array.add( 0, layoutPage.contents );
}
/**
* merges two dictionaries.
*
* @param dest
* @param source
*/
private void mergeDictionary( COSName name, COSDictionary dest, COSDictionary source, Map objectNameMap )
{
COSDictionary destDict = (COSDictionary) dest.getDictionaryObject( name );
COSDictionary sourceDict = (COSDictionary) source.getDictionaryObject( name );
if (destDict == null)
{
destDict = new COSDictionary();
dest.setItem( name, destDict );
}
if (sourceDict != null)
{
for (Map.Entry<COSName, COSBase> entry : sourceDict.entrySet())
{
COSName mappedKey = (COSName) objectNameMap.get( entry.getKey().getName() );
if (mappedKey != null)
{
destDict.setItem( mappedKey, entry.getValue() );
}
}
}
}
/**
* merges two arrays.
*
* @param dest
* @param source
*/
private void mergeArray( COSName name, COSDictionary dest, COSDictionary source )
{
COSArray destDict = (COSArray) dest.getDictionaryObject( name );
COSArray sourceDict = (COSArray) source.getDictionaryObject( name );
if (destDict == null)
{
destDict = new COSArray();
dest.setItem( name, destDict );
}
for (int sourceDictIdx = 0; sourceDict != null && sourceDictIdx < sourceDict.size(); sourceDictIdx++)
{
COSBase key = sourceDict.get( sourceDictIdx );
if (key instanceof COSName)
{
COSName keyname = (COSName) key;
boolean bFound = false;
for (int destDictIdx = 0; destDictIdx < destDict.size(); destDictIdx++)
{
COSBase destkey = destDict.get( destDictIdx );
if (destkey instanceof COSName)
{
COSName destkeyname = (COSName) destkey;
if (destkeyname.equals( keyname ))
{
bFound = true;
break;
}
}
}
if (!bFound)
{
destDict.add( keyname );
}
}
}
}
}