Files
OCRmyPDF/jhove/classes/edu/harvard/hul/ois/jhove/module/PdfModule.java
T
2013-04-26 14:46:47 +02:00

4116 lines
160 KiB
Java

/**********************************************************************
* Jhove - JSTOR/Harvard Object Validation Environment
* Copyright 2003-2007 by JSTOR and the President and Fellows of Harvard College
*
* 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 2 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; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
**********************************************************************/
package edu.harvard.hul.ois.jhove.module;
import edu.harvard.hul.ois.jhove.*;
import edu.harvard.hul.ois.jhove.module.pdf.*;
import java.io.*;
import java.util.*;
//Importing org.xml.sax.* would make Parser ambiguous
import org.xml.sax.XMLReader;
import org.xml.sax.SAXException;
//import org.xml.sax.InputSource;
import javax.xml.parsers.SAXParserFactory;
/**
* Module for identification and validation of PDF files.
*/
public class PdfModule
extends ModuleBase
{
/******************************************************************
* PRIVATE CLASS FIELDS.
******************************************************************/
private static final String NAME = "PDF-hul";
private static final String RELEASE = "1.7";
private static final int [] DATE = {2012, 8, 12};
private static final String [] FORMAT = {
"PDF", "Portable Document Format"
};
private static final String COVERAGE =
"PDF 1.0-1.6; PDF/X-1 (ISO 15930-1:2001), X-1a (ISO 15930-4:2003), " +
"X-2 (ISO 15930-5:2003), and X-3 (ISO 15930-6:2003); Tagged PDF; " +
"Linearized PDF; PDF/A (ISO/CD 19005-1)";
private static final String [] MIMETYPE = {"application/pdf"};
private static final String WELLFORMED = "A PDF file is " +
"well-formed if it meets the criteria defined in Chapter " +
"3 of the PDF Reference 1.6 (5th edition, 2004)";
private static final String VALIDITY = null;
private static final String REPINFO = null;
private static final String NOTE = "This module does *not* validate " +
"data within content streams (including operators) or encrypted data";
private static final String RIGHTS = "Copyright 2003-2007 by JSTOR and " +
"the President and Fellows of Harvard College. " +
"Released under the GNU Lesser General Public License.";
private static final String ENCRYPTED = "<May be encrypted>";
/** Font type selectors. */
public final static int F_TYPE0 = 1,
F_TYPE1 = 2,
F_TT = 3,
F_TYPE3 = 4,
F_MM1 = 5,
F_CID0 = 6,
F_CID2 = 7;
/******************************************************************
* PRIVATE INSTANCE FIELDS.
******************************************************************/
/* The maximum number of fonts that will be reported before we just
* give up and report a stub to avoid running out of memory. */
protected int DEFAULT_MAX_FONTS = 1000;
/* Constants for trailer parsing */
private static final int EOFSCANSIZE = 1024;
private static final int XREFSCANSIZE = 128; // generous...
protected RandomAccessFile _raf;
protected Parser _parser;
protected String _version;
protected Property _metadata;
protected Property _xmpProp;
protected long _eof;
protected long _startxref;
protected long _prevxref;
protected int _numFreeObjects;
protected Property _idProperty;
protected int _objCount; // Count of objects in the cross-reference table
protected int _numObjects; // Value of the "Size" entry in the trailer dictionary
protected int _numTrailers; // Count of the number of trailers (updates)
protected Map _objects; // Map of the objects in the file
protected long[] _xref; // array of object offsets from xref table
protected int[] [] _xref2; // array of int[2], giving object stream and offset when _xref[i] < 0
protected boolean _xrefIsStream; // true if xref streams rather than tables are used
protected boolean _encrypted; // equivalent to _encryptDictRef != null
protected List<Property> _docCatalogList; // Info extracted from doc cat dict
protected List<Property> _encryptList; // Info from encryption dict
protected List<Property> _docInfoList; // info from doc info dict
protected List<Property> _extStreamsList; // List of external streams
protected List<Property> _imagesList; // List of image streams
protected List<Property> _filtersList; // List of filters
protected List<Property> _pagesList; // List of PageObjects
protected Map<Integer, PdfObject> _type0FontsMap; // Map of type 0 font dictionaries
protected Map<Integer, PdfObject> _type1FontsMap; // Map of type 1 font dictionaries
protected Map<Integer, PdfObject> _mmFontsMap; // Map of multi master font dictionaries
protected Map<Integer, PdfObject> _type3FontsMap; // Map of type 3 font dictionaries
protected Map<Integer, PdfObject> _trueTypeFontsMap; // Map of TrueType font dictionaries
protected Map<Integer, PdfObject> _cid0FontsMap; // Map of CIDFont/Type1 dictionaries
protected Map<Integer, PdfObject> _cid2FontsMap; // Map of CIDFont/TrueType dictionaries
protected Map<Integer, Integer> _pageSeqMap; // Map associating page object dicts with sequence numbers
protected PdfIndirectObj _docCatDictRef;
protected PdfIndirectObj _encryptDictRef;
protected PdfIndirectObj _docInfoDictRef;
protected PdfIndirectObj _pagesDictRef;
protected PdfDictionary _docCatDict;
protected PdfDictionary _docInfoDict;
protected PageTreeNode _docTreeRoot;
protected PdfDictionary _pageLabelDict;
protected PageLabelNode _pageLabelRoot;
protected NameTreeNode _embeddedFiles;
protected NameTreeNode _destNames;
protected PdfDictionary _encryptDict;
protected PdfDictionary _trailerDict;
protected PdfDictionary _viewPrefDict;
protected PdfDictionary _outlineDict;
protected PdfDictionary _destsDict;
protected boolean _showFonts;
protected boolean _showOutlines;
protected boolean _showAnnotations;
protected boolean _showPages;
protected boolean _actionsExist;
protected boolean _pdfACompliant; // flag checking PDF/A compliance
protected boolean _recursionWarned; // Check if warning has been issued on recursive outlines.
/* These three variables track whether a message has been posted
notifying the user of omitted information. */
protected boolean _skippedFontsReported;
protected boolean _skippedOutlinesReported;
protected boolean _skippedAnnotationsReported;
protected boolean _skippedPagesReported;
/** List of profile checkers */
protected List<PdfProfile> _profile;
/** Cached object stream. */
protected ObjectStream _cachedObjectStream;
/** Object number of cached object stream. */
protected int _cachedStreamIndex;
/** Map of visited nodes when walking through an outline. */
protected Set<Integer> _visitedOutlineNodes;
/** maximum number of fonts to report full information on. */
protected int maxFonts;
/** Number of fonts reported so far. */
protected int _nFonts;
/* These are the message texts to post in case of omitted
information. */
private final static String fontsSkippedString =
"Fonts exist, but are not displayed; to display " +
"remove param value of f from the config file";
private final static String outlinesSkippedString =
"Outlines exist, but are not displayed; to display " +
"remove param value of o from the config file";
private final static String annotationsSkippedString =
"Annotations exist, but are not displayed; to display " +
"remove param value of a from the config file";
private final static String pagesSkippedString =
"Page information is not displayed; to display " +
"remove param value of p from the config file";
/* Warning messages. */
protected final static String outlinesRecursiveString =
"Outlines contain recursive references.";
/* Name-to-value array pairs for NISO metadata */
private final static String[] compressionStrings =
{ "LZWDecode", /* "FlateDecode", */ "RunLengthDecode", "DCTDecode", "CCITTFaxDecode"};
private final static int[] compressionValues =
{ 5, /* 8, */ 32773, 6, 2};
/* The value of 2 (CCITTFaxDecode) is a placeholder; additional
* checking of the K parameter is needed to determine the real
* value if that's returned. */
private final static String [] colorSpaceStrings =
{ "Lab", "DeviceRGB", "DeviceCMYK", "DeviceGray", "Indexed" };
private final static int[] colorSpaceValues =
{ 8, 2, 5, 1, 3 };
/******************************************************************
* CLASS CONSTRUCTOR.
******************************************************************/
/**
* Creates an instance of the module and initializes identifying
* information.
*/
public PdfModule ()
{
super (NAME, RELEASE, DATE, FORMAT, COVERAGE, MIMETYPE, WELLFORMED,
VALIDITY, REPINFO, NOTE, RIGHTS, true);
Agent agent = new Agent ("Harvard University Library",
AgentType.EDUCATIONAL);
agent.setAddress ("Office for Information Systems, " +
"90 Mt. Auburn St., " +
"Cambridge, MA 02138");
agent.setTelephone ("+1 (617) 495-3724");
agent.setEmail("jhove-support@hulmail.harvard.edu");
_vendor = agent;
Document doc = new Document ("PDF Reference: Adobe Portable " +
"Document Format, Version 1.4",
DocumentType.BOOK);
agent = new Agent ("Adobe Systems, Inc.", AgentType.COMMERCIAL);
agent.setAddress ("345 Park Avenue, San Jose, California 95110-2704");
agent.setTelephone ("+1 (408) 536-6000");
agent.setFax ("+1 (408) 537-6000");
agent.setWeb ("http://www.adobe.com/");
doc.setPublisher (agent);
doc.setDate ("2001-12");
doc.setEdition ("3rd edition");
doc.setIdentifier (new Identifier ("0-201-75839-3",
IdentifierType.ISBN));
doc.setIdentifier (new Identifier ("http://partners.adobe.com/asn/" +
"acrobat/docs/File_Format_" +
"Specifications/PDFReference.pdf",
IdentifierType.URL));
_specification.add (doc);
doc = new Document ("PDF Reference: Adobe Portable " +
"Document Format, Version 1.5",
DocumentType.BOOK);
doc.setPublisher (agent);
doc.setDate ("2003");
doc.setEdition ("4th edition");
doc.setIdentifier (new Identifier (
"http://partners.adobe.com/public/developer/en/pdf/PDFReference15_v6.pdf",
IdentifierType.URL));
_specification.add (doc);
doc = new Document ("PDF Reference: Adobe Portable " +
"Document Format, Version 1.6",
DocumentType.BOOK);
doc.setPublisher (agent);
doc.setDate ("2004-11");
doc.setEdition ("5th edition");
doc.setIdentifier (new Identifier (
"http://partners.adobe.com/public/developer/en/pdf/PDFReference16.pdf",
IdentifierType.URL));
_specification.add (doc);
doc = new Document ("Graphic technology -- Prepress " +
"digital data exchange -- Use of PDF -- " +
"Part 1: Complete exchange using CMYK data " +
"(PDF/X-1 and PDF/X-1a)",
DocumentType.STANDARD);
Agent isoAgent = new Agent ("ISO", AgentType.STANDARD);
isoAgent.setAddress ("1, rue de Varembe, Casa postale 56, " +
"CH-1211, Geneva 20, Switzerland");
isoAgent.setTelephone ("+41 22 749 01 11");
isoAgent.setFax ("+41 22 733 34 30");
isoAgent.setEmail ("iso@iso.ch");
isoAgent.setWeb ("http://www.iso.org");
doc.setPublisher (isoAgent);
doc.setDate ("2001-12-06");
doc.setIdentifier (new Identifier ("ISO 15930-1:2001",
IdentifierType.ISO));
_specification.add (doc);
doc = new Document ("Graphic technology -- Prepress " +
"digital data exchange -- Use of PDF -- " +
"Part 4: Complete exchange using CMYK and " +
"spot colour printing data using " +
"PDF 1.4 (PDF/X-1a)",
DocumentType.STANDARD);
doc.setPublisher (isoAgent);
doc.setDate ("2003-08-04");
doc.setIdentifier (new Identifier ("ISO 15930-4:2003",
IdentifierType.ISO));
_specification.add (doc);
doc = new Document ("Graphic technology -- Prepress " +
"digital data exchange -- Use of PDF -- " +
"Part 5: Partial exchange of printing data " +
"using PDF 1.4 (PDF/X-2)",
DocumentType.STANDARD);
doc.setPublisher (isoAgent);
doc.setDate ("2003-08-05");
doc.setIdentifier (new Identifier ("ISO 15930-5:2003",
IdentifierType.ISO));
_specification.add (doc);
doc = new Document ("Graphic technology -- Prepress " +
"digital data exchange -- Use of PDF -- " +
"Part 6: Complete exchange suitable for " +
"colour-managed workflows using " +
"PDF 1.4 (PDF/X-3)",
DocumentType.STANDARD);
doc.setPublisher (isoAgent);
doc.setDate ("2003-08-06");
doc.setIdentifier (new Identifier ("ISO 15930-6:2003",
IdentifierType.ISO));
_specification.add (doc);
_signature.add (new ExternalSignature (".pdf",
SignatureType.EXTENSION,
SignatureUseType.OPTIONAL));
_signature.add (new InternalSignature ("%PDF-1.",
SignatureType.MAGIC,
SignatureUseType.MANDATORY,
0));
doc = new Document ("Document management -- Electronic " +
"document file format for long-term " +
"preservation -- Part 1: Use of PDF (PDF/A)",
DocumentType.RFC);
doc.setPublisher (isoAgent);
doc.setDate ("2003-11-30");
doc.setIdentifier (new Identifier ("ISO/CD 19005-1",
IdentifierType.ISO));
doc.setIdentifier (new Identifier
("http://www.aiim.org/documents/standards/ISO_19005-1_(E).doc",
IdentifierType.URL));
_specification.add (doc);
_profile = new ArrayList<PdfProfile> (6);
_profile.add (new LinearizedProfile (this));
TaggedProfile tpr = new TaggedProfile (this);
_profile.add (tpr);
AProfile apr = new AProfile (this);
_profile.add (apr);
// Link AProfile to TaggedProfile to save checking
// the former twice.
apr.setTaggedProfile (tpr);
AProfileLevelA apra = new AProfileLevelA (this);
_profile.add (apra);
// AProfileLevelA depends on AProfile
apra.setAProfile(apr);
X1Profile x1 = new X1Profile (this);
_profile.add (x1);
X1aProfile x1a = new X1aProfile (this);
_profile.add (x1a);
// Linking the X1 profile to the X1a profile saves checking the former twice.
x1a.setX1Profile (x1);
_profile.add (new X2Profile (this));
_profile.add (new X3Profile (this));
_showAnnotations = false;
_showFonts = false;
_showOutlines = false;
_showPages = false;
maxFonts = DEFAULT_MAX_FONTS;
}
/******************************************************************
* PUBLIC INSTANCE METHODS.
*
* Parsing methods.
******************************************************************/
/** Reset parameter settings.
* Returns to a default state without any parameters.
*/
public void resetParams ()
throws Exception
{
_showAnnotations = true;
_showFonts = true;
_showOutlines = true;
_showPages = true;
maxFonts = DEFAULT_MAX_FONTS;
}
/**
* Per-action initialization. May be called multiple times.
*
* @param param The module parameter; under command-line Jhove, the -p parameter.
* If the parameter contains the indicated characters, then the
* specified information is omitted; otherwise, it is included.
* (This is the reverse of the behavior prior to beta 3.)
* These characters may be provided as separate parameters,
* or all in a single parameter.
* <ul>
* <li>a: annotations</li>
* <li>f: fonts</li>
* <li>o: outlines</li>
* <li>p: pages</li>
* </ul><br>
* The parameter is case-independent. A null parameter is
* equivalent to the empty string.
*/
public void param (String param)
{
if (param != null) {
param = param.toLowerCase ();
if (param.indexOf ('a') >= 0) {
_showAnnotations = false;
}
if (param.indexOf ('f') >= 0) {
_showFonts = false;
}
if (param.indexOf ('o') >= 0) {
_showOutlines = false;
}
if (param.indexOf ('p') >= 0) {
_showPages = false;
}
if (param.indexOf ('n') >= 0) {
// Parse out the number after the n, and use that to set
// the maximum number of fonts reported. Default is DEFAULT_MAX_FONTS.
int n = param.indexOf ('n');
StringBuffer b = new StringBuffer ();
for (int i = n + 1; i < param.length(); i++) {
char ch = param.charAt(i);
if (Character.isDigit (ch)) {
b.append(ch);
}
else {
break;
}
}
try {
int mx = Integer.parseInt (b.toString ());
if (mx > 0) {
maxFonts = mx;
}
}
catch (Exception e) {}
}
}
}
/**
* Parse a file and stores descriptive information. A RandomAccessFile
* must be used to represent the object.
*
* @param raf A PDF file
* @param info A clean RepInfo object, which will be modified to hold
* the descriptive information
*/
public final void parse (RandomAccessFile raf, RepInfo info)
throws IOException
{
initParse ();
info.setFormat (_format[0]);
info.setMimeType (_mimeType[0]);
info.setModule (this);
_objects = new HashMap ();
_raf = raf;
Tokenizer tok = new FileTokenizer (_raf);
_parser = new Parser (tok);
_parser.setObjectMap (_objects);
List<Property> metadataList = new ArrayList<Property> (11);
/* We construct a big whopping property,
which contains up to 11 subproperties */
_metadata = new Property ("PDFMetadata",
PropertyType.PROPERTY,
PropertyArity.LIST,
metadataList);
if (_raf.length () > 10000000000L) { // that's 10^10
_pdfACompliant = false; // doesn't meet size limit in Appendix C of PDF spec
}
if (!parseHeader (info)) {
return;
}
if (!findLastTrailer (info)) {
return;
}
/* Walk through the linked trailer and cross reference
sections. */
_prevxref = -1;
boolean lastTrailer = true;
while (_startxref > 0) {
// After the first (last) trailer, parse only for next "Prev" link
if (!parseTrailer (info, !lastTrailer)) {
return;
}
if (!readXRefInfo (info)) {
return;
}
++_numTrailers;
if (_xrefIsStream) {
/* If we have an xref stream, readXRefInfo dealt with all
* the streams in a single call. */
break;
}
// Beware infinite loop on badly broken file
if (_startxref == _prevxref) {
info.setMessage (new ErrorMessage
("Cross reference tables are broken",
_parser.getOffset ()));
info.setWellFormed (false);
return;
}
_startxref = _prevxref;
lastTrailer = false;
}
if (!readDocCatalogDict (info)) {
return;
}
if (!readEncryptDict (info)) {
return;
}
if (!readDocInfoDict (info)) {
return;
}
if (!readDocumentTree (info)) {
return;
}
if (!readPageLabelTree (info)) {
return;
}
if (!readXMPData (info)) {
return;
}
findExternalStreams (info);
if (!findFilters (info)) {
return;
}
findImages (info);
findFonts (info);
/* Object is well-formed PDF. */
/* We may have already done the checksums while converting a
temporary file. */
Checksummer ckSummer = null;
if (_je != null && _je.getChecksumFlag () &&
info.getChecksum ().size () == 0) {
ckSummer = new Checksummer ();
calcRAChecksum (ckSummer, raf);
setChecksums (ckSummer, info);
}
info.setVersion (_version);
metadataList.add(new Property ("Objects",
PropertyType.INTEGER,
new Integer (_numObjects)));
metadataList.add (new Property ("FreeObjects",
PropertyType.INTEGER,
new Integer (_numFreeObjects)));
metadataList.add (new Property ("IncrementalUpdates",
PropertyType.INTEGER,
new Integer (_numTrailers)));
if (_docCatalogList != null) {
metadataList.add (new Property("DocumentCatalog",
PropertyType.PROPERTY,
PropertyArity.LIST,
_docCatalogList));
}
if (_encryptList != null) {
metadataList.add (new Property ("Encryption",
PropertyType.PROPERTY,
PropertyArity.LIST,
_encryptList));
}
if (_docInfoList != null) {
metadataList.add (new Property ("Info",
PropertyType.PROPERTY,
PropertyArity.LIST,
_docInfoList));
}
if (_idProperty != null) {
metadataList.add (_idProperty);
}
if (_extStreamsList != null && !_extStreamsList.isEmpty ()) {
metadataList.add (new Property ("ExternalStreams",
PropertyType.PROPERTY,
PropertyArity.LIST,
_extStreamsList));
}
if (_filtersList != null && !_filtersList.isEmpty ()) {
metadataList.add (new Property ("Filters",
PropertyType.PROPERTY,
PropertyArity.LIST,
_filtersList));
}
if (_imagesList != null && !_imagesList.isEmpty ()) {
metadataList.add (new Property ("Images",
PropertyType.PROPERTY,
PropertyArity.LIST,
_imagesList));
}
if (_showFonts || _verbosity == Module.MAXIMUM_VERBOSITY) {
try { addFontsProperty (metadataList); }
catch (NullPointerException e) {
info.setMessage(new ErrorMessage ("unexpected error in parsing font property", e.toString()));
}
}
if (_nFonts > maxFonts) {
info.setMessage(new InfoMessage ("Too many fonts to report; some fonts omitted.",
"Total fonts = " + _nFonts));
}
if (_xmpProp != null) {
metadataList.add (_xmpProp);
}
addPagesProperty (metadataList, info);
if (!doOutlineStuff (info)) {
return;
}
info.setProperty (_metadata);
/* Check for profile conformance. */
if (!_parser.getPDFACompliant ()) {
_pdfACompliant = false;
}
ListIterator<PdfProfile> pter = _profile.listIterator ();
if (info.getWellFormed() == RepInfo.TRUE) {
// Well-formedness is necessary to satisfy any profile.
while (pter.hasNext ()) {
PdfProfile prof = (PdfProfile) pter.next ();
if (prof.satisfiesProfile (_raf, _parser)) {
info.setProfile (prof.getText ());
}
}
}
}
/**
* Returns true if the module hasn't detected any violations
* of PDF/A compliance. This must return true, but is not
* sufficient by itself, to establish compliance. The
* <code>AProfile</code> profiler makes the final determination.
*/
public boolean mayBePDFACompliant ()
{
return _pdfACompliant;
}
/**
* Returns the document tree root.
*/
public PageTreeNode getDocumentTree ()
{
return _docTreeRoot;
}
/**
* Returns the document information dictionary.
*/
public PdfDictionary getDocInfo ()
{
return _docInfoDict;
}
/**
* Returns the encryption dictionary.
*/
public PdfDictionary getEncryptionDict ()
{
return _encryptDict;
}
/**
* Return true if Actions have been detected in the file.
*/
public boolean getActionsExist ()
{
return _actionsExist;
}
/**
* Initialize the module. This is called at the start
* of parse restore the module to its initial state.
*/
protected final void initParse ()
{
super.initParse ();
_xref = null;
_xref2 = null;
_version = "";
_objects = null;
_numFreeObjects = 0;
_objCount = 0;
_docInfoList = null;
_extStreamsList = null;
_docCatalogList = null;
_encryptList = null;
_imagesList = null;
_filtersList = null;
_pagesList = null;
_type0FontsMap = null;
_type1FontsMap = null;
_mmFontsMap = null;
_type3FontsMap = null;
_trueTypeFontsMap = null;
_cid0FontsMap = null;
_cid2FontsMap = null;
_docCatDictRef = null;
_encryptDictRef = null;
_docInfoDictRef = null;
_pagesDictRef = null;
_docCatDict = null;
_docInfoDict = null;
_docTreeRoot = null;
_pageLabelDict = null;
_encryptDict = null;
_trailerDict = null;
_viewPrefDict = null;
_outlineDict = null;
_destsDict = null;
_pageSeqMap = null;
_pageLabelRoot = null;
_embeddedFiles = null;
_destNames = null;
_skippedFontsReported = false;
_skippedOutlinesReported = false;
_skippedAnnotationsReported = false;
_skippedPagesReported = false;
_idProperty = null;
_actionsExist = false;
_numObjects = 0;
_numTrailers = -1;
_pdfACompliant = true; // assume compliance till disproven
_xmpProp = null;
_cachedStreamIndex = -1;
_nFonts = 0;
}
protected boolean parseHeader (RepInfo info) throws IOException
{
Token token = null;
String value = null;
final String nohdr = "No PDF header";
/* Parse file header. */
boolean foundSig = false;
for (;;) {
if (_parser.getOffset() > 1024) {
break;
}
try {
token = null;
token = _parser.getNext (1024L);
}
catch (IOException ee) {
break;
}
catch (Exception e) {} // fall through
if (token == null) {
break;
}
if (token instanceof Comment) {
value = ((Comment) token).getValue ();
if (value.indexOf ("PDF-1.") == 0) {
foundSig = true;
_version = value.substring (4, 7);
/* If we got this far, take note that the signature is OK. */
info.setSigMatch(_name);
break;
}
// The implementation notes (though not the spec)
// allow an alternative signature of %!PS-Adobe-N.n PDF-M.m
if (value.indexOf ("!PS-Adobe-") == 0) {
// But be careful: that much by itself is the standard
// PostScript signature.
int n = value.indexOf ("PDF-1.");
if (n >= 11) {
foundSig = true;
_version = value.substring (n + 4);
// However, this is not PDF-A compliant.
_pdfACompliant = false;
info.setSigMatch (_name);
break;
}
}
}
// If we don't find it right at the beginning, we aren't
// PDF/A compliant.
_pdfACompliant = false;
}
if (!foundSig) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage (nohdr, 0L));
return false;
}
// Check for PDF/A conformance. The next item must be
// a comment with four characters, each greater than 127
try {
token = _parser.getNext ();
String cmt = ((Comment) token).getValue ();
char[] cmtArray = cmt.toCharArray ();
int ctlcnt = 0;
for (int i = 0; i < 4; i++) {
if ((int) cmtArray[i] > 127) {
ctlcnt++;
}
}
if (ctlcnt < 4) {
_pdfACompliant = false;
}
}
catch (Exception e) {
// Most likely a ClassCastException on a non-comment
_pdfACompliant = false;
}
return true;
}
private long lastEOFOffset(RandomAccessFile raf) throws IOException {
long offset = 0;
long flen = 0;
byte[] buf = null;
// overkill to restore fileposition, but make this
// as side-effect free as possible
long savepos = raf.getFilePointer();
flen = raf.length();
buf = new byte[(int) Math.min(EOFSCANSIZE, flen)];
offset = flen - buf.length;
raf.seek(offset);
raf.read(buf);
raf.seek(savepos);
//OK:
// flen is the total length of the file
// offset is 1024 bytes from the end of file or 0 if file is shorter than 1024
// buf contains all bytes from offset to end of file
long eofpos = -1;
// Note the limits, selected so the index never is out of bounds
for (int i = buf.length-4; i >= 1; i--) {
if (buf[i] == '%') {
if ((buf[i-1] == '%') &&
(buf[i+1] == 'E') &&
(buf[i+2] == 'O') &&
(buf[i+3] == 'F')) {
eofpos = offset+i-1;
break;
}
}
}
// if (Tracing.T_MODULE) System.out.println(flen - eofpos);
return eofpos;
}
private long lastStartXrefOffset(RandomAccessFile raf, long eofOffset) throws IOException {
long offset = 0;
long flen = 0;
byte[] buf = null;
// overkill to restore fileposition, but make this
// as side-effect free as possible
long savepos = raf.getFilePointer();
flen = raf.length();
if (eofOffset <= 0) {
eofOffset = flen;
}
if (eofOffset >= flen) {
eofOffset = flen;
}
buf = new byte[(int) Math.min(XREFSCANSIZE, eofOffset)];
offset = eofOffset - buf.length;
raf.seek(offset);
raf.read(buf);
raf.seek(savepos);
//OK:
// flen is the total length of the file
// offset is 128 bytes from the end of file or 0 if file is shorter than 128
// buf contains all bytes from offset to end of file
long xrefpos = -1;
// Note the limits, selected so the index never is out of bounds
for (int i = buf.length-9; i >= 0; i--) {
if (buf[i] == 's') {
if ((buf[i+1] == 't') &&
(buf[i+2] == 'a') &&
(buf[i+3] == 'r') &&
(buf[i+4] == 't') &&
(buf[i+5] == 'x') &&
(buf[i+6] == 'r') &&
(buf[i+7] == 'e') &&
(buf[i+8] == 'f')) {
xrefpos = offset+i;
break;
}
}
}
// if (Tracing.T_MODULE) System.out.println(flen - xrefpos);
return xrefpos;
}
/** Locate the last trailer of the file */
protected boolean findLastTrailer (RepInfo info) throws IOException
{
/* Parse file trailer. Technically, this should be the last thing in
* the file, but we follow the Acrobat convention of looking in the
* last 1024 bytes. Since incremental updates may add multiple
* EOF comments, make sure that we use the last one in the file. */
Token token = null;
String value = null;
_eof = lastEOFOffset(_raf);
if (_eof < 0L) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage ("No PDF trailer",
_raf.length ()));
return false;
}
// For PDF-A compliance, this must be at the very end.
/* Fix contributed by FCLA, 2007-05-30, to test for trailing data
* properly.
*
* if (_raf.length () - _eof > 6) {
*/
if (_raf.length () - _eof > 7) {
_pdfACompliant = false;
}
/* Retrieve the "startxref" keyword. */
long startxrefoffset = lastStartXrefOffset(_raf, _eof);
_startxref = -1L;
if (startxrefoffset >= 0) {
try {
_parser.seek (startxrefoffset); // points to the 'startxref' kw
//_parser.seek (_eof - 23); // should we allow more slop?
}
catch (PdfException e) {}
while (true) {
try {
token = _parser.getNext ();
}
catch (Exception e) {
// we're starting at an arbitrary point, so there
// can be parsing errors. Ignore them till we get
// back in sync.
continue;
}
if (token == null) {
break;
}
if (token instanceof Keyword) {
value = ((Keyword) token).getValue ();
if (value.equals ("startxref")) {
try {
token = _parser.getNext ();
}
catch (Exception e) {
break; // no excuses here
}
if (token != null && token instanceof Numeric) {
_startxref = ((Numeric) token).getLongValue ();
}
}
}
}
}
if (_startxref < 0L) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage ("Missing startxref keyword " +
"or value", _parser.getOffset ()));
return false;
}
return true;
}
/* Parse a "trailer" (which is not necessarily the last
thing in the file, as trailers can be linked.) */
protected boolean parseTrailer (RepInfo info,
boolean prevOnly)
throws IOException
{
Token token = null;
String value = null;
String invalidMsg = "Invalid cross-reference table";
/* Parse the trailer dictionary. */
try {
_parser.seek (_startxref);
/* The next object may be either the keyword "xref", signifying
* a classic cross-reference table, or a stream object,
* signifying the new-style cross-reference stream.
*/
Token xref = _parser.getNext ();
if (xref instanceof Keyword) {
_xrefIsStream = false;
_parser.getNext (Numeric.class, invalidMsg); // first object number
_objCount = ((Numeric) _parser.getNext
(Numeric.class, invalidMsg)).getIntegerValue ();
_parser.seek (_parser.getOffset () + _objCount*20);
}
else if (xref instanceof Numeric) {
/* No cross-ref tables to backtrack. */
_xrefIsStream = true;
_prevxref = -1;
/* But I do need to read the dictionary at this point, to get
* essential stuff out of it. */
PdfStream str = (PdfStream) _parser.readObjectDef((Numeric) xref);
PdfDictionary dict = str.getDict();
_docCatDictRef = (PdfIndirectObj) dict.get ("Root");
if (_docCatDictRef == null) {
throw new PdfInvalidException
("Root entry missing in cross-ref stream dictionary",
_parser.getOffset ());
}
/* We don't need to see a trailer dictionary.
* Move along, move along. */
return true;
}
/* Now find the "trailer" keyword. */
long trailer = -1L;
while ((token = _parser.getNext ()) != null) {
if (token instanceof Keyword) {
value = ((Keyword) token).getValue ();
if (value.equals ("trailer")) {
token = _parser.getNext ();
if (token instanceof DictionaryStart) {
trailer = _parser.getOffset () - 7L;
break;
}
}
}
}
if (trailer < 0L) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage ("No file trailer",
_parser.getOffset ()));
return false;
}
_trailerDict = _parser.readDictionary ();
PdfObject obj;
// Extract contents of the trailer dictionary
_prevxref = -1;
obj = _trailerDict.get ("Prev");
if (obj != null) {
if (obj != null && obj instanceof PdfSimpleObject) {
token = ((PdfSimpleObject) obj ).getToken ();
if (token instanceof Numeric)
_prevxref = ((Numeric) token).getLongValue ();
}
if (_prevxref < 0) {
throw new PdfInvalidException
("Invalid Prev offset in trailer dictionary",
_parser.getOffset ());
}
}
// If this isn't the last (first read) trailer, then we
// ignore all the other dictionary entries.
if (prevOnly) {
return true;
}
obj = _trailerDict.get ("Size");
if (obj != null) {
_numObjects = -1;
if (obj != null && obj instanceof PdfSimpleObject) {
token = ((PdfSimpleObject) obj ).getToken ();
if (token instanceof Numeric)
_numObjects = ((Numeric) token).getIntegerValue ();
_xref = new long[_numObjects];
}
if (_numObjects < 0) {
throw new PdfInvalidException
("Invalid Size entry in trailer dictionary",
_parser.getOffset ());
}
if (_numObjects > 8388607) {
// Appendix C implementation limit is enforced by PDF/A
_pdfACompliant = false;
}
}
else throw new PdfInvalidException
("Size entry missing in trailer dictionary",
_parser.getOffset ());
_docCatDictRef = (PdfIndirectObj) _trailerDict.get ("Root");
if (_docCatDictRef == null) {
throw new PdfInvalidException
("Root entry missing in trailer dictionary",
_parser.getOffset ());
}
_encryptDictRef =(PdfIndirectObj) _trailerDict.get ("Encrypt"); // This is at least v. 1.1
_encrypted = (_encryptDictRef != null);
_parser.setEncrypted (_encrypted);
PdfObject infoObj = _trailerDict.get("Info");
if (infoObj != null && !(infoObj instanceof PdfIndirectObj)) {
throw new PdfInvalidException ("Trailer dictionary Info key is " +
"not an indirect reference",
_parser.getOffset ());
}
_docInfoDictRef = (PdfIndirectObj) infoObj;
obj = _trailerDict.get ("ID"); // This is at least v. 1.1
if (obj != null) {
String badID = "Invalid ID in trailer";
if (obj instanceof PdfArray) {
String [] id = new String[2];
try {
PdfArray idArray = (PdfArray) obj;
Vector<PdfObject> idVec = idArray.getContent ();
if (idVec.size () != 2) {
throw new PdfInvalidException (badID);
}
PdfSimpleObject idobj = (PdfSimpleObject) idVec.get(0);
id[0] = toHex
(((StringValuedToken) idobj.getToken () ).getRawBytes ());
idobj = (PdfSimpleObject) idVec.get(1);
id[1] = toHex
(((StringValuedToken) idobj.getToken () ).getRawBytes ());
_idProperty = new Property ("ID", PropertyType.STRING,
PropertyArity.ARRAY, id);
}
catch (Exception e) {
throw new PdfInvalidException (badID);
}
}
else {
throw new PdfInvalidException (badID, _parser.getOffset ());
}
}
obj = _trailerDict.get ("XRefStm");
if (obj != null) {
/* We have a "hybrid" cross-reference scheme. This means we have
* to go through the cross-reference stream and have its entries
* supplement the cross-reference section. */
_logger.warning("Hybrid cross-reference not yet implemented");
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// If it's merely invalid rather than ill-formed, keep going
return (e instanceof PdfInvalidException);
}
return true;
}
/* Parses the cross-reference table or stream. */
protected boolean readXRefInfo (RepInfo info) throws IOException
{
if (_xrefIsStream) {
return readXRefStreams (info);
}
else {
return readXRefTables (info);
}
}
/* Parses the cross-reference streams. This is called from
* readXRefInfo if there is no cross-reference table.
* I still need to deal with hybrid cases. All linked cross-reference
* streams are handled here.
*/
protected boolean readXRefStreams (RepInfo info) throws IOException
{
_pdfACompliant = false; // current version of PDF/A doesn't recognize XREF streams
while (_startxref > 0) {
try {
_parser.seek (_startxref);
PdfStream pstream =
(PdfStream) _parser.readObjectDef ();
int sObjNum = pstream.getObjNumber();
CrossRefStream xstream = new CrossRefStream (pstream);
if (!xstream.isValid ()) {
return false;
}
xstream.initRead (_raf);
int no = xstream.getNumObjects ();
if (_xref == null) {
_xref = new long [no];
_xref2 = new int[no] [];
}
if (sObjNum < 0 || sObjNum >= no) {
throw new PdfMalformedException
("Invalid object number in cross-reference stream",
_parser.getOffset ());
}
_xref[sObjNum] = _startxref; // insert the index of the xref stream itself
_startxref = xstream.getPrevXref();
try {
while (xstream.readNextObject()) {
int objNum = xstream.getObjNum();
if (xstream.isObjCompressed ()) {
// Hold off on this branch
_xref[objNum] = -1; // defers to _xref2
_xref2[objNum] = new int[] {
xstream.getContentStreamObjNum(),
xstream.getContentStreamIndex()
};
}
else {
if (_xref[objNum] == 0) {
_xref[objNum] = xstream.getOffset ();
}
}
}
_numFreeObjects += xstream.getFreeCount ();
}
catch (IOException e) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage
("Malformed cross reference stream",
_parser.getOffset ()));
return false;
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// If it's merely invalid rather than ill-formed, keep going
return (e instanceof PdfInvalidException);
}
}
return true; // incomplete, but let it through
}
/* Parses the cross-reference table. This is called from
* readXRefInfo if there is a cross-reference table. */
protected boolean readXRefTables (RepInfo info) throws IOException
{
Token token = null;
String badXref = "Malformed cross-reference table";
try {
_parser.seek (_startxref);
token = _parser.getNext (); // "xref" keyword or numeric
if (token instanceof Keyword) {
while ((token = _parser.getNext ()) != null) {
int firstObj = 0;
// Look for the start of a cross-ref subsection, which
// begins with a base object number and a count.
if (token instanceof Numeric) {
firstObj = ((Numeric) token).getIntegerValue ();
}
else {
// On anything else, assume we're done with this section.
// (Most likely we've hit the keyword "trailer".
break;
}
_objCount = ((Numeric) _parser.getNext ()).getIntegerValue ();
for (int i=0; i<_objCount; i++) {
// In reading the cross-reference table, also check
// the extra syntactic requirements of PDF/A.
long offset = ((Numeric) _parser.getNext
(Numeric.class, badXref)).getLongValue ();
_parser.getNext (); // Generation number
if (_parser.getWSString ().length () > 1) {
_pdfACompliant = false;
}
token = _parser.getNext (Keyword.class, badXref);
if (_parser.getWSString ().length () > 1) {
_pdfACompliant = false;
}
// A keyword of "n" signifies an object in use,
// "f" signifies a free object. If we already
// have an entry for this object, don't replace it.
String keyval = ((Keyword) token).getValue ();
if (keyval.equals ("n")) {
if (_xref[firstObj + i] == 0) {
_xref[firstObj + i] = offset;
}
}
else if (keyval.equals ("f")) {
_numFreeObjects++;
}
else {
throw new PdfMalformedException
("Illegal operator in xref table",
_parser.getOffset ());
}
}
}
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage (e.getMessage (),
_parser.getOffset ()));
return false;
}
catch (Exception e) {
info.setValid (false);
info.setMessage (new ErrorMessage (e.getMessage (),
_parser.getOffset ()));
}
return true;
}
protected boolean readDocCatalogDict (RepInfo info)
throws IOException
{
final String nocat = "No document catalog dictionary";
Property p = null;
_docCatDict = null;
_docCatalogList = new ArrayList<Property> (2);
// Get the Root reference which we had before, and
// resolve it to the dictionary object.
if (_docCatDictRef == null) {
info.setWellFormed (false);
info.setMessage (new ErrorMessage
(nocat, 0));
return false;
}
try {
_docCatDict = (PdfDictionary) resolveIndirectObject
(_docCatDictRef);
}
catch (Exception e) {
e.printStackTrace();
}
if (_docCatDict == null) {
// If it fails here, it's ill-formed, not
// just invalid
info.setWellFormed (false);
info.setMessage (new ErrorMessage
(nocat, 0));
return false;
}
try {
PdfObject viewPref = _docCatDict.get("ViewerPreferences");
viewPref = resolveIndirectObject (viewPref);
if (viewPref instanceof PdfDictionary) {
_viewPrefDict = (PdfDictionary) viewPref;
p = buildViewPrefProperty (_viewPrefDict);
_docCatalogList.add (p);
}
String pLayoutText = "SinglePage"; // default
PdfObject pLayout = resolveIndirectObject (_docCatDict.get ("PageLayout"));
if (pLayout instanceof PdfSimpleObject) {
pLayoutText = ((PdfSimpleObject) pLayout).getStringValue ();
}
p = new Property ("PageLayout",
PropertyType.STRING,
pLayoutText);
_docCatalogList.add (p);
String pModeText = "UseNone"; // default
PdfObject pMode = resolveIndirectObject (_docCatDict.get ("PageMode"));
if (pMode instanceof PdfSimpleObject) {
pModeText = ((PdfSimpleObject) pMode).getStringValue ();
}
p = new Property ("PageMode",
PropertyType.STRING,
pModeText);
_docCatalogList.add (p);
PdfObject outlines = resolveIndirectObject (_docCatDict.get ("Outlines"));
if (outlines instanceof PdfDictionary) {
_outlineDict = (PdfDictionary) outlines;
}
PdfObject lang = resolveIndirectObject (_docCatDict.get ("Lang"));
if (lang != null && lang instanceof PdfSimpleObject) {
String langText = ((PdfSimpleObject) lang).getStringValue ();
p = new Property ("Language", PropertyType.STRING,
_encrypted ? ENCRYPTED : langText);
_docCatalogList.add (p);
}
// The Pages dictionary doesn't go into the property,
// but this is a convenient time to grab it and the page label dictionary.
_pagesDictRef = (PdfIndirectObj) _docCatDict.get ("Pages");
_pageLabelDict = (PdfDictionary)
resolveIndirectObject (_docCatDict.get ("PageLabels"));
// Grab the Version entry, and use it to override the
// file header IF it's later.
PdfObject vers = resolveIndirectObject (_docCatDict.get ("Version"));
if (vers instanceof PdfSimpleObject) {
String versString = ((PdfSimpleObject) vers).getStringValue ();
String infoVersString = _version;
try {
double ver = Double.parseDouble (versString);
double infoVer = Double.parseDouble (infoVersString);
/* Set a message if this doesn't agree with RepInfo */
if (ver != infoVer) {
info.setMessage (new InfoMessage
("File header gives version as " +
versString +
", but catalog dictionary gives version as " +
infoVersString));
}
/* Replace the version in RepInfo if this is larger */
if (ver > infoVer) {
_version = versString;
}
}
catch (NumberFormatException e) {
throw new PdfInvalidException
("Invalid Version in document catalog");
}
}
// Get the Names dictionary in order to grab the
// EmbeddedFiles and Dests entries.
final String badname = "Invalid Names dictionary";
try {
PdfDictionary namesDict =
(PdfDictionary) resolveIndirectObject (_docCatDict.get ("Names"));
if (namesDict != null) {
PdfDictionary embeddedDict = (PdfDictionary) resolveIndirectObject
(namesDict.get ("EmbeddedFiles"));
if (embeddedDict != null) {
_embeddedFiles = new NameTreeNode (this, null, embeddedDict);
}
PdfDictionary dDict = (PdfDictionary) resolveIndirectObject
(namesDict.get ("Dests"));
if (dDict != null) {
_destNames = new NameTreeNode (this, null, dDict);
}
}
}
catch (ClassCastException ce) {
throw new PdfInvalidException (badname);
}
catch (Exception e) {
throw new PdfMalformedException (badname);
}
// Get the optional Dests dictionary. Note that destinations
// may be specified in either of two completely different
// ways: a dictionary here, or a name tree from the Names
// dictionary.
try {
_destsDict = (PdfDictionary) resolveIndirectObject
(_docCatDict.get ("Dests"));
}
catch (ClassCastException ce) {
throw new PdfInvalidException ("Invalid Dests dictionary");
}
catch (Exception e) {
throw new PdfMalformedException ("Invalid Dests dictionary");
}
}
catch (PdfException e) {
e.disparage (info); // clears Valid or WellFormed as appropriate
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Keep going if it's only invalid
return (e instanceof PdfInvalidException);
}
catch (Exception e) {
// Unexpected exception -- declare not well-formed
info.setWellFormed (false);
info.setMessage (new ErrorMessage
(e.toString (), _parser.getOffset ()));
return false;
}
return true;
}
protected boolean readEncryptDict (RepInfo info)
throws IOException
{
String filterText = "";
String effText = null;
// Get the reference which we had before, and
// resolve it to the dictionary object.
if (_encryptDictRef == null) {
return true; // encryption entry is optional
}
try {
_encryptList = new ArrayList<Property> (6);
PdfDictionary dict = (PdfDictionary) resolveIndirectObject
(_encryptDictRef);
_encryptDict = dict;
PdfObject filter = dict.get ("Filter");
if (filter instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject)filter).getToken ();
if (tok instanceof Name) {
filterText = ((Name) tok).getValue ();
}
}
Property p = new Property ("SecurityHandler",
PropertyType.STRING,
filterText);
_encryptList.add (p);
//PdfObject eff = dict.get ("EFF");
if (filter instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject)filter).getToken ();
if (tok instanceof Name) {
effText = ((Name) tok).getValue ();
}
}
if (effText != null) {
p = new Property ("EFF", PropertyType.STRING, effText);
_encryptList.add (p);
}
int algValue = 0;
PdfObject algorithm = dict.get ("V");
if (algorithm instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject) algorithm).getToken ();
if (tok instanceof Numeric) {
algValue = ((Numeric) tok).getIntegerValue ();
if (_je != null && _je.getShowRawFlag ()) {
p = new Property ("Algorithm",
PropertyType.INTEGER,
new Integer (algValue));
}
else {
try {
p = new Property ("Algorithm",
PropertyType.STRING,
PdfStrings.ALGORITHM[algValue]);
}
catch (Exception e) {
throw new PdfInvalidException
("Invalid algorithm value in encryption dictionary",
_parser.getOffset ());
}
}
if (p != null) {
_encryptList.add (p);
}
}
}
int keyLen = 40;
PdfObject length = dict.get ("Length");
if (length instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject) length).getToken ();
if (tok instanceof Numeric) {
keyLen = ((Numeric) tok).getIntegerValue ();
}
if (_je != null) {
p = new Property ("KeyLength",
PropertyType.INTEGER,
new Integer (keyLen));
_encryptList.add (p);
}
}
if ("Standard".equals (filterText)) {
List stdList = new ArrayList (4);
// Flags have a known meaning only if Standard
// security handler was specified
PdfObject flagObj = dict.get ("P");
PdfObject revObj = dict.get ("R");
int rev = 2; // assume old rev if not present
if (revObj instanceof PdfSimpleObject) {
rev = ((PdfSimpleObject) revObj).getIntValue ();
}
if (flagObj instanceof PdfSimpleObject) {
int flags =
((PdfSimpleObject) flagObj).getIntValue ();
String[] flagStrs;
if (rev == 2) {
flagStrs = PdfStrings.USERPERMFLAGS2;
}
else {
flagStrs = PdfStrings.USERPERMFLAGS3;
}
p = buildUserPermProperty (flags, flagStrs);
stdList.add (p);
stdList.add (new Property ("Revision",
PropertyType.INTEGER,
new Integer (rev)));
}
PdfObject oObj = dict.get ("O");
if (oObj != null) {
if (oObj instanceof PdfSimpleObject) {
stdList.add (new Property ("OwnerString",
PropertyType.STRING,
toHex (((PdfSimpleObject) oObj).getRawBytes ())));
}
}
PdfObject uObj = dict.get ("U");
if (uObj != null) {
if (uObj instanceof PdfSimpleObject) {
stdList.add (new Property ("UserString",
PropertyType.STRING,
toHex (((PdfSimpleObject) uObj).getRawBytes ())));
}
}
_encryptList.add (new Property ("StandardSecurityHandler",
PropertyType.PROPERTY,
PropertyArity.LIST,
stdList));
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
return (e instanceof PdfInvalidException);
}
return true;
}
protected boolean readDocInfoDict (RepInfo info)
throws IOException
{
// Get the Info reference which we had before, and
// resolve it to the dictionary object.
if (_docInfoDictRef == null) {
return true; // Info is optional
}
_docInfoList = new ArrayList<Property> (9);
try {
_docInfoDict = (PdfDictionary) resolveIndirectObject
(_docInfoDictRef);
addStringProperty (_docInfoDict, _docInfoList, "Title", "Title");
addStringProperty (_docInfoDict, _docInfoList, "Author", "Author");
addStringProperty (_docInfoDict, _docInfoList, "Subject", "Subject");
addStringProperty (_docInfoDict, _docInfoList, "Keywords", "Keywords");
addStringProperty (_docInfoDict, _docInfoList, "Creator", "Creator");
addStringProperty (_docInfoDict, _docInfoList, "Producer", "Producer");
// CreationDate requires string-to-date conversion
// ModDate does too
addDateProperty (_docInfoDict, _docInfoList, "CreationDate", "CreationDate");
addDateProperty (_docInfoDict, _docInfoList, "ModDate", "ModDate");
addStringProperty (_docInfoDict, _docInfoList, "Trapped", "Trapped");
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Keep parsing if it's only invalid
return (e instanceof PdfInvalidException);
}
catch (Exception e) {
info.setWellFormed(false);
info.setMessage (new ErrorMessage ("Unexpected exception " +
e.getClass().getName()));
}
return true;
}
protected boolean readDocumentTree (RepInfo info)
throws IOException
{
try {
if (_pagesDictRef == null) {
throw new PdfInvalidException ("Document page tree not found");
}
PdfObject pagesObj = resolveIndirectObject (_pagesDictRef);
if (!(pagesObj instanceof PdfDictionary))
throw new PdfMalformedException ("Invalid page dictionary object");
PdfDictionary pagesDict = (PdfDictionary) pagesObj;
_docTreeRoot = new PageTreeNode (this, null, pagesDict);
_docTreeRoot.buildSubtree (true, 100);
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Continue parsing if it's only invalid
return (e instanceof PdfInvalidException);
}
catch (Exception e) {
// Catch any odd exceptions
info.setMessage (new ErrorMessage (e.getClass().getName(), _parser.getOffset ()));
info.setWellFormed(false);
return false;
}
return true;
}
protected boolean readPageLabelTree (RepInfo info)
{
// the page labels number tree is optional.
try {
if (_pageLabelDict != null) {
_pageLabelRoot = new PageLabelNode (this, null, _pageLabelDict);
_pageLabelRoot.buildSubtree ();
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Continue parsing if it's only invalid
return (e instanceof PdfInvalidException);
}
catch (Exception e) {
info.setWellFormed(false);
info.setMessage (new ErrorMessage ("Unexpected exception " +
e.getClass().getName()));
return false;
}
return true; // always succeeds
}
protected boolean readXMPData (RepInfo info)
{
final String badMetadata = "Invalid or ill-formed XMP metadata";
try {
PdfStream metadata = (PdfStream) resolveIndirectObject (_docCatDict.get ("Metadata"));
if (metadata == null) {
return true; // Not required
}
//PdfDictionary metaDict = metadata.getDict ();
// Create an InputSource to feed the parser.
SAXParserFactory factory =
SAXParserFactory.newInstance();
factory.setNamespaceAware (true);
XMLReader parser = factory.newSAXParser ().getXMLReader ();
PdfXMPSource src = new PdfXMPSource (metadata, getFile ());
XMPHandler handler = new XMPHandler ();
parser.setContentHandler (handler);
parser.setErrorHandler (handler);
// We have to parse twice. The first time, we may get
// an encoding change as part of an exception thrown. If this
// happens, we create a new InputSource with the encoding, and
// continue.
try {
parser.parse (src);
_xmpProp = src.makeProperty ();
}
catch (SAXException se) {
String msg = se.getMessage ();
if (msg != null && msg.startsWith ("ENC=")) {
String encoding = msg.substring (5);
try {
src = new PdfXMPSource (metadata, getFile (), encoding);
parser.parse (src);
_xmpProp = src.makeProperty ();
}
catch (UnsupportedEncodingException uee) {
throw new PdfInvalidException (badMetadata);
}
}
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Continue parsing if it's only invalid
return (e instanceof PdfInvalidException);
}
catch (Exception e) {
info.setMessage (new ErrorMessage (badMetadata,
_parser.getOffset ()));
info.setValid (false);
return false;
}
return true;
}
protected void findExternalStreams (RepInfo info) throws IOException
{
_extStreamsList = new LinkedList<Property> ();
// stop processing if there is no root for the document tree
if (_docTreeRoot == null)
return;
_docTreeRoot.startWalk ();
try {
for (;;) {
// Get all the page objects in the document sequentially
PageObject page = _docTreeRoot.nextPageObject ();
if (page == null) {
break;
}
// Get the streams for the page and walk through them
List<PdfObject> streams = page.getContentStreams ();
if (streams != null) {
ListIterator<PdfObject> streamIter = streams.listIterator ();
while (streamIter.hasNext ()) {
PdfStream stream = (PdfStream) streamIter.next ();
String specStr = stream.getFileSpecification ();
if (specStr != null) {
Property prop = new Property ("File",
PropertyType.STRING,
specStr);
_extStreamsList.add (prop);
}
}
}
}
}
catch (Exception e) {
info.setWellFormed(false);
info.setMessage (new ErrorMessage ("Unexpected exception " +
e.getClass().getName()));
}
}
/** Locates the filters in the content stream dictionaries
* and generate a list of unique pipelines.
*
* @return <code>false</code> if the filter structure is
* defective.
*/
protected boolean findFilters (RepInfo info)
throws IOException
{
_filtersList = new LinkedList<Property> ();
// stop processing if there is no root for the document tree
if (_docTreeRoot == null)
return false;
_docTreeRoot.startWalk ();
try {
for (;;) {
// Get all the page objects in the document sequentially
PageObject page = _docTreeRoot.nextPageObject ();
if (page == null) {
break;
}
// Get the streams for the page and walk through them
List<PdfObject> streams = page.getContentStreams ();
if (streams != null) {
ListIterator<PdfObject> streamIter = streams.listIterator ();
while (streamIter.hasNext ()) {
PdfStream stream = (PdfStream) streamIter.next ();
Filter[] filters = stream.getFilters ();
extractFilters (filters, stream);
}
}
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
// Continue parsing if it's only invalid
return (e instanceof PdfInvalidException);
}
return true;
}
/** Finds the filters in a stream or array object which is the value
* of a stream's Filter key, and put them in _filtersList
* if a duplicate isn't there already. If the name is
* "Crypt", appends a colon and the name if available.
* Returns the filter string whether it's added or not,
* or null if there are no filters.
*/
protected String extractFilters (Filter[] filters, PdfStream stream)
{
/* Concatenate the names into a string of names separated
* by spaces. */
int len = filters.length;
if (len == 0) {
return null;
}
StringBuffer buf = new StringBuffer ();
for (int i = 0; i < len; i++) {
Filter filt = filters[i];
String fname = filt.getFilterName ();
buf.append(fname);
/* If it's a Crypt filter, add the crypt name. */
if ("Crypt".equals (fname)) {
String cname = filt.getNameParam();
if (cname != null) {
buf.append (":" + cname);
}
}
if (i < len - 1) {
buf.append (' ');
}
}
String filterStr = buf.toString ();
boolean unique = true;
// Check for uniqueness.
Iterator<Property> iter = _filtersList.iterator ();
while (iter.hasNext ()) {
Property p = (Property) iter.next ();
String s = (String) p.getValue ();
if (s.equals (filterStr)) {
unique = false;
break;
}
}
if (filterStr != null && unique) {
Property prop = new Property ("FilterPipeline",
PropertyType.STRING,
filterStr);
_filtersList.add (prop);
}
return filterStr;
}
protected void findImages (RepInfo info) throws IOException
{
_imagesList = new LinkedList<Property> ();
_docTreeRoot.startWalk ();
try {
for (;;) {
// Get all the page objects in the document sequentially
PageObject page = _docTreeRoot.nextPageObject ();
if (page == null) {
break;
}
// Get the resources for the page and look for image XObjects
PdfDictionary rsrc = page.getResources ();
if (rsrc != null) {
PdfDictionary xo = (PdfDictionary)
resolveIndirectObject (rsrc.get ("XObject"));
if (xo != null) {
Iterator<PdfObject> iter = xo.iterator ();
while (iter.hasNext ()) {
// Get an XObject and check if it's an image.
PdfDictionary xobdict = null;
PdfObject xob = resolveIndirectObject
((PdfObject) iter.next ());
if (xob instanceof PdfStream) {
xobdict = ((PdfStream) xob).getDict ();
}
if (xobdict != null) {
PdfSimpleObject subtype = (PdfSimpleObject) xobdict.get ("Subtype");
if ("Image".equals (subtype.getStringValue ())) {
// It's an image XObject. Report stuff.
List<Property> imgList = new ArrayList<Property> (10);
Property prop = new Property ("Image",
PropertyType.PROPERTY,
PropertyArity.LIST,
imgList);
NisoImageMetadata niso = new NisoImageMetadata ();
imgList.add (new Property ("NisoImageMetadata",
PropertyType.NISOIMAGEMETADATA, niso));
niso.setMimeType("application/pdf");
PdfSimpleObject widObj = (PdfSimpleObject)
xobdict.get ("Width");
niso.setImageWidth(widObj.getIntValue ());
PdfSimpleObject htObj = (PdfSimpleObject)
xobdict.get ("Height");
niso.setImageLength(htObj.getIntValue ());
// Check for filters to add to the filter list
Filter[] filters = ((PdfStream) xob).getFilters ();
String filt = extractFilters (filters, (PdfStream) xob);
if (filt != null) {
// If the filter is one which the NISO schema
// knows about, put it in the NISO metadata,
// otherwise put it in a Filter property.
int nisoFilt = nameToNiso (filt,
compressionStrings, compressionValues);
if (nisoFilt >= 0) {
/* If it's 2, it's a CCITTFaxDecode
* filter. There may be an optional
* K entry that can change the
* value.
*/
PdfObject parms =
xobdict.get ("DecodeParms");
if (parms != null) {
PdfSimpleObject kobj = null;
if (parms instanceof
PdfDictionary) {
kobj = (PdfSimpleObject)
((PdfDictionary) parms).get ("K");
}
/* Note that the DecodeParms
* value may also be an array
* of dictionaries. We are not
* handling that contingency.
*/
if (kobj != null) {
int k = kobj.getIntValue();
if (k < 0) {
nisoFilt = 4;
}
else if (k > 0) {
nisoFilt = 3;
}
}
}
niso.setCompressionScheme(nisoFilt);
}
else {
imgList.add (new Property("Filter",
PropertyType.STRING,
filt));
}
}
else {
niso.setCompressionScheme(1); // no filter
}
// Check for color space info
PdfObject colorSpc = xobdict.get ("ColorSpace");
if (colorSpc != null) {
String colorName = null;
if (colorSpc instanceof PdfSimpleObject) {
colorName = ((PdfSimpleObject) colorSpc).getStringValue ();
}
else if (colorSpc instanceof PdfArray) {
Vector<PdfObject> vec = ((PdfArray) colorSpc).getContent ();
// Use the first element, which is the color space family
PdfSimpleObject fam = (PdfSimpleObject) vec.elementAt (0);
colorName = fam.getStringValue ();
}
if (colorName != null) {
int nisoSpace = nameToNiso (colorName,
colorSpaceStrings, colorSpaceValues);
if (nisoSpace >= 0) {
niso.setColorSpace(nisoSpace);
}
else {
imgList.add (new Property ("ColorSpace",
PropertyType.STRING,
colorName));
}
}
}
PdfSimpleObject bpc = (PdfSimpleObject)
xobdict.get ("BitsPerComponent");
if (bpc != null) {
//imgList.add (new Property ("BitsPerComponent",
// PropertyType.INTEGER,
// new Integer (bpc.getIntValue ())));
niso.setBitsPerSample(new int[] { bpc.getIntValue() });
}
PdfSimpleObject intent = (PdfSimpleObject)
xobdict.get ("Intent");
if (intent != null) {
imgList.add (new Property ("Intent",
PropertyType.STRING,
intent.getStringValue ()));
}
PdfSimpleObject imgmsk = (PdfSimpleObject)
xobdict.get ("ImageMask");
if (imgmsk != null) {
boolean b = imgmsk.isTrue ();
imgList.add (new Property ("ImageMask",
PropertyType.BOOLEAN,
new Boolean (b)));
}
PdfArray dcd = (PdfArray) xobdict.get ("Decode");
if (dcd != null) {
Vector<PdfObject> dcdvec = dcd.getContent ();
List<Integer> dcdlst = new ArrayList<Integer> (dcdvec.size ());
Iterator<PdfObject> diter = dcdvec.iterator ();
while (diter.hasNext ()) {
PdfSimpleObject d = (PdfSimpleObject) diter.next ();
dcdlst.add (new Integer (d.getIntValue ()));
}
imgList.add (new Property ("Decode",
PropertyType.INTEGER,
PropertyArity.LIST,
dcdlst));
}
PdfSimpleObject intrp = (PdfSimpleObject)
xobdict.get ("Interpolate");
if (intrp != null) {
boolean b = intrp.isTrue ();
imgList.add (new Property ("Interpolate",
PropertyType.BOOLEAN,
new Boolean (b)));
}
PdfSimpleObject nam = (PdfSimpleObject)
xobdict.get ("Name");
if (nam != null) {
imgList.add (new Property ("Name",
PropertyType.STRING,
nam.getStringValue ()));
}
PdfSimpleObject id = (PdfSimpleObject)
resolveIndirectObject
(xobdict.get ("ID"));
if (id != null) {
String idstr = toHex (id.getStringValue ());
imgList.add (new Property ("ID",
PropertyType.STRING,
idstr));
}
_imagesList.add (prop);
}
}
}
}
}
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
}
catch (Exception e) {
info.setWellFormed(false);
info.setMessage (new ErrorMessage ("Unexpected exception " +
e.getClass().getName()));
}
}
/* Convert a Filter name to a NISO compression scheme value.
* If the name is unknown to NISO, return -1. */
protected int nameToNiso (String name,
String[] nameArray,
int[] valArray)
{
for (int i = 0; i < nameArray.length; i++) {
if (nameArray[i].equals (name)) {
return valArray[i];
}
}
return -1; // no match
}
protected void findFonts (RepInfo info) throws IOException
{
_type0FontsMap = new HashMap<Integer, PdfObject> ();
_type1FontsMap = new HashMap<Integer, PdfObject> ();
_trueTypeFontsMap = new HashMap<Integer, PdfObject> ();
_mmFontsMap = new HashMap<Integer, PdfObject> ();
_type3FontsMap = new HashMap<Integer, PdfObject> ();
_cid0FontsMap = new HashMap<Integer, PdfObject> ();
_cid2FontsMap = new HashMap<Integer, PdfObject> ();
try {
_docTreeRoot.startWalk ();
for (;;) {
// This time we need all the page objects and page tree
// nodes, because resources can be inherited from
// page tree nodes.
DocNode node = _docTreeRoot.nextDocNode ();
if (node == null) {
break;
}
// Get the fonts for the node
PdfDictionary fonts = null;
fonts = node.getFontResources ();
if (fonts != null) {
// In order to make sure we have a collection of
// unique fonts, we store them in a map keyed by
// object number.
Iterator<PdfObject> fontIter = fonts.iterator ();
while (fontIter.hasNext ()) {
PdfObject fontRef = (PdfObject) fontIter.next ();
PdfDictionary font = (PdfDictionary)
resolveIndirectObject (fontRef);
addFontToMap (font);
// If we've been directed appropriately,
// we accumulate the information, but don't
// report it. In that case, we post a message
// just once to that effect.
if (!_skippedFontsReported &&
!_showFonts &&
_verbosity != Module.MAXIMUM_VERBOSITY) {
info.setMessage (new InfoMessage
(fontsSkippedString));
_skippedFontsReported = true;
}
}
}
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (), _parser.getOffset ()));
return;
}
catch (Exception e) {
// Unexpected exception.
_logger.warning( "PdfModule.findFonts: " + e.toString ());
info.setWellFormed (false);
info.setMessage (new ErrorMessage
("Unexpected error in findFonts", e.toString (),
_parser.getOffset ()));
return;
}
}
/** Add the font to the appropriate map, and return its subtype.
* If we've exceeded the maximum number of fonts, then ignore it. */
protected String addFontToMap (PdfDictionary font)
{
if (++_nFonts > maxFonts) {
return null;
}
String subtypeStr = null;
try {
PdfSimpleObject subtype =
(PdfSimpleObject) font.get ("Subtype");
subtypeStr = subtype.getStringValue ();
if ("Type0".equals (subtypeStr)) {
_type0FontsMap.put(
new Integer (font.getObjNumber ()),
font);
// If the font is Type 0, we must go
// through its descendant fonts
PdfObject desc0 = font.get ("DescendantFonts");
PdfArray descendants =
(PdfArray) resolveIndirectObject (desc0);
Vector<PdfObject> subfonts = descendants.getContent ();
Iterator<PdfObject> subfontIter = subfonts.iterator ();
while (subfontIter.hasNext ()) {
PdfObject subfont = (PdfObject) subfontIter.next ();
subfont = resolveIndirectObject (subfont);
addFontToMap ((PdfDictionary) subfont);
}
}
else if ("Type1".equals (subtypeStr)) {
_type1FontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
else if ("MMType1".equals (subtypeStr)) {
_mmFontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
else if ("Type3".equals (subtypeStr)) {
_type3FontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
else if ("TrueType".equals (subtypeStr)) {
_trueTypeFontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
else if ("CIDFontType0".equals (subtypeStr)) {
_cid0FontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
else if ("CIDFontType2".equals (subtypeStr)) {
_cid2FontsMap.put(
new Integer (font.getObjNumber ()),
font);
}
return subtypeStr;
}
catch (Exception e) {
return null;
}
}
/******************************************************************
* PRIVATE CLASS METHODS.
******************************************************************/
protected static String toHex (String s)
{
StringBuffer buffer = new StringBuffer ("0x");
int len = s.length ();
for (int i=0; i<len; i++) {
String h = Integer.toHexString (s.charAt (i));
if (h.length () < 2) {
buffer.append ("0");
}
buffer.append (h);
}
return buffer.toString ();
}
protected static String toHex (Vector<Integer> v)
{
StringBuffer buffer = new StringBuffer ("0x");
int len = v.size ();
for (int i=0; i<len; i++) {
int hdigit = ((Integer) v.elementAt (i)).intValue ();
String h = Integer.toHexString (hdigit);
if (h.length () < 2) {
buffer.append ("0");
}
buffer.append (h);
}
return buffer.toString ();
}
// Store a PDF object in the object map. The key is a Long
// consisting of the concatenation of the object and generation
// numbers. Do I really need this? Is the xref table
// sufficient?
/* protected void addObject (int objectNum, int genNum, Object obj)
{
long key = ((long) objectNum << 32) +
((long) genNum & 0XFFFFFFFF);
_objects.put (new Long (key), obj);
} */
/**
* If the argument is an indirect object reference,
* returns the object it resolves to, otherwise returns
* the object itself. In particular, calling with null will
* return null.
*/
public PdfObject resolveIndirectObject(PdfObject indObj)
throws PdfException, IOException
{
if (indObj instanceof PdfIndirectObj) {
int objIndex = ((PdfIndirectObj) indObj).getObjNumber ();
/* Here we need to allow for the possibility that the
* object is compressed in an object stream. That means
* creating a new structure (call it _xref2) that contains
* the stream object number and offset whenever _xref[objIndex]
* is negative. _xref2 will have to contain the content
* stream object number (which will itself have to be
* resolved) and the offset into the object stream.
*/
return getObject (objIndex, 30);
}
else {
return indObj;
}
}
/** Returns an object of a given number. This may involve
* recursion into object streams, in which case it calls itself.
*
* @param objIndex The object number to look up
* @param recGuard The maximum permitted number of recursion levels;
* no particular value is required, but 30 or more
* should avoid false exceptions.
*/
protected PdfObject getObject (int objIndex, int recGuard)
throws PdfException, IOException
{
/* Guard against infinite recursion */
if (recGuard <= 0) {
throw new PdfMalformedException ("Improper nesting of object streams");
}
final String nogood = "Invalid object number or object stream";
long offset = _xref [objIndex];
if (offset == 0) {
return null; // This is considered legitimate by the spec
}
if (offset < 0) {
/* The object is located in an object stream. Need to get the
* object stream first.
* Be cautious dealing with _cachedStreamIndex and _cachedObjectStream;
* these can be modified by a recursive call to getObject. */
try {
int streamObjIndex = _xref2[objIndex][0];
PdfObject streamObj;
ObjectStream ostrm = null;
if (streamObjIndex ==_cachedStreamIndex) {
ostrm = _cachedObjectStream;
// Reset it
if (ostrm.isValid ()) {
ostrm.readIndex ();
}
}
else {
streamObj =
resolveIndirectObject (getObject (streamObjIndex, recGuard - 1));
if (streamObj instanceof PdfStream) {
ostrm = new ObjectStream ((PdfStream) streamObj, _raf);
if (ostrm.isValid ()) {
ostrm.readIndex ();
_cachedObjectStream = ostrm;
_cachedStreamIndex = streamObjIndex;
}
else {
throw new PdfMalformedException (nogood);
}
}
}
/* And finally extract the object from the object stream. */
return ostrm.getObject (objIndex);
}
catch (Exception e) {
/* Fall through with error */
}
throw new PdfMalformedException (nogood);
}
else {
_parser.seek (offset);
PdfObject obj = _parser.readObjectDef ();
obj.setObjNumber (objIndex);
return obj;
}
}
/**
* Return the RandomAccessFile being read.
*/
public RandomAccessFile getFile ()
{
return _raf;
}
/**
* Returns the catalog dictionary object.
*/
public PdfDictionary getCatalogDict ()
{
return _docCatDict;
}
/**
* Returns the trailer dictionary object.
*/
public PdfDictionary getTrailerDict ()
{
return _trailerDict;
}
/**
* Returns the viewer preferences dictionary object.
*/
public PdfDictionary getViewPrefDict ()
{
return _viewPrefDict;
}
/**
* Returns the outlines dictionary object.
*/
public PdfDictionary getOutlineDict ()
{
return _outlineDict;
}
/**
* Get a font map. The map returned is determined by the selector.
* Any other value returns null.
*/
public Map<Integer, PdfObject> getFontMap (int selector)
{
switch (selector) {
case F_TYPE0:
return _type0FontsMap;
case F_TYPE1:
return _type1FontsMap;
case F_TT:
return _mmFontsMap;
case F_TYPE3:
return _type3FontsMap;
case F_MM1:
return _mmFontsMap;
case F_CID0:
return _cid0FontsMap;
case F_CID2:
return _cid2FontsMap;
default:
return null;
}
}
/**
* Return a List of all the font maps. Together, these contain
* all the fonts and subfonts in the document. Some of the maps
* may be null.
*/
public List<Map<Integer, PdfObject>> getFontMaps ()
{
List<Map<Integer, PdfObject>> lst = new ArrayList<Map<Integer, PdfObject>> (7);
lst.add (_type0FontsMap);
lst.add (_type1FontsMap);
lst.add (_mmFontsMap);
lst.add (_type3FontsMap);
lst.add (_trueTypeFontsMap);
lst.add (_cid0FontsMap);
lst.add (_cid2FontsMap);
return lst;
}
/**
* Returns a NameTreeNode for the EmbeddedFiles entry of the
* Names dictionary. Returns null if there isn't one.
*/
public NameTreeNode getEmbeddedFiles ()
{
return _embeddedFiles;
}
/**
* Add the various font lists as a fonts property. Note: only add
* the "Fonts" property if there are, in fact, fonts defined.
*/
protected void addFontsProperty (List<Property> metadataList)
{
List<Property> fontTypesList = new LinkedList<Property> ();
Property fontp = null;
if (_type0FontsMap != null && !_type0FontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("Type0", _type0FontsMap, F_TYPE0);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
// Report an error here?
}
}
if (_type1FontsMap != null && !_type1FontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("Type1", _type1FontsMap, F_TYPE1);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
// Report an error here?
}
}
if (_trueTypeFontsMap != null && !_trueTypeFontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("TrueType", _trueTypeFontsMap,
F_TT);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
// Report an error here?
}
}
if (_type3FontsMap != null && !_type3FontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("Type3", _type3FontsMap, F_TYPE3);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
}
}
if (_mmFontsMap != null && !_mmFontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("MMType1", _mmFontsMap, F_MM1);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
}
}
if (_cid0FontsMap != null && !_cid0FontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("CIDFontType0", _cid0FontsMap,
F_CID0);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
}
}
if (_cid2FontsMap != null && !_cid2FontsMap.isEmpty ()) {
try {
fontp = buildFontProperty ("CIDFontType2", _cid2FontsMap,
F_CID2);
fontTypesList.add (fontp);
}
catch (ClassCastException e) {
}
}
if (fontTypesList.size () > 0) {
metadataList.add (new Property ("Fonts", PropertyType.PROPERTY,
PropertyArity.LIST,
fontTypesList));
}
}
/* Build Pages property, with associated subproperties. */
protected void addPagesProperty (List<Property> metadataList, RepInfo info)
{
_pagesList = new LinkedList<Property> ();
_pageSeqMap = new HashMap<Integer, Integer> (500);
try {
_docTreeRoot.startWalk ();
int pageIndex = 0;
// Start the pipe with two entries.
// We always need to have the current and the next
// entry from the page label tree in order to determine
// the lower and upper bounds of the applicable range.
// If the first entry has a bound greater than zero,
// that appears to be an undefined situation, so we
// always treat the first entry as starting at zero.
if (_pageLabelRoot != null) {
if (!_pageLabelRoot.findNextKeyValue ()) {
throw new PdfMalformedException ("Bad page labels");
}
_pageLabelRoot.findNextKeyValue ();
}
for (;;) {
// Get all the page objects in the document sequentially
// Have to do this in two passes so that link
// destinations can be properly reported.
PageObject page = _docTreeRoot.nextPageObject ();
if (page == null) {
break;
}
_pageSeqMap.put (
new Integer (page.getDict ().getObjNumber ()),
new Integer (pageIndex + 1));
}
_docTreeRoot.startWalk ();
for (;;) {
PageObject page = _docTreeRoot.nextPageObject ();
if (page == null) {
break;
}
Property p = buildPageProperty (page, pageIndex++, info);
_pagesList.add (p);
}
if (_showPages || _verbosity == Module.MAXIMUM_VERBOSITY) {
Property prop = new Property ("Pages",
PropertyType.PROPERTY,
PropertyArity.LIST,
_pagesList);
metadataList.add (prop);
}
else {
if (!_skippedPagesReported) {
info.setMessage (new InfoMessage
(pagesSkippedString));
}
_skippedPagesReported = true;
}
}
catch (PdfException e) {
e.disparage (info);
info.setMessage (new ErrorMessage
(e.getMessage (),
_parser.getOffset ()));
return ;
}
}
/* Build a subproperty for one PageObject. */
protected Property buildPageProperty (PageObject page,
int idx,
RepInfo info)
throws PdfException
{
List<Property> pagePropList = new ArrayList<Property> (4);
try {
// Foo on Java's inability to return values through
// parameters. Passing an array is a crock to achieve
// that effect.
int nominalNum[] = new int[1];
Property plProp = buildPageLabelProperty (page, idx, nominalNum);
if (plProp != null) {
pagePropList.add (plProp);
}
if (plProp == null || nominalNum[0] != idx + 1) {
// Page sequence is different from label, or
// there is no label. Make it 1-based.
pagePropList.add (new Property ("Sequence",
PropertyType.INTEGER,
new Integer (idx + 1)));
}
}
catch (PdfException e) {
throw e;
}
catch (Exception f) {
throw new PdfMalformedException ("Invalid page label info");
}
try {
List<Property> annotsList = new LinkedList<Property> ();
PdfArray annots = page.getAnnotations ();
if (annots != null) {
Vector<PdfObject> contents = annots.getContent ();
for (int i = 0; i < contents.size (); i++) {
PdfDictionary annot =
(PdfDictionary) resolveIndirectObject
((PdfObject) contents.elementAt (i));
annotsList.add (buildAnnotProperty (annot, info));
}
if (!annotsList.isEmpty ()) {
if (_showAnnotations ||
_verbosity == Module.MAXIMUM_VERBOSITY) {
Property annotProp = new Property ("Annotations",
PropertyType.PROPERTY,
PropertyArity.LIST,
annotsList);
pagePropList.add (annotProp);
}
else {
// We don't report annotations if we got here,
// but we do report that we don't report them.
if (!_skippedAnnotationsReported) {
info.setMessage (new InfoMessage
(annotationsSkippedString));
_skippedAnnotationsReported = true;
}
}
}
}
}
catch (PdfException e) {
throw e;
}
catch (Exception f) {
throw new PdfMalformedException ("Invalid Annotation list");
}
try {
// Rotation property is inheritable
PdfSimpleObject rot = (PdfSimpleObject) page.get ("Rotate", true);
if (rot != null && rot.getIntValue () != 0) {
pagePropList.add (new Property ("Rotate",
PropertyType.INTEGER,
new Integer (rot.getIntValue ())));
}
// UserUnit property (1.6), not inheritable
PdfSimpleObject uu = (PdfSimpleObject) page.get ("UserUnit", false);
if (uu != null) {
pagePropList.add (new Property ("UserUnit",
PropertyType.DOUBLE,
new Double (rot.getDoubleValue())));
}
// Viewport dictionaries (1.6), not inheritable
PdfArray vp = (PdfArray) page.get ("VP", false);
if (vp != null) {
Vector<PdfObject> vpv = vp.getContent();
Iterator<PdfObject> iter = vpv.iterator();
List<Property> vplist = new ArrayList<Property> (vpv.size());
while (iter.hasNext ()) {
PdfDictionary vpd = (PdfDictionary)
resolveIndirectObject((PdfObject) iter.next ());
PdfObject vpdbb = vpd.get ("BBox");
List<Property> vpPropList = new ArrayList<Property> ();
vpPropList.add (makeRectProperty
((PdfArray) resolveIndirectObject (vpdbb),
"BBox"));
PdfObject meas = vpd.get ("Measure");
if (meas instanceof PdfDictionary) {
vpPropList.add (buildMeasureProperty ((PdfDictionary) meas));
// No, that's wrong -- the Viewport property itself
// needs to be a list with a bounding box.
}
vplist.add (new Property ("Viewport",
PropertyType.PROPERTY,
PropertyArity.LIST,
vpPropList));
}
pagePropList.add (new Property ("Viewports",
PropertyType.PROPERTY,
PropertyArity.LIST,
vplist));
}
// Thumbnail -- we just report if it's there. It's a
// non-inheritable property
PdfObject thumb = page.get ("Thumb", false);
if (thumb != null) {
pagePropList.add (new Property ("Thumb",
PropertyType.BOOLEAN,
Boolean.TRUE));
}
return new Property ("Page",
PropertyType.PROPERTY,
PropertyArity.LIST,
pagePropList);
}
// catch (PdfException e) {
// throw e;
// }
catch (Exception f) {
throw new PdfMalformedException ("Invalid page dictionary");
}
}
/* Build a subproperty of a subproperty for page labels.
* The nomNumRef argument is a crock for returning the
* nominal number; element 0 of the array is replaced
* by the nominal number of the page. */
protected Property buildPageLabelProperty (PageObject page,
int pageIndex,
int[] nomNumRef)
throws PdfException
{
if (_pageLabelRoot == null) {
return null; // no page label info
}
// Note that our "current" page is the page label tree's
// "previous" key. Sorry about that...
int curFirstPage = _pageLabelRoot.getPrevKey ();
int nextFirstPage = _pageLabelRoot.getCurrentKey ();
try {
// If we're onto the next page range, advance our pointers.
if (pageIndex >= nextFirstPage) {
_pageLabelRoot.findNextKeyValue ();
curFirstPage = nextFirstPage;
}
PdfDictionary pageLabelDict =
(PdfDictionary) resolveIndirectObject
(_pageLabelRoot.getPrevValue ());
StringBuffer labelText = new StringBuffer ();
PdfSimpleObject prefixObj =
(PdfSimpleObject) pageLabelDict.get ("P");
if (prefixObj != null) {
labelText.append (prefixObj.getStringValue ());
}
PdfSimpleObject firstPageObj =
(PdfSimpleObject) pageLabelDict.get ("St");
int nominalPage;
if (firstPageObj != null) {
nominalPage = pageIndex - curFirstPage +
firstPageObj.getIntValue ();
}
else {
nominalPage = pageIndex - curFirstPage + 1;
}
if (nominalPage <= 0) {
throw new PdfInvalidException ("Invalid page label sequence");
}
nomNumRef[0] = nominalPage;
// Get the numbering style. If there is no numbering
// style entry, the label consists only of the prefix.
PdfSimpleObject numStyleObj =
(PdfSimpleObject) pageLabelDict.get ("S");
String numStyle;
if (numStyleObj == null) {
numStyle = null;
}
else {
numStyle = numStyleObj.getStringValue ();
}
if ("D".equals (numStyle)) {
// Nice, simple decimal numbers
labelText.append (nominalPage);
}
else if ("R".equals (numStyle)) {
// Upper case roman numerals
labelText.append
(PageLabelNode.intToRoman (nominalPage, true));
}
else if ("r".equals (numStyle)) {
// Lower case roman numerals
labelText.append
(PageLabelNode.intToRoman (nominalPage, false));
}
else if ("A".equals (numStyle)) {
// Uppercase letters (A-Z, AA-ZZ, ...)
labelText.append
(PageLabelNode.intToBase26 (nominalPage, true));
}
else if ("a".equals (numStyle)) {
// Lowercase letters (a-z, aa-zz, ...)
labelText.append
(PageLabelNode.intToBase26 (nominalPage, false));
}
// It screws up the PDF output if we have a blank Label property.
if (labelText.length() == 0) {
labelText.append("[empty]");
}
return new Property ("Label",
PropertyType.STRING,
labelText.toString ());
}
catch (Exception e) {
throw new PdfMalformedException ("Problem with page label structure");
}
}
/* Build a subproperty for a measure dictionary. */
protected Property buildMeasureProperty (PdfDictionary meas)
{
List<Property> plist = new ArrayList<Property> ();
PdfObject itemObj = meas.get ("Subtype");
if (itemObj instanceof PdfSimpleObject) {
plist.add (new Property ("Subtype",
PropertyType.STRING,
((PdfSimpleObject)itemObj).getStringValue ()));
}
itemObj = meas.get ("R");
if (itemObj instanceof PdfSimpleObject) {
plist.add (new Property ("Ratio",
PropertyType.STRING,
((PdfSimpleObject) itemObj).getStringValue ()));
}
// All kinds of stuff I could add -- limit it to the required
// X, Y, D and A arrays.
itemObj = meas.get ("X");
if (itemObj instanceof PdfArray) {
Vector<PdfObject> v = ((PdfArray) itemObj).getContent ();
double[] x = new double[v.size()];
for (int i = 0; i < v.size (); i++) {
PdfSimpleObject xobj = (PdfSimpleObject) v.elementAt (i);
x[i] = xobj.getDoubleValue();
}
plist.add (new Property ("X", PropertyType.DOUBLE,
PropertyArity.ARRAY, x));
}
itemObj = meas.get ("Y");
if (itemObj instanceof PdfArray) {
Vector<PdfObject> v = ((PdfArray) itemObj).getContent ();
double[] x = new double[v.size()];
for (int i = 0; i < v.size (); i++) {
PdfSimpleObject xobj = (PdfSimpleObject) v.elementAt (i);
x[i] = xobj.getDoubleValue();
}
plist.add (new Property ("Y", PropertyType.DOUBLE,
PropertyArity.ARRAY, x));
}
itemObj = meas.get ("D");
if (itemObj instanceof PdfArray) {
Vector<PdfObject> v = ((PdfArray) itemObj).getContent ();
double[] x = new double[v.size()];
for (int i = 0; i < v.size (); i++) {
PdfSimpleObject xobj = (PdfSimpleObject) v.elementAt (i);
x[i] = xobj.getDoubleValue();
}
plist.add (new Property ("Distance", PropertyType.DOUBLE,
PropertyArity.ARRAY, x));
}
itemObj = meas.get ("A");
if (itemObj instanceof PdfArray) {
Vector<PdfObject> v = ((PdfArray) itemObj).getContent ();
double[] x = new double[v.size()];
for (int i = 0; i < v.size (); i++) {
PdfSimpleObject xobj = (PdfSimpleObject) v.elementAt (i);
x[i] = xobj.getDoubleValue();
}
plist.add (new Property ("Area", PropertyType.DOUBLE,
PropertyArity.ARRAY, x));
}
return new Property ("Measure",
PropertyType.PROPERTY,
PropertyArity.LIST,
plist);
}
/* Build a subproperty of a subproperty for an annotation. */
protected Property buildAnnotProperty (PdfDictionary annot, RepInfo info)
throws PdfException
{
List<Property> propList = new ArrayList<Property> (7);
PdfObject itemObj;
try {
// Subtype is required
itemObj = (PdfSimpleObject) annot.get ("Subtype");
propList.add (new Property ("Subtype",
PropertyType.STRING,
((PdfSimpleObject)itemObj).getStringValue ()));
// Contents is optional for some subtypes, required for
// others. We consider it optional here.
itemObj = (PdfSimpleObject) annot.get ("Contents");
if (itemObj != null) {
propList.add (new Property ("Contents", PropertyType.STRING,
_encrypted ? ENCRYPTED :
((PdfSimpleObject)itemObj).getStringValue ()));
}
// Rectangle is required, and must be in the rectangle format
itemObj = annot.get ("Rect");
propList.add (makeRectProperty
((PdfArray) resolveIndirectObject (itemObj),
"Rect"));
// Name comes from the NM entry and is optional
itemObj = annot.get ("NM");
if (itemObj != null) {
propList.add (new Property ("Name",
PropertyType.STRING,
((PdfSimpleObject)itemObj).getStringValue ()));
}
// LastModified is optional. The documentation says that
// a PDF date is preferred but not guaranteed. We just
// put it out as a string.
itemObj = annot.get ("M");
if (itemObj != null) {
Literal lastModLit =
(Literal) ((PdfSimpleObject) itemObj).getToken ();
Property dateProp;
dateProp = new Property ("LastModified",
PropertyType.STRING,
lastModLit.getValue ());
propList.add (dateProp);
}
// Flags.
itemObj = annot.get ("F");
if (itemObj != null) {
int flagValue = ((PdfSimpleObject) itemObj).getIntValue ();
Property flagProp = (buildBitmaskProperty (flagValue,
"Flags",
PdfStrings.ANNOTATIONFLAGS,
"No flags set"));
if (flagProp != null) {
propList.add (flagProp);
}
}
// Appearance dictionary -- just check if it's there.
itemObj = annot.get ("AP");
if (itemObj != null) {
propList.add (new Property ("AppearanceDictionary",
PropertyType.BOOLEAN,
Boolean.TRUE));
}
// Action dictionary -- if it's there, set actionsExist
itemObj = annot.get ("A");
if (itemObj != null) {
_actionsExist = true;
itemObj = resolveIndirectObject (itemObj);
// Actions are as common as Destinations for
// connecting to destination pages. If the Action
// is of type GoTo, note its destination.
PdfSimpleObject annType = (PdfSimpleObject)
((PdfDictionary) itemObj).get ("S");
if (annType == null) {
throw new PdfMalformedException ("Annotation dictionary " +
"missing required type (S) entry");
}
if ("GoTo".equals (annType.getStringValue ())) {
PdfObject destObj =
((PdfDictionary) itemObj).get ("D");
if (destObj != null) {
addDestination (destObj, "ActionDest", propList,
info);
}
}
}
// Destination object.
itemObj = annot.get ("Dest");
if (itemObj != null) {
addDestination (itemObj, "Destination", propList, info);
}
// Reply Type (RT) (1.6)
itemObj = annot.get ("RT");
if (itemObj instanceof PdfSimpleObject) {
String type = ((PdfSimpleObject) itemObj).getStringValue();
propList.add (new Property ("ReplyType",
PropertyType.STRING,
type));
}
// Intent (IT) (1.6)
itemObj = annot.get ("IT");
if (itemObj instanceof PdfSimpleObject) {
String type = ((PdfSimpleObject) itemObj).getStringValue();
propList.add (new Property ("Intent",
PropertyType.STRING,
type));
}
// Callout Line (CL) (1.6)
itemObj = annot.get ("CL");
if (itemObj instanceof PdfArray) {
Vector<PdfObject> clData = ((PdfArray) itemObj).getContent();
// This should be an array of numbers.
Iterator<PdfObject> iter = clData.iterator ();
List<Double> clList = new ArrayList<Double> (6);
while (iter.hasNext ()) {
PdfSimpleObject clItem = (PdfSimpleObject) iter.next ();
clList.add (new Double (clItem.getDoubleValue()));
}
propList.add (new Property ("CalloutLine",
PropertyType.DOUBLE,
PropertyArity.LIST,
clList));
}
return new Property ("Annotation", PropertyType.PROPERTY,
PropertyArity.LIST, propList);
}
catch (PdfMalformedException ee) {
// Just rethrow these
throw ee;
}
catch (Exception e) {
throw new PdfMalformedException ("Invalid Annotation property");
}
}
/* Given a PdfObject that stands for a Destination, add
* a representative property to the property list.
*/
protected void addDestination (PdfObject itemObj, String propName,
List<Property> propList, RepInfo info)
throws PdfException
{
try {
Destination dest = new Destination (itemObj, this, false);
if (dest.isIndirect()) {
// Encryption messes up name trees
if (!_encrypted) {
int pageObjNum = resolveIndirectDest
(dest.getIndirectDest ());
if (pageObjNum == -1) {
// The scope of the reference is outside this
// file, so we just report it as such.
propList.add (new Property (propName,
PropertyType.STRING,
"External"));
}
else {
propList.add (new Property (propName,
PropertyType.INTEGER,
new Integer (pageObjNum)));
}
}
}
else {
int pageObjNum = dest.getPageDestObjNumber ();
Integer destPg = (Integer)
_pageSeqMap.get (new Integer (pageObjNum));
if (destPg != null) {
propList.add (new Property (propName,
PropertyType.INTEGER,
destPg));
}
}
}
catch (Exception e) {
propList.add (new Property (propName, PropertyType.STRING,
"null"));
info.setMessage (new ErrorMessage (e.getMessage (),
_parser.getOffset ()));
info.setValid (false);
}
}
/* Build up a property for one of the kinds of fonts
* in the file.
*/
protected Property buildFontProperty (String name, Map map, int fontType)
{
List<Property> fontList = new LinkedList<Property> (); // list of fonts
Iterator<PdfObject> fontIter = map.values ().iterator ();
while (fontIter.hasNext ()) {
// For each font in the map, build a property for it,
// which consists of a list of scalar properties. Each kind
// of font is spec'ed to have a slightly different set of
// properties, grumble...
PdfDictionary dict = (PdfDictionary) fontIter.next ();
List<Property> fontPropList = oneFontPropList (dict, fontType);
Property fProp = new Property ("Font",
PropertyType.PROPERTY,
PropertyArity.LIST,
fontPropList);
fontList.add (fProp);
}
return new Property (name,
PropertyType.PROPERTY,
PropertyArity.LIST,
fontList);
}
/* Build the Property list for a given font */
protected List<Property> oneFontPropList (PdfDictionary dict, int fontType)
{
List<Property> fontPropList = new LinkedList<Property> ();
Property prop;
if (fontType == F_TYPE1 || fontType == F_TYPE3 || fontType == F_MM1 ||
fontType == F_TT) {
PdfObject tempObj = dict.get ("Name");
PdfSimpleObject nameObj = null;
if (tempObj instanceof PdfSimpleObject) {
nameObj = (PdfSimpleObject) tempObj;
}
else if (tempObj instanceof PdfIndirectObj) {
nameObj = (PdfSimpleObject)
((PdfIndirectObj) tempObj).getObject ();
}
if (nameObj != null) {
String nameStr = nameObj.getStringValue ();
prop = new Property ("Name", PropertyType.STRING, nameStr);
fontPropList.add (prop);
}
}
String baseStr = null;
if (fontType != F_TYPE3) {
PdfObject tempObj = dict.get ("BaseFont");
PdfSimpleObject baseFontObj = null;
if (tempObj instanceof PdfSimpleObject) {
baseFontObj = (PdfSimpleObject) tempObj;
}
else if (tempObj instanceof PdfIndirectObj) {
baseFontObj = (PdfSimpleObject)
((PdfIndirectObj) tempObj).getObject ();
}
if (baseFontObj != null) {
baseStr = baseFontObj.getStringValue ();
prop = new Property ("BaseFont", PropertyType.STRING,
baseStr);
fontPropList.add (prop);
}
}
if (fontType == F_CID0 || fontType == F_CID2) {
PdfObject elCid = dict.get ("CIDSystemInfo");
try {
elCid = resolveIndirectObject (elCid);
}
catch (Exception e) {}
if (elCid instanceof PdfDictionary) {
prop = buildCIDInfoProperty ((PdfDictionary) elCid);
fontPropList.add (prop);
}
}
if (fontType == F_TYPE1 || fontType == F_TT || fontType == F_MM1) {
if (isFontSubset (baseStr)) {
prop = new Property ("FontSubset", PropertyType.BOOLEAN,
Boolean.TRUE);
fontPropList.add (prop);
}
}
if (fontType == F_TYPE1 || fontType == F_TT || fontType == F_MM1 ||
fontType == F_TYPE3) {
PdfObject firstCharObj = dict.get("FirstChar");
if (firstCharObj instanceof PdfIndirectObj) {
firstCharObj = ((PdfIndirectObj) firstCharObj).getObject ();
}
try {
int firstChar = ((PdfSimpleObject) firstCharObj).getIntValue();
prop = new Property ("FirstChar", PropertyType.INTEGER,
new Integer (firstChar));
fontPropList.add (prop);
}
catch (Exception e) {}
PdfObject lastCharObj = dict.get("LastChar");
if (lastCharObj instanceof PdfIndirectObj) {
lastCharObj = ((PdfIndirectObj) lastCharObj).getObject ();
}
try {
int lastChar = ((PdfSimpleObject) lastCharObj).getIntValue ();
prop = new Property ("LastChar", PropertyType.INTEGER,
new Integer (lastChar));
fontPropList.add (prop);
}
catch (Exception e) {}
}
if (fontType == F_TYPE3) {
// Put FontBBox and CharProcs into properties
PdfObject bboxObj = dict.get("FontBBox");
try {
if (bboxObj instanceof PdfArray) {
fontPropList.add (makeRectProperty ((PdfArray) bboxObj,
"FontBBox"));
}
}
catch (Exception e) {}
// For CharProcs, we're just checking if it's there.
// (It's required for a Type 3 font.)
// PdfObject charProcs = dict.get ("CharProcs");
// prop = new Property ("CharProcs",
// PropertyType.BOOLEAN,
// new Boolean (charProcs != null));
// fontPropList.add (prop);
}
if (fontType == F_TYPE1 || fontType == F_TT || fontType == F_MM1 ||
fontType == F_CID0 || fontType == F_CID2) {
PdfObject descriptorObj = dict.get ("FontDescriptor");
try {
descriptorObj = resolveIndirectObject (descriptorObj);
}
catch (Exception e) {}
if (descriptorObj instanceof PdfDictionary) {
prop = buildFontDescriptorProperty ((PdfDictionary)
descriptorObj);
fontPropList.add (prop);
}
}
PdfObject encodingObj = dict.get ("Encoding");
try {
encodingObj = resolveIndirectObject (encodingObj);
}
catch (Exception e) {}
if (fontType == F_TYPE0 || fontType == F_TYPE1 || fontType == F_TT ||
fontType == F_MM1 || fontType == F_TYPE3) {
// Encoding property -- but only if Encoding is a name
if (encodingObj instanceof PdfSimpleObject) {
prop = new Property ("Encoding", PropertyType.STRING,
((PdfSimpleObject) encodingObj).getStringValue ());
fontPropList.add (prop);
}
}
if (fontType == F_TYPE1 || fontType == F_TT || fontType == F_MM1 ||
fontType == F_TYPE3) {
if (encodingObj != null && encodingObj instanceof PdfDictionary) {
prop = buildEncodingDictProperty ((PdfDictionary) encodingObj);
fontPropList.add (prop);
}
}
if (fontType == F_TYPE0) {
// Encoding is reported as a CMapDictionary property for type 0
if (encodingObj != null && encodingObj instanceof PdfStream) {
prop = buildCMapDictProperty ((PdfStream) encodingObj);
fontPropList.add (prop);
}
}
if (fontType == F_TYPE3) {
// All we're interested in for Resources is whether
// the dictionary exists
PdfObject rsrc = dict.get ("Resources");
if (rsrc != null) {
prop = new Property ("Resources", PropertyType.BOOLEAN,
Boolean.TRUE);
fontPropList.add (prop);
}
}
if (fontType == F_TYPE0 || fontType == F_TYPE1 || fontType == F_TT ||
fontType == F_MM1 || fontType == F_TYPE3) {
PdfObject toUniObj = dict.get ("ToUnicode");
if (toUniObj != null) {
prop = new Property ("ToUnicode", PropertyType.BOOLEAN,
Boolean.TRUE);
fontPropList.add (prop);
}
}
return fontPropList;
}
/* Code for CMapProperty for Type 0 fonts, based on the Encoding
* entry, broken out of buildFontProperty.
*/
protected Property buildCMapDictProperty (PdfStream encoding)
{
PdfDictionary dict = encoding.getDict ();
List<Property> propList = new ArrayList<Property> (4);
Property prop = new Property ("CMapDictionary",
PropertyType.PROPERTY,
PropertyArity.LIST,
propList);
Property subprop;
//PdfObject mapName = dict.get ("CMapName");
PdfObject cidSysInfo = dict.get ("CIDSystemInfo");
// We can use buildCIDInfoProperty here to build the subproperty
PdfDictionary cidDict;
List<Property> cidList = new LinkedList<Property> ();
try {
if (cidSysInfo instanceof PdfDictionary) {
// One CIDInfo dictionary
cidDict = (PdfDictionary) cidSysInfo;
subprop = buildCIDInfoProperty (cidDict);
cidList.add (subprop);
}
else if (cidSysInfo instanceof PdfArray) {
// Many CIDInfo dictionaries
Vector<PdfObject> v = ((PdfArray) cidSysInfo).getContent ();
for (int i = 0; i < v.size (); i++) {
cidDict = (PdfDictionary) v.elementAt (i);
Property subsubprop = buildCIDInfoProperty (cidDict);
cidList.add (subsubprop);
}
}
}
catch (Exception e) {}
if (!cidList.isEmpty ()) {
subprop = new Property ("CIDSystemInfos",
PropertyType.PROPERTY,
PropertyArity.LIST,
cidList);
propList.add (subprop);
}
//PdfObject wMod = dict.get ("WMode");
//PdfObject useCMap = dict.get ("UseCMap");
return prop;
}
/* Code for CIDInfoProperty for CIDFontType0 and CIDFontType2
* conts.
*/
protected Property buildCIDInfoProperty (PdfDictionary dict)
{
List<Property> propList = new ArrayList<Property> (3);
Property prop = new Property ("CIDSystemInfo",
PropertyType.PROPERTY,
PropertyArity.LIST,
propList);
Property subprop;
// Add the registry identifier
PdfObject reg = dict.get ("Registry");
if (reg instanceof PdfSimpleObject) {
try {
String regText = ((PdfSimpleObject) reg).getStringValue ();
subprop = new Property ("Registry", PropertyType.STRING,
_encrypted ? ENCRYPTED : regText);
propList.add (subprop);
}
catch (Exception e) {}
}
// Add the name of the char collection within the registry
PdfObject order = dict.get ("Ordering");
if (reg instanceof PdfSimpleObject) {
try {
String ordText =
((PdfSimpleObject) order).getStringValue ();
subprop = new Property ("Registry",
PropertyType.STRING,
ordText);
propList.add (subprop);
}
catch (Exception e) {}
}
PdfObject supp = dict.get ("Supplement");
if (supp instanceof PdfSimpleObject) {
try {
int suppvalue = ((PdfSimpleObject) supp).getIntValue ();
subprop = new Property ("Supplement",
PropertyType.INTEGER,
new Integer (suppvalue));
propList.add (subprop);
}
catch (Exception e) {}
}
return prop;
}
/* Code for EncodingDictionary Property for type 1, 3, TrueType, and
* MM fonts. This is based on a dictionary entry with the same name
* as the one for buildCMapDictProperty, but different information.
* Included properties are BaseEncoding and Differences.
*/
protected Property buildEncodingDictProperty (PdfDictionary encodingDict)
{
List<Property> propList = new ArrayList<Property> (2);
Property prop = new Property ("EncodingDictionary",
PropertyType.PROPERTY,
PropertyArity.LIST,
propList);
PdfObject baseEnc = encodingDict.get ("BaseEncoding");
if (baseEnc instanceof PdfSimpleObject) {
String baseEncString = ((PdfSimpleObject) baseEnc).getStringValue ();
if (baseEncString != null) {
Property baseEncProp = new Property ("BaseEncoding",
PropertyType.STRING,
baseEncString);
propList.add (baseEncProp);
}
}
PdfObject diffs = encodingDict.get ("Differences");
Property diffsProp = new Property ("Differences",
PropertyType.BOOLEAN,
new Boolean (diffs != null));
propList.add (diffsProp);
return prop;
}
/* Separated-out code for FontDescriptor property. This
* is a list of six Properies: FontName, Flags,
* FontBBox, FontFile, FontFile2, and FontFile3.
*/
protected Property buildFontDescriptorProperty
(PdfDictionary encodingDict)
{
List<Property> propList = new ArrayList<Property> (6);
Property prop = new Property ("FontDescriptor",
PropertyType.PROPERTY,
PropertyArity.LIST,
propList);
Property subprop;
try {
PdfSimpleObject fName =
(PdfSimpleObject) encodingDict.get ("FontName");
String fNameStr = fName.getStringValue ();
subprop = new Property ("FontName",
PropertyType.STRING,
fNameStr);
propList.add (subprop);
}
catch (Exception e) {}
try {
PdfSimpleObject flags =
(PdfSimpleObject) encodingDict.get ("Flags");
int flagValue = flags.getIntValue ();
subprop = buildBitmaskProperty (flagValue,
"Flags",
PdfStrings.FONTDESCFLAGS,
"No flags set");
if (subprop != null) {
propList.add (subprop);
}
}
catch (Exception e) {}
try {
PdfArray bboxObj =
(PdfArray) encodingDict.get ("FontBBox");
double[] bbox = bboxObj.toRectangle ();
// toRectangle is written to return an array of double,
// which is what the bounding box is in the most general
// case; but the spec requires an array of integer, so
// we convert is. This may seem like an excess of work,
// but I'd rather have toRectangle do the right thing
// rather than losing generality.
if (bbox != null) {
int [] ibbox = new int[4];
for (int i = 0; i < 4; i++) {
ibbox[i] = (int) bbox[i];
}
subprop = new Property ("FontBBox",
PropertyType.INTEGER,
PropertyArity.ARRAY,
ibbox);
propList.add (subprop);
}
}
catch (Exception e) {}
PdfObject fontFile = encodingDict.get ("FontFile");
if (fontFile != null) {
// All we care about is whether it exists or not
subprop = new Property ("FontFile", PropertyType.BOOLEAN,
Boolean.TRUE);
propList.add (subprop);
}
fontFile = encodingDict.get ("FontFile2");
if (fontFile != null) {
subprop = new Property ("FontFile2", PropertyType.BOOLEAN,
Boolean.TRUE);
propList.add (subprop);
}
fontFile = encodingDict.get ("FontFile3");
if (fontFile != null) {
subprop = new Property ("FontFile3", PropertyType.BOOLEAN,
Boolean.TRUE);
propList.add (subprop);
}
return prop;
}
protected Property buildViewPrefProperty (PdfDictionary prefDict)
{
Property p;
PdfObject ob;
boolean b;
String s;
List<Property> propList = new ArrayList<Property> (12);
Property prop = new Property ("ViewerPreferences",
PropertyType.PROPERTY,
PropertyArity.LIST,
propList);
ob = prefDict.get("HideToolbar");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("HideToolbar", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("HideMenubar");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("HideMenubar", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("HideWindowUI");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("HideWindowUI", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("FitWindow");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("FitWindow", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("CenterWindow");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("CenterWindow", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("DisplayDocTitle");
if (ob instanceof PdfSimpleObject) {
b = ((PdfSimpleObject) ob).isTrue ();
}
else {
b = false;
}
p = new Property ("DisplayDocTitle", PropertyType.BOOLEAN, new Boolean (b));
propList.add (p);
ob = prefDict.get("NonFullScreenPageMode");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "UseNone";
p = new Property ("NonFullScreenPageMode", PropertyType.STRING, s);
propList.add (p);
ob = prefDict.get("Direction");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "L2R";
p = new Property ("Direction", PropertyType.STRING, s);
propList.add (p);
ob = prefDict.get("ViewArea");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "CropBox";
p = new Property ("ViewArea", PropertyType.STRING, s);
propList.add (p);
ob = prefDict.get("ViewClip");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "CropBox";
p = new Property ("ViewClip", PropertyType.STRING, s);
propList.add (p);
ob = prefDict.get("PrintArea");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "CropBox";
p = new Property ("PrintArea", PropertyType.STRING, s);
propList.add (p);
ob = prefDict.get("PageClip");
if (ob instanceof PdfSimpleObject) {
s = ((PdfSimpleObject) ob).getStringValue ();
}
else s = "CropBox";
p = new Property ("PageClip", PropertyType.STRING, s);
propList.add (p);
return prop;
}
/* Return TRUE if the string is a font subset string, which begins
with six uppercase letters and then a plus sign */
protected boolean isFontSubset (String baseStr)
{
if (baseStr == null || baseStr.length () < 7) {
return false;
}
for (int i = 0; i < 6; i++) {
char ch = baseStr.charAt (i);
if (!Character.isUpperCase (ch)) {
return false;
}
}
return (baseStr.charAt (6) == '+');
}
/* Create the "Outlines" property from the Outlines item in the
catalog dictionary. As a side effect, we set the actionsExist
flag if any Actions are found. Because we check destinations,
this can't be called till the page tree is built.
Outlines can be recursive, according to Adobe people, so we have
to track visited nodes.
*/
protected Property buildOutlinesProperty (PdfDictionary dict, RepInfo info)
throws PdfException
{
_recursionWarned = false;
_visitedOutlineNodes = new HashSet<Integer> ();
String malformed = "Malformed outline dictionary";
List<Property> itemList = new LinkedList<Property> ();
Property prop = new Property ("Outlines",
PropertyType.PROPERTY,
PropertyArity.LIST,
itemList);
try {
PdfObject item = resolveIndirectObject (dict.get ("First"));
// In PDF 1.4, "First" and "Last" are unconditionally required. However,
// in 1.6, they can be omitted if there are no open or closed outline items.
// Strictly speaking, we should do several additional checks, but letting the
// outline go as empty seems sufficient.
// if (item == null || !(item instanceof PdfDictionary)) {
// throw new PdfInvalidException ("Outline dictionary missing required entry");
// }
int listCount = 0; // Guard against looping
while (item != null) {
Integer onum = new Integer (item.getObjNumber ());
Property p = buildOutlineItemProperty ((PdfDictionary) item, info);
itemList.add (p);
item = resolveIndirectObject (((PdfDictionary) item).get ("Next"));
if (item == null) {
break;
}
// Check if this object is its own sibling. (It really does happen!)
if (item.getObjNumber() == onum.intValue ()) {
if (!_recursionWarned) {
info.setMessage (new InfoMessage
(outlinesRecursiveString));
_recursionWarned = true;
}
break;
}
if (++listCount > 2000) {
break;
}
}
}
catch (PdfException e1) {
throw e1;
}
catch (Exception e) {
throw new PdfMalformedException (malformed);
}
if (itemList.isEmpty ()) {
return null;
}
return prop;
}
/* Create an item property within the outlines hierarchy. If an
Outline item property has children, then there is a list
property called "Children" with elements called "Item".
It calls itself recursively to walk down the outline. */
protected Property buildOutlineItemProperty (PdfDictionary dict, RepInfo info)
throws PdfException
{
String invalid = "Invalid outline dictionary item";
List<Property> itemList = new ArrayList<Property> (3);
try {
Property prop = new Property ("Item",
PropertyType.PROPERTY,
PropertyArity.LIST,
itemList);
PdfSimpleObject title = (PdfSimpleObject)
resolveIndirectObject (dict.get ("Title"));
if (title == null) {
throw new PdfInvalidException (invalid);
}
itemList.add (new Property ("Title", PropertyType.STRING,
_encrypted ? ENCRYPTED :
title.getStringValue ()));
// Check other required stuff
if (dict.get ("Parent") == null) {
throw new PdfInvalidException (invalid);
}
PdfObject cnt = dict.get ("Count");
if (cnt != null &&
(!(cnt instanceof PdfSimpleObject) ||
!(((PdfSimpleObject) cnt).getToken () instanceof Numeric))) {
throw new PdfInvalidException (invalid);
}
// The entries for Prev, Next, First, and Last must
// all be indirect references or absent. Just cast them to
// throw an exception if they're something else
PdfIndirectObj ob = (PdfIndirectObj) dict.get ("Prev");
ob = (PdfIndirectObj) dict.get ("Next");
ob = (PdfIndirectObj) dict.get ("First");
ob = (PdfIndirectObj) dict.get ("First");
// Check if there are Actions in the outline. This saves going
// through the outlines all over again if a Profile checker
// needs to know this. We flag only the existence of one or more Actions
// in the document.
if (dict.get ("A") != null) {
_actionsExist = true;
}
PdfObject destObj = dict.get ("Dest");
if (destObj != null) {
destObj = resolveIndirectObject(destObj);
Destination dest = new Destination (destObj, this, false);
if (dest.isIndirect()) {
itemList.add (new Property ("Destination",
PropertyType.STRING,
dest.getIndirectDest ()));
}
else {
int pageObjNum = dest.getPageDestObjNumber ();
Integer destPg = (Integer)
_pageSeqMap.get (new Integer (pageObjNum));
if (destPg != null) {
itemList.add (new Property ("Destination",
PropertyType.INTEGER,
destPg));
}
}
}
PdfDictionary child =
(PdfDictionary) resolveIndirectObject (dict.get ("First"));
if (child != null) {
List<Property> childList = new LinkedList<Property> ();
Property childProp = new Property ("Children",
PropertyType.PROPERTY,
PropertyArity.LIST,
childList);
// We aren't catching all possible combinations of looping. Put a maximum
// on the list just to be safe.
int listCount = 0;
while (child != null) {
Integer onum = new Integer (child.getObjNumber ());
if (_visitedOutlineNodes.contains (onum)) {
/* We have recursion! */
if (!_recursionWarned) {
// Warn of recursion
info.setMessage (new InfoMessage
(outlinesRecursiveString));
_recursionWarned = true;
}
}
else {
_visitedOutlineNodes.add (onum);
Property p = buildOutlineItemProperty ((PdfDictionary) child, info);
childList.add (p);
}
child = (PdfDictionary)
resolveIndirectObject (child.get ("Next"));
if (child == null) {
break;
}
// Check if this object is its own sibling. (It really does happen!)
if (child.getObjNumber() == onum.intValue ()) {
if (!_recursionWarned) {
info.setMessage (new InfoMessage
(outlinesRecursiveString));
_recursionWarned = true;
}
break;
}
if (++listCount > 2000)
break; // safety check
}
itemList.add (childProp);
}
return prop;
}
catch (PdfException pe) {
throw pe;
}
catch (ClassCastException ce) {
throw new PdfInvalidException (invalid);
}
catch (Exception e) {
throw new PdfInvalidException (invalid);
}
}
/* This is separated out from readDocCatalogDict, where it
would otherwise make sense, because we can't build
the outlines property till we have a page tree to
locate destinations. */
protected boolean doOutlineStuff (RepInfo info)
{
if (_outlineDict != null) {
try {
Property oprop = buildOutlinesProperty
((PdfDictionary) _outlineDict, info);
if (_showOutlines || _verbosity == Module.MAXIMUM_VERBOSITY) {
if (oprop != null){
_docCatalogList.add (oprop);
}
}
else if (!_skippedOutlinesReported) {
// We report that we aren't reporting skipped outlines
info.setMessage (new InfoMessage
(outlinesSkippedString));
_skippedOutlinesReported = true;
}
}
catch (PdfException e) {
info.setMessage (new ErrorMessage (e.getMessage(),
_parser.getOffset ()));
e.disparage (info);
// If it's just invalid, we can keep going
return (e instanceof PdfInvalidException);
}
}
return true;
}
/* Given a PdfSimpleObject representing a key,
look up the Destination which it references.
There are two completely different ways this can be done,
though any given PDF file is supposed to implement only one.
If _destsDict is non-null, we look the string up there, and
may find either a dictionary or an array. Otherwise
if _destNames is non-null, it's a NameTreeNode which contains
the mapping. In either case, the destination could be
external, in which case we just return a string saying so.
(The implementation of Destinations in PDF is a prime example
of design by stone soup.)
We return the page sequence number for the referenced page.
If we can't find a match for the reference, we return -1.
*/
protected int resolveIndirectDest (PdfSimpleObject key)
throws PdfException
{
if (_destNames != null) {
Destination dest = new Destination (_destNames.get (key.getRawBytes ()),
this, true);
// if (dest == null) {
// return -1;
// }
return dest.getPageDestObjNumber ();
}
else {
return -1; // This is probably an error, actually
}
}
/* Build the user permission property., */
protected Property buildUserPermProperty (int flags, String[] flagStrs)
{
return buildBitmaskProperty (flags, "UserAccess", flagStrs,
"No permissions");
}
/** Add a string proprerty, based on a dictionary entry
with a string value, to a specified List. */
protected void addStringProperty(PdfDictionary dict,
List<Property> propList,
String key,
String propName)
{
String propText = null;
PdfObject propObject = dict.get (key);
if (propObject instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject)propObject).getToken ();
if (tok instanceof Literal) {
if (_encrypted) {
propText = ENCRYPTED;
}
else {
propText = ((Literal) tok).getValue ();
}
propList.add (new Property (propName,
PropertyType.STRING,
propText));
}
}
}
/** Add a date proprerty, based on a dictionary entry
with a string value, to a specified List. */
protected void addDateProperty(PdfDictionary dict,
List<Property> propList,
String key,
String propName)
throws PdfException
{
if (_encrypted) {
return; // can't decipher an encrypted date
}
PdfObject propObject = dict.get (key);
if (propObject instanceof PdfSimpleObject) {
Token tok = ((PdfSimpleObject)propObject).getToken ();
if (tok instanceof Literal) {
Date propDate = ((Literal) tok).parseDate ();
if (propDate != null) {
propList.add (new Property (propName,
PropertyType.DATE,
propDate));
}
else {
throw new PdfInvalidException ("Improperly formed date",
0);
}
}
}
}
/* General function for adding a property with a 32-bit
value, with an array of Strings to interpret
the value as a bitmask. */
protected Property buildBitmaskProperty (int val, String name,
String [] valueNames,
String defaultStr)
{
if (_je != null && _je.getShowRawFlag ()) {
return new Property (name,
PropertyType.INTEGER,
new Integer (val));
}
else {
List<String> slist = new LinkedList<String> ();
try {
for (int i = 0; i < valueNames.length; i++) {
if ((val & (1 << i)) != 0 &&
valueNames[i].length () > 0) {
slist.add (valueNames[i]);
}
}
// Provision for a default string if the property
// would otherwise have an empty list
if (slist.isEmpty() && defaultStr != null) {
slist.add (defaultStr);
}
}
catch (Exception e) {
return null;
}
return new Property (name, PropertyType.STRING,
PropertyArity.LIST, slist);
}
}
/* Take a PdfArray which is supposed to conform to the rectangle
description (i.e., it's an array of 4 numbers) and create
a Property which is an array of 4 integers. */
protected Property makeRectProperty (PdfArray arrObj, String name)
throws PdfException
{
int [] iarr = new int[4];
double[] arr = ((PdfArray) arrObj).toRectangle ();
// toRectangle is written to return an array of double,
// which is what the bounding box is in the most general
// case; but the spec requires an array of integer, so
// we convert is. This may seem like an excess of work,
// but I'd rather have toRectangle do the right thing
// rather than losing generality.
for (int i = 0; i < 4; i++) {
iarr[i] = (int) arr[i];
}
return new Property (name,
PropertyType.INTEGER,
PropertyArity.ARRAY,
iarr);
}
}