/*
* Copyright (c) 2008 Los Alamos National Security, LLC.
* With modifications by Brasiliana Digital Library (http://brasiliana.usp.br), 2010.
*
* Los Alamos National Laboratory
* Research Library
* Digital Library Research & Prototyping Team
*
* This library 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.1 of the License, or (at your option) any later version.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package gov.lanl.adore.djatoka.kdu;
import gov.lanl.adore.djatoka.DjatokaDecodeParam;
import gov.lanl.adore.djatoka.DjatokaException;
import gov.lanl.adore.djatoka.IExtract;
import gov.lanl.adore.djatoka.io.reader.PNMReader;
import gov.lanl.adore.djatoka.util.IOUtils;
import gov.lanl.adore.djatoka.util.ImageProcessingUtils;
import gov.lanl.adore.djatoka.util.ImageRecord;
import gov.lanl.adore.djatoka.util.JP2ImageInfo;
import gov.lanl.util.ExecuteStreamHandler;
import gov.lanl.util.PumpStreamHandler;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.StringTokenizer;
import kdu_jni.Jp2_family_src;
import kdu_jni.Jpx_source;
import kdu_jni.KduException;
import kdu_jni.Kdu_codestream;
import kdu_jni.Kdu_coords;
import kdu_jni.Kdu_dims;
import kdu_jni.Kdu_global;
import kdu_jni.Kdu_params;
import org.apache.log4j.Logger;
import gov.lanl.util.ConfigurationManager;
/**
* Java bridge for kdu_expand application
* @author Ryan Chute
*
*/
public class KduExtractExe implements IExtract {
private static final Logger logger = Logger.getLogger(KduExtractExe.class);
private static boolean isWindows = false;
private static String env;
private static String exe;
private static String[] envParams;
private final static BufferedImage OOB = getOutOfBoundsImage();
/** Extract App Name "kdu_expand" */
public static final String KDU_EXPAND_EXE = "kdu_expand";
/** UNIX/Linux Standard Out Path: "/dev/stdout" */
public static String STDOUT = "/dev/stdout";
public static String STDIN = "/dev/stdin";
private static final String PROPS_KAKADU_HOME = "kakadu.home";
private static String kakaduHome;
static {
env = System.getProperty(KDU_EXPAND_EXE)
+ System.getProperty("file.separator");
exe = env
+ ((System.getProperty("os.name").contains("Win")) ? KDU_EXPAND_EXE
+ ".exe"
: KDU_EXPAND_EXE);
if (System.getProperty("os.name").startsWith("Mac")) {
envParams = new String[]{"DYLD_LIBRARY_PATH="
+ System.getProperty("DYLD_LIBRARY_PATH")};
} else if (System.getProperty("os.name").startsWith("Win")) {
isWindows = true;
} else if (System.getProperty("os.name").startsWith("Linux")) {
envParams = new String[]{"LD_LIBRARY_PATH="
+ System.getProperty("LD_LIBRARY_PATH")};
} else if (System.getProperty("os.name").startsWith("Solaris")) {
envParams = new String[]{"LD_LIBRARY_PATH="
+ System.getProperty("LD_LIBRARY_PATH")};
}
logger.debug("envParams: " + ((envParams != null) ? envParams[0] + " | " : "") + exe);
kakaduHome = System.getProperty(KDU_EXPAND_EXE);
logger.info("Setting " + KDU_EXPAND_EXE + ": " + System.getProperty(KDU_EXPAND_EXE));
}
public KduExtractExe() {
if (kakaduHome == null) {
logger.info("Trying to set kakadu.home.");
try {
kakaduHome = ConfigurationManager.getProperty(PROPS_KAKADU_HOME);
} catch (Exception e) {
logger.info(e, e);
kakaduHome = System.getProperty(PROPS_KAKADU_HOME);
}
}
logger.debug("conf.kakadu.home: " + kakaduHome);
env = kakaduHome + System.getProperty("file.separator");
exe = env
+ ((System.getProperty("os.name").contains("Win"))
? KDU_EXPAND_EXE + ".exe"
: KDU_EXPAND_EXE);
}
/**
* Extracts region defined in DjatokaDecodeParam as BufferedImage
* @param input InputStream containing a JPEG 2000 image bitstream.
* @param params DjatokaDecodeParam instance containing region and transform settings.
* @return extracted region as a BufferedImage
* @throws DjatokaException
*/
public BufferedImage processUsingTemp(InputStream input, DjatokaDecodeParam params)
throws DjatokaException {
File in;
// Copy to tmp file
try {
in = File.createTempFile("tmp", ".jp2");
FileOutputStream fos = new FileOutputStream(in);
in.deleteOnExit();
IOUtils.copyStream(input, fos);
} catch (IOException e) {
logger.error(e, e);
throw new DjatokaException(e);
}
BufferedImage bi = process(in.getAbsolutePath(), params);
if (in != null)
in.delete();
return bi;
}
/**
* Extracts region defined in DjatokaDecodeParam as BufferedImage
* @param is InputStream containing a JPEG 2000 image bitstream.
* @param params DjatokaDecodeParam instance containing region and transform settings.
* @return extracted region as a BufferedImage
* @throws DjatokaException
*/
public BufferedImage process(final InputStream is, DjatokaDecodeParam params) throws DjatokaException {
if (isWindows)
return processUsingTemp(is, params);
ArrayList<Double> dims = null;
if (params.getRegion() != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copyStream(is, baos);
dims = getRegionMetadata(new ByteArrayInputStream(baos.toByteArray()), params);
return process(new ByteArrayInputStream(baos.toByteArray()), dims, params);
} else
return process(is, dims, params);
}
/**
* Extracts region defined in DjatokaDecodeParam as BufferedImage
* @param is InputStream containing a JPEG 2000 image bitstream.
* @param dims region extraction dimensions
* @param params DjatokaDecodeParam instance containing region and transform settings.
* @return extracted region as a BufferedImage
* @throws DjatokaException
*/
public BufferedImage process(final InputStream is, ArrayList<Double> dims, DjatokaDecodeParam params) throws DjatokaException {
String input = STDIN;
String output = STDOUT;
BufferedImage bi = null;
try {
final String command = getKduExtractCommand(input, output, dims, params);
final Process process = Runtime.getRuntime().exec(command, envParams, new File(env));
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
ExecuteStreamHandler streamHandler = new PumpStreamHandler(stdout, stderr, is);
try {
streamHandler.setProcessInputStream(process.getOutputStream());
streamHandler.setProcessOutputStream(process.getInputStream());
streamHandler.setProcessErrorStream(process.getErrorStream());
} catch (IOException e) {
logger.error(e, e);
if (process != null) {
closeStreams(process);
}
throw e;
}
streamHandler.start();
try {
waitFor(process);
final ByteArrayInputStream bais = new ByteArrayInputStream(stdout.toByteArray());
bi = new PNMReader().open(bais);
streamHandler.stop();
} catch (ThreadDeath t) {
logger.error(t, t);
process.destroy();
throw t;
} finally {
if (process != null) {
closeStreams(process);
}
}
} catch (Exception e) {
logger.error(e, e);
throw new DjatokaException(e);
}
return bi;
}
/**
* Extracts region defined in DjatokaDecodeParam as BufferedImage
* @param input absolute file path of JPEG 2000 image file.
* @param params DjatokaDecodeParam instance containing region and transform settings.
* @return extracted region as a BufferedImage
* @throws DjatokaException
*/
public BufferedImage process(String input, DjatokaDecodeParam params)
throws DjatokaException {
String output = STDOUT;
File winOut = null;
BufferedImage bi = null;
if (isWindows) {
try {
winOut = File.createTempFile("pipe_", ".ppm");
winOut.deleteOnExit();
} catch (IOException e) {
logger.error(e, e);
throw new DjatokaException(e);
}
output = winOut.getAbsolutePath();
}
Runtime rt = Runtime.getRuntime();
try {
ArrayList<Double> dims = getRegionMetadata(input, params);
String command = getKduExtractCommand(input, output, dims, params);
final Process process = rt.exec(command, envParams, new File(env));
if (output != null) {
try {
if (output.equals(STDOUT)) {
bi = new PNMReader().open(new BufferedInputStream(process.getInputStream()));
} else if (isWindows) {
process.waitFor();
try {
bi = new PNMReader().open(new BufferedInputStream(new FileInputStream(new File(output))));
} catch (Exception e) {
logger.error(e, e);
if (winOut != null)
winOut.delete();
throw e;
}
if (winOut != null)
winOut.delete();
}
} catch (RuntimeException e) {
logger.debug("Request out of bounds");
bi = OOB;
} catch (Exception e) {
String error = null;
try {
error = new String(IOUtils.getByteArray(process.getErrorStream()));
} catch (Exception e1) {
e1.printStackTrace();
}
logger.error(error, e);
if (error != null)
throw new DjatokaException(error);
else
throw new DjatokaException(e);
} finally {
if (process != null) {
closeStreams(process);
}
}
}
} catch (IOException e) {
logger.error(e, e);
}
return bi;
}
/**
* Extracts region defined in DjatokaDecodeParam as BufferedImage
*
* @param input
* ImageRecord wrapper containing file reference, inputstream,
* etc.
* @param params
* DjatokaDecodeParam instance containing region and transform
* settings.
* @return extracted region as a BufferedImage
* @throws DjatokaException
*/
public BufferedImage process(ImageRecord input, DjatokaDecodeParam params)
throws DjatokaException {
if (input.getImageFile() != null) {
return process(input.getImageFile(), params);
} else if (input.getObject() != null) {
return process(getStreamFromObject(input.getObject()), params);
} else {
throw new DjatokaException(
"File not defined and Input Object Type "
+ input.getObject().getClass().getName()
+ " is not supported");
}
}
/**
* Gets Kdu Extract Command-line based on dims and params
* @param input absolute file path of JPEG 2000 image file.
* @param output absolute file path of PGM output image
* @param dims array of region parameters (i.e. y,x,h,w)
* @param params contains rotate and level extraction information
* @return command line string to extract region using kdu_extract
*/
public final String getKduExtractCommand(String input, String output,
ArrayList<Double> dims, DjatokaDecodeParam params) {
StringBuffer command = new StringBuffer(exe);
if (input.equals(STDIN))
command.append(" -no_seek");
command.append(" -quiet -i ").append(escape(new File(input).getAbsolutePath()));
command.append(" -o ").append(escape(new File(output).getAbsolutePath()));
command.append(" ").append(toKduExtractArgs(params));
if (dims != null && dims.size() == 4) {
StringBuffer region = new StringBuffer();
region.append("{").append(dims.get(0)).append(",").append(
dims.get(1)).append("}").append(",");
region.append("{").append(dims.get(2)).append(",").append(
dims.get(3)).append("}");
command.append("-region ").append(region.toString()).append(" ");
}
logger.debug(command.toString());
return command.toString();
}
/**
* Returns populated JPEG 2000 ImageRecord instance
* @param r ImageRecord containing file path the JPEG 2000 image
* @return a populated JPEG 2000 ImageRecord instance
* @throws DjatokaException
*/
public final ImageRecord getMetadata(ImageRecord r) throws DjatokaException {
if (r == null)
throw new DjatokaException("ImageRecord is null");
if (r.getImageFile() == null && r.getObject() != null) {
ImageRecord ir = getMetadata(getStreamFromObject(r.getObject()));
ir.setObject(r.getObject());
return ir;
}
File f = new File(r.getImageFile());
if (!f.exists())
throw new DjatokaException("Image Does Not Exist");
if (!ImageProcessingUtils.checkIfJp2(r.getImageFile()))
throw new DjatokaException("Not a JP2 image.");
if (f.length() <= 4096) {
// If < 4K bytes, image may be corrupt, use safer pure Java Metadata gatherer.
try {
return getMetadata(new FileInputStream(f));
} catch (Exception e) {
throw new DjatokaException("Invalid file.");
}
}
Jpx_source inputSource = new Jpx_source();
Jp2_family_src jp2_family_in = new Jp2_family_src();
int ref_component = 0;
try {
jp2_family_in.Open(r.getImageFile(), true);
inputSource.Open(jp2_family_in, true);
Kdu_codestream codestream = new Kdu_codestream();
codestream.Create(inputSource.Access_codestream(ref_component).Open_stream());
int minLevels = codestream.Get_min_dwt_levels();
int depth = codestream.Get_bit_depth(ref_component);
int colors = codestream.Get_num_components();
int[] frames = new int[1];
inputSource.Count_compositing_layers(frames);
Kdu_dims image_dims = new Kdu_dims();
codestream.Get_dims(ref_component, image_dims);
Kdu_coords imageSize = image_dims.Access_size();
r.setWidth(imageSize.Get_x());
r.setHeight(imageSize.Get_y());
r.setDWTLevels(minLevels);
int djatokaLevels = ImageProcessingUtils.getLevelCount(r.getWidth(), r.getHeight());
r.setLevels((djatokaLevels > minLevels) ? minLevels : djatokaLevels);
r.setBitDepth(depth);
r.setNumChannels(colors);
r.setCompositingLayerCount(frames[0]);
int[] v = new int[1];
Kdu_params p = codestream.Access_siz().Access_cluster("COD");
if (p != null) {
p.Get(Kdu_global.Clayers, 0, 0, v, true, true, true);
if (v[0] > 0)
r.setQualityLayers(v[0]);
}
if (codestream.Exists())
codestream.Destroy();
inputSource.Native_destroy();
jp2_family_in.Native_destroy();
} catch (KduException e) {
logger.error(e, e);
throw new DjatokaException(e);
}
return r;
}
/**
* Returns populated JPEG 2000 ImageRecord instance
* @param is an InputStream containing the JPEG 2000 codestream
* @return a populated JPEG 2000 ImageRecord instance
* @throws DjatokaException
*
*/
public final ImageRecord getMetadata(final InputStream is) throws DjatokaException {
JP2ImageInfo info;
try {
info = new JP2ImageInfo(is);
} catch (IOException e) {
logger.error(e, e);
throw new DjatokaException(e);
}
return info.getImageRecord();
}
/**
* Returns array of XMLBox records contained in JP2 resource.
* @param r an ImageRecord containing a file path to resource or has object defined
* @return an array of XML records contained in JP2 XMLboxes
*/
public final String[] getXMLBox(ImageRecord r) throws DjatokaException {
String[] xml = null;
try {
if (r.getImageFile() == null && r.getObject() != null) {
xml = new JP2ImageInfo(getStreamFromObject(r.getObject())).getXmlDocs();
} else {
xml = new JP2ImageInfo(new File(r.getImageFile())).getXmlDocs();
}
} catch (IOException e) {
logger.error(e, e);
}
return xml;
}
/**
* Utility method to determine type of object stored in ImageRecord
* and to return it as an InputStream
* @param o
* @return an InputStream for the resource contained in ImageRecord object
*/
public static InputStream getStreamFromObject(Object o) {
if (o instanceof BufferedInputStream)
return (InputStream) o;
if (o instanceof InputStream)
return new BufferedInputStream((InputStream) o);
if (o instanceof byte[])
return new ByteArrayInputStream((byte[]) o);
logger.error(o.getClass().getName() + " is not a supported ImageRecord object type.");
return null;
}
private final ArrayList<Double> getRegionMetadata(InputStream input,
DjatokaDecodeParam params) throws DjatokaException {
ImageRecord r = getMetadata(input);
return getRegionMetadata(r, params);
}
private final ArrayList<Double> getRegionMetadata(String input,
DjatokaDecodeParam params) throws DjatokaException {
ImageRecord r = getMetadata(new ImageRecord(input));
return getRegionMetadata(r, params);
}
private final ArrayList<Double> getRegionMetadata(ImageRecord r, DjatokaDecodeParam params)
throws DjatokaException {
if (params.getLevel() >= 0) {
int levels = ImageProcessingUtils.getLevelCount(r.getWidth(), r.getHeight());
levels = (r.getDWTLevels() < levels) ? r.getDWTLevels() : levels;
int reduce = levels - params.getLevel();
params.setLevelReductionFactor((reduce >= 0) ? reduce : 0);
} else if (params.getLevel() == -1 && params.getRegion() == null && params.getScalingDimensions() != null) {
int width = params.getScalingDimensions()[0];
int height = params.getScalingDimensions()[1];
int levels = ImageProcessingUtils.getLevelCount(r.getWidth(), r.getHeight());
int scale_level = ImageProcessingUtils.getScalingLevel(r.getWidth(), r.getHeight(), width, height);
levels = (r.getDWTLevels() < levels) ? r.getDWTLevels() : levels;
int reduce = levels - scale_level;
System.out.println(reduce);
params.setLevelReductionFactor((reduce >= 0) ? reduce : 0);
}
int reduce = 1 << params.getLevelReductionFactor();
ArrayList<Double> dims = new ArrayList<Double>();
if (params.getRegion() != null) {
StringTokenizer st = new StringTokenizer(params.getRegion(), "{},");
String token;
// top
if ((token = st.nextToken()).contains("."))
dims.add(Double.parseDouble(token));
else {
int t = Integer.parseInt(token);
if (r.getHeight() < t)
throw new DjatokaException("Region inset out of bounds: " + t + ">" + r.getHeight());
dims.add(Double.parseDouble(token) / r.getHeight());
}
// left
if ((token = st.nextToken()).contains(".")) {
dims.add(Double.parseDouble(token));
} else {
int t = Integer.parseInt(token);
if (r.getWidth() < t)
throw new DjatokaException("Region inset out of bounds: " + t + ">" + r.getWidth());
dims.add(Double.parseDouble(token) / r.getWidth());
}
// height
if ((token = st.nextToken()).contains(".")) {
dims.add(Double.parseDouble(token));
} else
dims.add(Double.parseDouble(token)
/ (Double.valueOf(r.getHeight()) / Double
.valueOf(reduce)));
// width
if ((token = st.nextToken()).contains(".")) {
dims.add(Double.parseDouble(token));
} else
dims.add(Double.parseDouble(token)
/ (Double.valueOf(r.getWidth()) / Double
.valueOf(reduce)));
}
return dims;
}
private static BufferedImage getOutOfBoundsImage() {
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
int rgb = bi.getRGB(0, 0);
int alpha = (rgb >> 24) & 0xff;
bi.setRGB(0, 0, alpha);
return bi;
}
private static String toKduExtractArgs(DjatokaDecodeParam params) {
StringBuffer sb = new StringBuffer();
if (params.getLevelReductionFactor() > 0)
sb.append("-reduce ").append(params.getLevelReductionFactor()).append(" ");
if (params.getRotationDegree() > 0)
sb.append("-rotate ").append(params.getRotationDegree()).append(" ");
if (params.getCompositingLayer() > 0)
sb.append("-jpx_layer ").append(params.getCompositingLayer()).append(" ");
return sb.toString();
}
private static final String escape(String path) {
if (path.contains(" "))
path = "\"" + path + "\"";
return path;
}
// Process Handler Utils
private int waitFor(Process process) {
try {
process.waitFor();
return process.exitValue();
} catch (InterruptedException e) {
process.destroy();
}
return 2;
}
private static void closeStreams(Process process) {
close(process.getInputStream());
close(process.getOutputStream());
close(process.getErrorStream());
process.destroy();
}
private static void close(InputStream device) {
if (device != null) {
try {
device.close();
} catch (IOException ioex) {
}
}
}
private static void close(OutputStream device) {
if (device != null) {
try {
device.close();
} catch (IOException ioex) {
}
}
}
}