/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package uk.bl.wa.tika.parser.pdf.itext;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.File;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import org.apache.jempbox.xmp.XMPMetadata;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AbstractParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.image.xmp.JempboxExtractor;
import org.apache.tika.sax.XHTMLContentHandler;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import uk.bl.wa.tika.parser.pdf.XMPSchemaPDFA;
import com.itextpdf.text.pdf.PdfDictionary;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.PdfTextExtractor;
/**
* PDF parser.
* <p>
* This parser can process also encrypted PDF documents if the required password is given as a part of the input metadata associated with a document. If no password is given, then this parser will try decrypting the document using the empty
* password that's often used with PDFs.
*
* FIXME Note that 'creator' as in 'author' overwrites the 'pdf:creator' (as in sofware app) somehow.
*
* @author Roger Coram, Andrew Jackson <Andrew.Jackson@bl.uk>
*/
public class PDFParser extends AbstractParser {
/** Serial version UID */
private static final long serialVersionUID = -752276948656079347L;
/**
* Metadata key for giving the document password to the parser.
*
* @since Apache Tika 0.5
*/
public static final String PASSWORD = "org.apache.tika.parser.pdf.password";
private static final Set<MediaType> SUPPORTED_TYPES = Collections.singleton( MediaType.application( "pdf" ) );
public Set<MediaType> getSupportedTypes( ParseContext context ) {
return SUPPORTED_TYPES;
}
public static void main( String[] args ) {
try {
FileInputStream input = new FileInputStream( new File( "src/test/resources/simple-PDFA-1a.pdf" ) );
OutputStream output = System.out; //new FileOutputStream( new File( "Z:/part-00001.xml" ) );
PdfReader reader = new PdfReader( input );
StringBuilder builder = new StringBuilder();
Metadata metadata = new Metadata();
PDFParser.extractMetadata( reader, metadata );
builder.append( "<?xml version=\"1.0\" encoding=\"UTF-8\"?><wctdocs><![CDATA[" );
builder.append( PDFParser.extractText( reader ) );
builder.append( "]]></wctdocs>\n" );
input.close();
output.write( builder.toString().getBytes( "UTF-8" ) );
for( String key : metadata.names() ) {
output.write( (key+" : "+metadata.get(key)+"\n").getBytes( "UTF-8" ) );
}
output.close();
} catch( Exception e ) {
e.printStackTrace();
}
}
public PDFParser() {}
public void parse( InputStream stream, ContentHandler handler, Metadata metadata, ParseContext context ) throws IOException, SAXException, TikaException {
PdfReader reader = new PdfReader( stream );
PDFParser.extractMetadata( reader, metadata );
XHTMLContentHandler xhtml = new XHTMLContentHandler( handler, metadata );
xhtml.startDocument();
xhtml.startElement( "p" );
xhtml.characters( new String( PDFParser.extractText( reader ).getBytes( "UTF-8" ), "UTF-8" ) );
xhtml.endElement( "p" );
xhtml.endDocument();
}
private static String extractText( PdfReader reader ) {
StringBuilder output = new StringBuilder();
try {
int numPages = reader.getNumberOfPages();
int page = 1;
while( page <= numPages ) {
output.append( PdfTextExtractor.getTextFromPage( reader, page ) );
page++;
}
} catch( Exception e ) {
System.err.println( "PDFParser.extractText(): " + e.getMessage() );
}
return output.toString();
}
private static void extractMetadata( PdfReader reader, Metadata metadata ) {
try {
HashMap<String, String> map = reader.getInfo();
// Clone the PDF info:
for( String key : map.keySet() ) {
metadata.set( key.toLowerCase(), map.get( key ) );
}
// Add other data of interest:
metadata.set("pdf:version", "1."+reader.getPdfVersion());
metadata.set("pdf:numPages", ""+reader.getNumberOfPages());
metadata.set("pdf:cryptoMode", ""+getCryptoModeAsString(reader));
metadata.set("pdf:openedWithFullPermissions", ""+reader.isOpenedWithFullPermissions());
metadata.set("pdf:encrypted", ""+reader.isEncrypted());
metadata.set("pdf:metadataEncrypted", ""+reader.isMetadataEncrypted());
metadata.set("pdf:128key", ""+reader.is128Key());
metadata.set("pdf:tampered", ""+reader.isTampered());
// Also grap XMP metadata, if present:
byte[] xmpmd = reader.getMetadata();
if( xmpmd != null ) {
// This is standard Tika code for parsing standard stuff from the XMP:
JempboxExtractor extractor = new JempboxExtractor(metadata);
extractor.parse( new ByteArrayInputStream( xmpmd ) );
// This is custom XMP-handling code:
XMPMetadata xmp = XMPMetadata.load( new ByteArrayInputStream( xmpmd ) );
// There is a special class for grabbing data in the PDF schema - not sure it will add much here:
// Could parse xmp:CreatorTool and pdf:Producer etc. etc. out of here.
//XMPSchemaPDF pdfxmp = xmp.getPDFSchema();
// Added a PDF/A schema class:
xmp.addXMLNSMapping(XMPSchemaPDFA.NAMESPACE, XMPSchemaPDFA.class);
XMPSchemaPDFA pdfaxmp = (XMPSchemaPDFA) xmp.getSchemaByClass(XMPSchemaPDFA.class);
if( pdfaxmp != null ) {
metadata.set("pdfaid:part", pdfaxmp.getPart());
metadata.set("pdfaid:conformance", pdfaxmp.getConformance());
String version = "A-"+pdfaxmp.getPart()+pdfaxmp.getConformance().toLowerCase();
//metadata.set("pdfa:version", version );
metadata.set("pdf:version", version );
}
}
// Attempt to determine Adobe extension level:
PdfDictionary extensions = reader.getCatalog().getAsDict(PdfName.EXTENSIONS);
if( extensions != null ) {
PdfDictionary adobeExt = extensions.getAsDict(PdfName.ADBE);
if( adobeExt != null ) {
PdfName baseVersion = adobeExt.getAsName(PdfName.BASEVERSION);
int el = adobeExt.getAsNumber(PdfName.EXTENSIONLEVEL).intValue();
metadata.set("pdf:version", baseVersion.toString().substring(1)+" Adobe Extension Level "+el );
}
}
// Ensure the normalised metadata are mapped in:
if( map.get( "Title" ) != null )
metadata.set( Metadata.TITLE, map.get( "Title" ) );
if( map.get( "Author" ) != null )
metadata.set( Metadata.AUTHOR, map.get( "Author" ) );
} catch( Exception e ) {
System.err.println( "PDFParser.extractMetadata() caught Exception: " + e.getMessage() );
e.printStackTrace();
}
}
private static String getCryptoModeAsString( PdfReader reader ) {
int mode = reader.getCryptoMode();
// TODO Make this into a more readable string, but tricky as it's a bitmask.
// Need to use FLAG_A|FLAG_B output syntax, as strace does.
// @see com.itextpdf.text.pdf.PDFEncryption.setCryptoMode(int mode, int kl);
return ""+mode;
}
/**
* @deprecated This method will be removed in Apache Tika 1.0.
*/
public void parse( InputStream stream, ContentHandler handler, Metadata metadata ) throws IOException, SAXException, TikaException {
parse( stream, handler, metadata, new ParseContext() );
}
}