org.verapdf.cos.visitor.Writer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of parser Show documentation
Show all versions of parser Show documentation
veraPDF tools for parsing, modifying and creating PDF documents.
/**
* This file is part of veraPDF Parser, a module of the veraPDF project.
* Copyright (c) 2015, veraPDF Consortium
* All rights reserved.
*
* veraPDF Parser is free software: you can redistribute it and/or modify
* it under the terms of either:
*
* The GNU General public license GPLv3+.
* You should have received a copy of the GNU General Public License
* along with veraPDF Parser as the LICENSE.GPL file in the root of the source
* tree. If not, see http://www.gnu.org/licenses/ or
* https://www.gnu.org/licenses/gpl-3.0.en.html.
*
* The Mozilla Public License MPLv2+.
* You should have received a copy of the Mozilla Public License along with
* veraPDF Parser as the LICENSE.MPL file in the root of the source tree.
* If a copy of the MPL was not distributed with this file, you can obtain one at
* http://mozilla.org/MPL/2.0/.
*/
package org.verapdf.cos.visitor;
import org.verapdf.as.ASAtom;
import org.verapdf.as.ASCharsets;
import org.verapdf.as.exceptions.StringExceptions;
import org.verapdf.as.filters.io.ASBufferedInFilter;
import org.verapdf.as.io.ASInputStream;
import org.verapdf.cos.*;
import org.verapdf.cos.xref.COSXRefEntry;
import org.verapdf.cos.xref.COSXRefInfo;
import org.verapdf.cos.xref.COSXRefRange;
import org.verapdf.cos.xref.COSXRefSection;
import org.verapdf.io.InternalOutputStream;
import org.verapdf.io.SeekableInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Timur Kamalov
*/
public class Writer implements IVisitor {
private static final Logger LOGGER = Logger.getLogger(Writer.class.getCanonicalName());
protected InternalOutputStream os;
private long incrementalOffset;
protected COSXRefInfo info;
protected COSDocument document;
protected List toWrite;
protected List written;
private final NumberFormat formatXrefOffset = new DecimalFormat("0000000000");
private final NumberFormat formatXrefGeneration = new DecimalFormat("00000");
public static final String EOL = "\r\n";
public Writer(final COSDocument document, final String filename,
long incrementalOffset) throws IOException {
this(document, filename, true, incrementalOffset);
}
public Writer(final COSDocument document, final String filename,
final boolean append, long incrementalOffset) throws IOException {
this.document = document;
this.os = new InternalOutputStream(filename);
this.info = new COSXRefInfo();
this.toWrite = new ArrayList<>();
this.written = new ArrayList<>();
this.incrementalOffset = incrementalOffset;
if (append) {
this.os.seekEnd();
}
}
public void writeIncrementalUpdate(List changedObjects,
List addedObjects) {
List objectsToWrite = new ArrayList<>();
for (COSObject obj : changedObjects) {
COSKey key = obj.getObjectKey();
if (key != null) {
objectsToWrite.add(obj.getObjectKey());
}
}
changedObjects.clear();
objectsToWrite.addAll(prepareAddedObjects(addedObjects));
this.addToWrite(objectsToWrite);
this.writeBody();
COSTrailer trailer = document.getTrailer();
// document.getLastTrailerOffset() + 1 point EXACTLY at first byte of xref
this.setTrailer(trailer, document.getLastTrailerOffset() + 1);
this.writeXRefInfo();
this.clear();
}
private List prepareAddedObjects(List addedObjects) {
int cosKeyNumber = this.document.getLastKeyNumber() + 1;
List res = new ArrayList<>();
for (COSObject obj : addedObjects) {
if (!obj.isIndirect()) {
COSObject indirect = COSIndirect.construct(obj, this.document);
res.add(indirect.getObjectKey());
} else {
res.add(obj.getObjectKey());
}
}
addedObjects.clear();
return res;
}
public void visitFromBoolean(COSBoolean obj) {
try {
this.write(String.valueOf(obj.get()));
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromInteger(COSInteger obj) {
try {
this.write(obj.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromReal(COSReal obj) {
try {
this.write(obj.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromString(COSString obj) {
try {
this.write(obj.getPrintableString());
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromName(COSName obj) {
try {
this.write(obj.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromArray(COSArray obj) {
try {
this.write("[");
for (int i = 0; i < obj.size(); i++) {
this.write(obj.at(i));
this.write(" ");
}
this.write("]");
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromDictionary(COSDictionary obj) {
try {
this.write("<<");
for (Map.Entry entry : obj.getEntrySet()) {
this.write(entry.getKey());
this.write(" ");
this.write(entry.getValue());
this.write(" ");
}
this.write(">>");
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromStream(COSStream obj) {
long length;
ASInputStream in = obj.getData();
if (obj.getFilterFlags() == COSStream.FilterFlags.DECODE ||
obj.getFilterFlags() == COSStream.FilterFlags.DECRYPT_AND_DECODE) {
//TODO : Decode
}
try {
obj.setIntegerKey(ASAtom.LENGTH, getASInputStreamLength(in));
} catch (IOException e) {
LOGGER.log(Level.FINE, "Can't calculate length of ASInputStream", e);
}
visitFromDictionary(obj);
try {
this.write(EOL);
this.write("stream");
this.write(EOL);
length = getOffset();
byte[] buffer = new byte[1024];
long count;
in.reset();
while(true) {
count = in.read(buffer, 1024);
if (count == -1) {
break;
}
this.os.write(buffer, (int) count);
}
length = getOffset() - length;
obj.setLength(length);
this.write(EOL);
this.write("endstream");
} catch (IOException e) {
throw new RuntimeException(StringExceptions.WRITE_ERROR);
}
}
private static long getASInputStreamLength(ASInputStream stream) throws IOException {
if (stream instanceof SeekableInputStream) {
// That is the case of unfiltered stream
return ((SeekableInputStream) stream).getStreamLength();
} else {
// That is the case of fitered stream. Optimization can be reached
// if decoded data is stored in memory and not thrown away.
stream.reset();
byte[] buf = new byte[ASBufferedInFilter.BF_BUFFER_SIZE];
long res = 0;
int read = stream.read(buf);
while (read != -1) {
res += read;
read = stream.read(buf);
}
return res;
}
}
public void visitFromNull(COSNull obj) {
try {
this.write("null");
} catch (IOException e) {
e.printStackTrace();
}
}
public void visitFromIndirect(COSIndirect obj) {
try {
COSKey key = obj.getKey();
if (key.equals(new COSKey())) {
COSObject direct = obj.getDirect();
key = this.document.setObject(direct);
obj.setKey(key, this.document);
addToWrite(key);
}
this.write(key);
this.write(" R");
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
public void writeHeader(final String header) {
try {
this.write(header);
this.write(EOL);
String comment = new String(new char[] { '%', 0xE2, 0xE3, 0xCF, 0xD3 });
this.write(comment);
this.write(EOL);
} catch (IOException e) {
e.printStackTrace();
}
}
public void addToWrite(final COSKey key) {
this.toWrite.add(key);
}
public void addToWrite(final List keys) {
this.toWrite.addAll(keys);
}
public void writeBody() {
try {
while (!this.toWrite.isEmpty()) {
final COSKey key = this.toWrite.get(0);
this.toWrite.remove(0);
this.written.add(key);
write(key, this.document.getObject(key));
}
} catch (IOException e) {
throw new RuntimeException(StringExceptions.WRITE_ERROR);
}
}
public void freeObjects(final Map keys) {
for (Map.Entry entry : keys.entrySet()) {
addXRef(entry.getKey(), entry.getValue(), 'f');
}
}
public void setTrailer(final COSTrailer trailer) {
setTrailer(trailer, 0);
}
public void setTrailer(final COSTrailer trailer, final long prev) {
COSObject element = new COSObject();
COSCopier copier = new COSCopier(element);
trailer.getObject().accept(copier);
this.info.getTrailer().setObject(element);
this.info.getTrailer().setPrev(prev);
if (prev == 0) {
this.info.getTrailer().removeKey(ASAtom.ID);
}
}
public void writeXRefInfo() {
try {
this.info.setStartXRef(getOffset() + incrementalOffset);
this.info.getTrailer().setSize(this.info.getXRefSection().next());
this.write("xref"); this.write(EOL); this.write(info.getXRefSection());
this.write("trailer"); this.write(EOL); this.write(this.info.getTrailer().getObject()); this.write(EOL);
this.write("startxref"); this.write(EOL); this.write(this.info.getStartXRef()); this.write(EOL);
this.write("%%EOF"); this.write(EOL);
} catch (IOException e) {
e.printStackTrace();
}
}
public COSXRefInfo getXRefInfo() {
return this.info;
}
public void clear() {
try {
this.info = new COSXRefInfo();
this.toWrite.clear();
this.written.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() {
try {
this.os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
protected long getOffset() {
try {
return this.os.getOffset();
} catch (IOException e) {
e.printStackTrace();
return 0;
}
}
protected void write(final COSKey key, final COSObject object) throws IOException {
addXRef(key);
this.write(key);
this.write(" obj");
this.write(EOL);
this.write(object);
this.write(EOL);
this.write("endobj");
this.write(EOL);
}
protected void generateID() {
// TODO : finish this method
Long idTime = System.currentTimeMillis();
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
md5.update(Long.toString(idTime).getBytes("ISO-8859-1"));
COSObject idString = COSString.construct(md5.digest(), true);
//TODO : convert to COSArray
this.info.getTrailer().setID(idString);
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
}
}
protected COSKey getKeyToWrite(final COSKey key) {
return key;
}
protected void addXRef(final COSKey key, final long offset, final char free) {
this.info.getXRefSection().add(getKeyToWrite(key), offset, free);
}
public void addXRef(final COSKey key) {
addXRef(key, getOffset() + incrementalOffset, 'n');
}
protected void write(final boolean value) throws IOException {
this.os.write(value);
}
protected void write(final int value) throws IOException {
this.os.write(String.valueOf(value));
}
protected void write(final long value) throws IOException {
this.os.write(String.valueOf(value));
}
protected void write(final char value) throws IOException {
this.os.write(value);
}
protected void write(final String value) throws IOException {
this.os.write(value);
}
protected void write(final ASAtom value) throws IOException {
this.os.write(value.toString());
}
protected void write(final COSKey value) throws IOException {
final COSKey newKey = getKeyToWrite(value);
this.write(newKey.getNumber()); this.write(" "); this.write(newKey.getGeneration());
}
protected void write(final COSObject value) {
value.accept(this);
}
protected void write(final COSXRefRange value) throws IOException {
os.write(String.valueOf(value.start)).write(" ").write(String.valueOf(value.count)).write(EOL);
}
protected void write(final COSXRefEntry value) throws IOException {
String offset = formatXrefOffset.format(value.offset);
String generation = formatXrefGeneration.format(value.generation);
os.write(offset.getBytes(ASCharsets.ISO_8859_1));
os.write(" ");
os.write(generation.getBytes(ASCharsets.ISO_8859_1));
os.write(" ");
os.write(String.valueOf(value.free).getBytes(ASCharsets.US_ASCII));
os.write(EOL);
}
protected void write(final COSXRefSection value) throws IOException {
List range = value.getRange();
for (COSXRefRange entry : range) {
write(entry);
for (int j = entry.start; j < entry.next(); j++) {
this.write(value.getEntry(j));
}
}
}
}