/**
* 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.Color;
import java.awt.image.*;
import java.awt.RenderingHints;
import java.awt.Transparency;
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 xmax = 0;
// backup default for size, on the large side.
private static final int DEFAULT_XMAX = 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
{
// get config params
float xmax = (float) ConfigurationManager
.getIntProperty("thumbnail.maxwidth");
float ymax = (float) ConfigurationManager
.getIntProperty("thumbnail.maxheight");
boolean blurring = (boolean) ConfigurationManager
.getBooleanProperty("thumbnail.blurring");
boolean hqscaling = (boolean) ConfigurationManager
.getBooleanProperty("thumbnail.hqscaling");
// 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.");
}
if (xmax == 0)
{
xmax = DEFAULT_XMAX;
}
}
// 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();
if (!outf.exists()) outf = new File(outPrefix+"-00001.ppm");
if (!outf.exists()) outf = new File(outPrefix+"-0001.ppm");
if (!outf.exists()) outf = new File(outPrefix+"-001.ppm");
if (!outf.exists()) outf = new File(outPrefix+"-01.ppm");
if (!outf.exists()) outf = new File(outPrefix+"-1.ppm");
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.");
}
// read in bitstream's image
BufferedImage buf = source;
// now get the image dimensions
float xsize = (float) buf.getWidth(null);
float ysize = (float) buf.getHeight(null);
// if verbose flag is set, print out dimensions
// to STDOUT
if (MediaFilterManager.isVerbose)
{
System.out.println("original size: " + xsize + "," + ysize);
}
// scale by x first if needed
if (xsize > xmax)
{
// calculate scaling factor so that xsize * scale = new size (max)
float scale_factor = xmax / xsize;
// if verbose flag is set, print out extracted text
// to STDOUT
if (MediaFilterManager.isVerbose)
{
System.out.println("x scale factor: " + scale_factor);
}
// now reduce x size
// and y size
xsize = xsize * scale_factor;
ysize = ysize * scale_factor;
// if verbose flag is set, print out extracted text
// to STDOUT
if (MediaFilterManager.isVerbose)
{
System.out.println("new size: " + xsize + "," + ysize);
}
}
// scale by y if needed
if (ysize > ymax)
{
float scale_factor = ymax / ysize;
// now reduce x size
// and y size
xsize = xsize * scale_factor;
ysize = ysize * scale_factor;
}
// if verbose flag is set, print details to STDOUT
if (MediaFilterManager.isVerbose)
{
System.out.println("created thumbnail size: " + xsize + ", "
+ ysize);
}
// create an image buffer for the thumbnail with the new xsize, ysize
BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize,
BufferedImage.TYPE_INT_RGB);
// Use blurring if selected in config.
// a little blur before scaling does wonders for keeping moire in check.
if (blurring)
{
// send the buffered image off to get blurred.
buf = getBlurredInstance((BufferedImage) buf);
}
// Use high quality scaling method if selected in config.
// this has a definite performance penalty.
if (hqscaling)
{
// send the buffered image off to get an HQ downscale.
buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize,
(Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true);
}
// now render the image into the thumbnail buffer
Graphics2D g2d = thumbnail.createGraphics();
g2d.drawImage(buf, 0, 0, (int) xsize, (int) ysize, null);
// now create an input stream for the thumbnail buffer and return it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(thumbnail, "jpeg", baos);
// now get the array
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return bais; // hope this gets written out before its garbage collected!
}
public String[] getInputMIMETypes()
{
return ImageIO.getReaderMIMETypes();
}
public String[] getInputDescriptions()
{
return null;
}
public String[] getInputExtensions()
{
// Temporarily disabled as JDK 1.6 only
// return ImageIO.getReaderFileSuffixes();
return null;
}
public BufferedImage getNormalizedInstance(BufferedImage buf)
{
int type = (buf.getTransparency() == Transparency.OPAQUE) ?
BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB_PRE;
int w, h;
w = buf.getWidth();
h = buf.getHeight();
BufferedImage normal = new BufferedImage(w, h, type);
Graphics2D g2d = normal.createGraphics();
g2d.drawImage(buf, 0, 0, w, h, Color.WHITE, null);
g2d.dispose();
return normal;
}
public BufferedImage getBlurredInstance(BufferedImage buf)
{
/**
* Convenience method that returns a blurred instance of the
* provided {@code BufferedImage}.
*
*/
buf = getNormalizedInstance(buf);
// kernel for blur op
float[] matrix = {
0.111f, 0.111f, 0.111f,
0.111f, 0.111f, 0.111f,
0.111f, 0.111f, 0.111f,
};
// perform the blur and return the blurred version.
BufferedImageOp blur = new ConvolveOp( new Kernel(3, 3, matrix) );
BufferedImage blurbuf = blur.filter(buf, null);
return blurbuf;
}
/**
* Convenience method that returns a scaled instance of the
* provided {@code BufferedImage}.
*
* @param buf the original image to be scaled
* @param targetWidth the desired width of the scaled instance,
* in pixels
* @param targetHeight the desired height of the scaled instance,
* in pixels
* @param hint one of the rendering hints that corresponds to
* {@code RenderingHints.KEY_INTERPOLATION} (e.g.
* {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
* {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
* {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
* @param higherQuality if true, this method will use a multi-step
* scaling technique that provides higher quality than the usual
* one-step technique (only useful in downscaling cases, where
* {@code targetWidth} or {@code targetHeight} is
* smaller than the original dimensions, and generally only when
* the {@code BILINEAR} hint is specified)
* @return a scaled version of the original {@code BufferedImage}
*/
public BufferedImage getScaledInstance(BufferedImage buf,
int targetWidth,
int targetHeight,
Object hint,
boolean higherQuality)
{
int type = (buf.getTransparency() == Transparency.OPAQUE) ?
BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
BufferedImage scalebuf = (BufferedImage)buf;
int w, h;
if (higherQuality) {
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = buf.getWidth();
h = buf.getHeight();
} else {
// Use one-step technique: scale directly from original
// size to target size with a single drawImage() call
w = targetWidth;
h = targetHeight;
}
do {
if (higherQuality && w > targetWidth) {
w /= 2;
if (w < targetWidth) {
w = targetWidth;
}
}
if (higherQuality && h > targetHeight) {
h /= 2;
if (h < targetHeight) {
h = targetHeight;
}
}
BufferedImage tmp = new BufferedImage(w, h, type);
Graphics2D g2d = tmp.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
g2d.drawImage(scalebuf, 0, 0, w, h, Color.WHITE, null);
g2d.dispose();
scalebuf = tmp;
} while (w != targetWidth || h != targetHeight);
return scalebuf;
}
}