/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.mediafilter;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.log4j.Logger;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Utils;
/**
* Thumbnail MediaFilter for PDF sources
*
* This filter generates thumbnail images for PDF documents, _including_
* 3D PDF documents with 2D "poster" images. Since the PDFBox library
* does not understand these, and fails to render a lot of other PDFs,
* this filter forks a process running the "pdftoppm" program from the
* XPdf suite -- see http://www.foolabs.com/xpdf/
* This is a suite of open-source PDF tools that has been widely ported
* to Unix platforms and the ones we use (pdftoppm, pdfinfo) even
* run on Win32.
*
* This was written for the FACADE project but it is not directly connected
* to any of the other FACADE-specific software. The FACADE UI expects
* to find thumbnail images for 3D PDFs generated by this filter.
*
* Requires DSpace config properties keys:
*
* xpdf.path.pdftoppm -- absolute path to "pdftoppm" executable (required!)
* xpdf.path.pdfinfo -- absolute path to "pdfinfo" executable (required!)
* thumbnail.maxwidth -- borrowed from thumbnails, max dim of generated image
*
* @author Larry Stone
* @see org.dspace.app.mediafilter.MediaFilter
*/
public class XPDF2Thumbnail extends MediaFilter
{
private static Logger log = Logger.getLogger(XPDF2Thumbnail.class);
// maximum size of either preview image dimension
private static final int MAX_PX = 800;
// maxium DPI - use common screen res, 100dpi.
private static final int MAX_DPI = 100;
// command to get image from PDF; @FILE@, @OUTPUT@ are placeholders
private static final String XPDF_PDFTOPPM_COMMAND[] =
{
"@COMMAND@", "-q", "-f", "1", "-l", "1",
"-r", "@DPI@", "@FILE@", "@OUTPUTFILE@"
};
// command to get image from PDF; @FILE@, @OUTPUT@ are placeholders
private static final String XPDF_PDFINFO_COMMAND[] =
{
"@COMMAND@", "-f", "1", "-l", "1", "-box", "@FILE@"
};
// executable path for "pdftoppm", comes from DSpace config at runtime.
private String pdftoppmPath = null;
// executable path for "pdfinfo", comes from DSpace config at runtime.
private String pdfinfoPath = null;
// match line in pdfinfo output that describes file's MediaBox
private static final Pattern MEDIABOX_PATT = Pattern.compile(
"^Page\\s+\\d+\\s+MediaBox:\\s+([\\.\\d-]+)\\s+([\\.\\d-]+)\\s+([\\.\\d-]+)\\s+([\\.\\d-]+)");
// also from thumbnail.maxwidth in config
private int maxwidth = 0;
// backup default for size, on the large side.
private static final int DEFAULT_MAXWIDTH = 500;
public String getFilteredName(String oldFilename)
{
return oldFilename + ".jpg";
}
public String getBundleName()
{
return "THUMBNAIL";
}
public String getFormatString()
{
return "JPEG";
}
public String getDescription()
{
return "Generated Thumbnail";
}
// canonical MediaFilter method to generate the thumbnail as stream.
public InputStream getDestinationStream(InputStream sourceStream)
throws Exception
{
// sanity check: xpdf paths are required. can cache since it won't change
if (pdftoppmPath == null || pdfinfoPath == null)
{
pdftoppmPath = ConfigurationManager.getProperty("xpdf.path.pdftoppm");
pdfinfoPath = ConfigurationManager.getProperty("xpdf.path.pdfinfo");
if (pdftoppmPath == null)
{
throw new IllegalStateException("No value for key \"xpdf.path.pdftoppm\" in DSpace configuration! Should be path to XPDF pdftoppm executable.");
}
if (pdfinfoPath == null)
{
throw new IllegalStateException("No value for key \"xpdf.path.pdfinfo\" in DSpace configuration! Should be path to XPDF pdfinfo executable.");
}
maxwidth = ConfigurationManager.getIntProperty("thumbnail.maxwidth");
if (maxwidth == 0)
{
maxwidth = DEFAULT_MAXWIDTH;
}
}
// make local file copy of source PDF since the PDF tools
// require a file for random access.
// XXX fixme would be nice to optimize this if we ever get
// XXX a DSpace method to access (optionally!) the _file_ of
// a Bitstream in the asset store, only when there is one of course.
File sourceTmp = File.createTempFile("DSfilt",".pdf");
sourceTmp.deleteOnExit();
int status = 0;
BufferedImage source = null;
try
{
OutputStream sto = new FileOutputStream(sourceTmp);
Utils.copy(sourceStream, sto);
sto.close();
sourceStream.close();
// First get max physical dim of bounding box of first page
// to compute the DPI to ask for.. otherwise some AutoCAD
// drawings can produce enormous files even at 75dpi, for
// 48" drawings..
// run pdfinfo, look for MediaBox description in the output, e.g.
// "Page 1 MediaBox: 0.00 0.00 612.00 792.00"
//
int dpi = 0;
String pdfinfoCmd[] = XPDF_PDFINFO_COMMAND.clone();
pdfinfoCmd[0] = pdfinfoPath;
pdfinfoCmd[pdfinfoCmd.length-1] = sourceTmp.toString();
BufferedReader lr = null;
try
{
MatchResult mediaBox = null;
Process pdfProc = Runtime.getRuntime().exec(pdfinfoCmd);
lr = new BufferedReader(new InputStreamReader(pdfProc.getInputStream()));
String line;
for (line = lr.readLine(); line != null; line = lr.readLine())
{
// if (line.matches(MEDIABOX_PATT))
Matcher mm = MEDIABOX_PATT.matcher(line);
if (mm.matches())
{
mediaBox = mm.toMatchResult();
}
}
int istatus = pdfProc.waitFor();
if (istatus != 0)
{
log.error("XPDF pdfinfo proc failed, exit status=" + istatus + ", file=" + sourceTmp);
}
if (mediaBox == null)
{
log.error("Sanity check: Did not find \"MediaBox\" line in output of XPDF pdfinfo, file="+sourceTmp);
throw new IllegalArgumentException("Failed to get MediaBox of PDF with pdfinfo, cannot compute thumbnail.");
}
else
{
double x0 = Double.parseDouble(mediaBox.group(1));
double y0 = Double.parseDouble(mediaBox.group(2));
double x1 = Double.parseDouble(mediaBox.group(3));
double y1 = Double.parseDouble(mediaBox.group(4));
int maxdim = (int)Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
dpi = Math.min(MAX_DPI, (MAX_PX * 72 / maxdim));
log.debug("DPI: pdfinfo method got dpi="+dpi+" for max dim="+maxdim+" (points, 1/72\")");
}
}
catch (InterruptedException e)
{
log.error("Failed transforming file for preview: ",e);
throw new IllegalArgumentException("Failed transforming file for thumbnail: ",e);
}
catch (NumberFormatException e)
{
log.error("Failed interpreting pdfinfo results, check regexp: ",e);
throw new IllegalArgumentException("Failed transforming file for thumbnail: ",e);
}
finally
{
if (lr != null)
{
lr.close();
}
}
// Render page 1 using xpdf's pdftoppm
// Requires Sun JAI imageio additions to read ppm directly.
// this will get "-000001.ppm" appended to it by pdftoppm
File outPrefixF = File.createTempFile("prevu","out");
String outPrefix = outPrefixF.toString();
if (!outPrefixF.delete())
{
log.error("Unable to delete output file");
}
String pdfCmd[] = XPDF_PDFTOPPM_COMMAND.clone();
pdfCmd[0] = pdftoppmPath;
pdfCmd[pdfCmd.length-3] = String.valueOf(dpi);
pdfCmd[pdfCmd.length-2] = sourceTmp.toString();
pdfCmd[pdfCmd.length-1] = outPrefix;
File outf = new File(outPrefix+"-000001.ppm");
log.debug("Running xpdf command: "+Arrays.deepToString(pdfCmd));
try
{
Process pdfProc = Runtime.getRuntime().exec(pdfCmd);
status = pdfProc.waitFor();
log.debug("PDFTOPPM output is: "+outf+", exists="+outf.exists());
source = ImageIO.read(outf);
}
catch (InterruptedException e)
{
log.error("Failed transforming file for preview: ",e);
throw new IllegalArgumentException("Failed transforming file for preview: ",e);
}
finally
{
if (!outf.delete())
{
log.error("Unable to delete file");
}
}
}
finally
{
if (!sourceTmp.delete())
{
log.error("Unable to delete temporary source");
}
if (status != 0)
{
log.error("PDF conversion proc failed, exit status=" + status + ", file=" + sourceTmp);
}
}
if (source == null)
{
throw new IOException("Unknown failure while transforming file to preview: no image produced.");
}
// Scale image and return in-memory stream
BufferedImage toenail = scaleImage(source, maxwidth*3/4, maxwidth);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(toenail, "jpeg", baos);
return new ByteArrayInputStream(baos.toByteArray());
}
// scale the image, preserving aspect ratio, if at least one
// dimension is not between min and max.
private static BufferedImage scaleImage(BufferedImage source,
int min, int max)
{
int xsize = source.getWidth(null);
int ysize = source.getHeight(null);
int msize = Math.max(xsize, ysize);
BufferedImage result = null;
// scale the image if it's outside of requested range.
// ALSO pass through if min and max are both 0
if ((min == 0 && max == 0) ||
(msize >= min && Math.min(xsize, ysize) <= max))
{
return source;
}
else
{
int xnew = xsize * max / msize;
int ynew = ysize * max / msize;
result = new BufferedImage(xnew, ynew, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = result.createGraphics();
g2d.drawImage(source, 0, 0, xnew, ynew, null);
return result;
}
}
}