/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.utils;
import com.lightcrafts.image.BadImageFileException;
import com.lightcrafts.image.UnknownImageTypeException;
import com.lightcrafts.image.libs.LCJPEGReader;
import com.lightcrafts.image.libs.LCTIFFReader;
import com.lightcrafts.image.metadata.providers.*;
import com.lightcrafts.image.metadata.MetadataUtil;
import com.lightcrafts.jai.JAIContext;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.Date;
import java.util.Map;
import sun.nio.ch.DirectBuffer;
/**
* Get raw image data by inrerfacing with dcraw as a coprocess.
*
* @author Fabio Riccardi [fabio@lightcrafts.com]
*/
public final class DCRaw implements
ApertureProvider, CaptureDateTimeProvider, FocalLengthProvider,
ISOProvider, MakeModelProvider, ShutterSpeedProvider, WidthHeightProvider {
////////// public ////////////////////////////////////////////////////////
public static final class DCRawException extends LightCraftsException {
public final int code;
DCRawException(int code) {
super("DCRaw Error: " + code);
this.code = code;
}
}
static private Map<String,DCRaw> dcrawCache =
new LRUHashMap<String,DCRaw>(100);
public static synchronized DCRaw getInstanceFor( String fileName ) {
DCRaw instance = dcrawCache.get(fileName);
if (instance == null) {
instance = new DCRaw(fileName);
dcrawCache.put(fileName, instance);
}
return instance;
}
/**
* Construct a <code>DCRaw</code> object.
*
* @param fileName The full path of the raw image file.
*/
private DCRaw( String fileName ) {
m_fileName = fileName;
try {
runDCRawInfo(false);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* {@inheritDoc}
*/
public float getAperture() {
return m_aperture;
}
/**
* {@inheritDoc}
*/
public String getCameraMake( boolean includeModel ) {
String make = getMake();
final String model = getModel();
if ( make == null || model == null )
return null;
make = make.toUpperCase();
if ( !includeModel )
return make;
return MetadataUtil.undupMakeModel( make, model.toUpperCase() );
}
public float[] getCameraMultipliers() {
return m_cam_mul.clone();
}
public float[] getSecondaryCameraMultipliers() {
return m_secondary_cam_mul.clone();
}
public float[][] getCameraRGB() {
if ( m_rgb_cam == null )
return null;
return new float[][] {
{ m_rgb_cam[0], m_rgb_cam[1], m_rgb_cam[2] },
{ m_rgb_cam[3], m_rgb_cam[4], m_rgb_cam[5] },
{ m_rgb_cam[6], m_rgb_cam[7], m_rgb_cam[8] }
};
}
public float[][] getCameraXYZ() {
if ( m_xyz_cam == null )
return null;
return new float[][] {
{ m_xyz_cam[0], m_xyz_cam[1], m_xyz_cam[2] },
{ m_xyz_cam[3], m_xyz_cam[4], m_xyz_cam[5] },
{ m_xyz_cam[6], m_xyz_cam[7], m_xyz_cam[8] }
};
}
/**
* {@inheritDoc}
*/
public Date getCaptureDateTime() {
return m_captureDateTime > 0 ?
new Date( m_captureDateTime * 1000 ) : null;
}
public float[] getDaylightMultipliers() {
return m_pre_mul.clone();
}
/**
* {@inheritDoc}
*/
public float getFocalLength() {
return m_focalLength;
}
/**
* {@inheritDoc}
*/
public int getImageHeight() {
return m_height;
}
/**
* {@inheritDoc}
*/
public int getImageWidth() {
return m_width;
}
/**
* {@inheritDoc}
*/
public int getFullHeight() {
return m_fullHeight;
}
/**
* {@inheritDoc}
*/
public int getFullWidth() {
return m_fullWidth;
}
/**
* {@inheritDoc}
*/
public int getRawHeight() {
return m_rawHeight;
}
/**
* {@inheritDoc}
*/
public int getRawWidth() {
return m_rawWidth;
}
/**
* {@inheritDoc}
*/
public int getThumbHeight() {
return m_thumbHeight;
}
/**
* {@inheritDoc}
*/
public int getThumbWidth() {
return m_thumbWidth;
}
/**
* {@inheritDoc}
*/
public int getISO() {
return m_iso;
}
/**
* {@inheritDoc}
*/
public String getMake() {
return m_make;
}
/**
* {@inheritDoc}
*/
public String getModel() {
return m_model;
}
private static String readln(InputStream s) {
try {
int c;
StringBuffer sb = new StringBuffer();
while ((c = s.read()) > 0 && c != 255 && (c == '\n' || c == '\r'))
; // Skip carriage returns and line feeds
while (c > 0 && c != 255 && c != '\n' && c != '\r') {
sb.append((char) c);
c = s.read();
}
if ((c == -1 || c == 255) && sb.length() == 0)
return null;
return new String(sb);
} catch (IOException e) {
return null;
}
}
// TODO: we need a more robust parsing mechanism here...
private final static String FILENAME = "Filename: ";
private final static String TIMESTAMP = "Timestamp: ";
private final static String CAMERA = "Camera: ";
private final static String ISO = "ISO speed: ";
private final static String SHUTTER = "Shutter: ";
private final static String APERTURE = "Aperture: ";
private final static String FOCAL_LENGTH = "Focal Length: ";
private final static String NUM_RAW_IMAGES = "Number of raw images: ";
private final static String EMBEDDED_ICC_PROFILE = "Embedded ICC profile: ";
private final static String CANNOT_DECODE = "Cannot decode file";
private final static String THUMB_SIZE = "Thumb size: ";
private final static String FULL_SIZE = "Full size: ";
private final static String IMAGE_SIZE = "Image size: ";
private final static String OUTPUT_SIZE = "Output size: ";
private final static String RAW_COLORS = "Raw colors: ";
private final static String FILTER_PATTERN = "Filter pattern: ";
private final static String DAYLIGHT_MULTIPLIERS = "Daylight multipliers: ";
private final static String CAMERA_MULTIPLIERS = "Camera multipliers: ";
private final static String CAMERA_RGB_PROFILE = "Camera RGB Profile: ";
private final static String CAMERA_XYZ_PROFILE = "Camera XYZ Profile: ";
private static String DCRAW_PATH = "./dcraw";
static {
String appDir = System.getProperty("install4j.appDir");
if (appDir != null) {
DCRAW_PATH = appDir + "/dcraw";
}
}
private static String match(String s, String tag) {
if (s.startsWith(tag)) {
s = s.substring(tag.length());
int index = 0;
while(s.charAt(index) == ' ')
index++;
if (index > 0)
s = s.substring(index);
return s;
}
return null;
}
private void runDCRawInfo(boolean secondary) throws IOException {
String info[] = { DCRAW_PATH, "-v", "-i", "-t", "0", m_fileName };
String secondaryInfo[] = { DCRAW_PATH, "-v", "-i", "-s", "1", "-t", "0", m_fileName };
synchronized (DCRaw.class) {
Process p = null;
InputStream dcrawStdOut;
InputStream dcrawStdErr;
if (ForkDaemon.INSTANCE != null) {
ForkDaemon.INSTANCE.invoke(secondary ? secondaryInfo : info);
dcrawStdOut = ForkDaemon.INSTANCE.getStdOut();
dcrawStdErr = ForkDaemon.INSTANCE.getStdErr();
} else {
p = Runtime.getRuntime().exec(secondary ? secondaryInfo : info);
dcrawStdOut = p.getInputStream();
dcrawStdErr = new BufferedInputStream(p.getErrorStream());
}
// output expected on stdout
String line, args;
while ((line = readln(dcrawStdOut)) != null) {
// System.out.println(line);
String search;
if (secondary) {
if (line.startsWith(search = CAMERA_MULTIPLIERS)) {
String multipliers[] = line.substring(search.length()).split("\\s");
m_secondary_cam_mul[0] = Float.parseFloat(multipliers[0]);
m_secondary_cam_mul[1] = Float.parseFloat(multipliers[1]);
m_secondary_cam_mul[2] = Float.parseFloat(multipliers[2]);
m_secondary_cam_mul[3] = Float.parseFloat(multipliers[3]);
}
} else {
if (line.startsWith(search = FILENAME)) {
String filename = line.substring(search.length());
} else if (line.startsWith(search = TIMESTAMP)) {
String timestamp = line.substring(search.length());
m_captureDateTime = new Date(timestamp).getTime();
} else if (line.startsWith(search = CAMERA)) {
String camera = line.substring(search.length());
m_make = camera.substring(0, camera.indexOf(' '));
m_model = camera.substring(m_make.length() + 1);
} else if (line.startsWith(search = ISO)) {
String iso = line.substring(search.length());
m_iso = Integer.decode(iso);
} else if (line.startsWith(search = SHUTTER)) {
String shutterSpeed = line.substring(search.length() + 2);
float exposureTime = 0;
try {
exposureTime = Float.valueOf(shutterSpeed.substring(0, shutterSpeed.indexOf(" sec")));
if (exposureTime != 0)
m_shutterSpeed = 1 / exposureTime;
} catch (NumberFormatException e) { }
} else if (line.startsWith(search = APERTURE)) {
String aperture = line.substring(search.length() + 2);
try {
m_aperture = Float.valueOf(aperture);
} catch (NumberFormatException e) { }
} else if (line.startsWith(search = FOCAL_LENGTH)) {
String focalLenght = line.substring(search.length());
try {
m_focalLength = Float.valueOf(focalLenght.substring(0, focalLenght.indexOf(" mm")));
} catch (NumberFormatException e) { }
} else if (line.startsWith(search = NUM_RAW_IMAGES)) {
String numRawImages = line.substring(search.length());
m_secondaryPixels = numRawImages.startsWith("2");
} else if (line.startsWith(search = EMBEDDED_ICC_PROFILE)) {
String embeddedICCProfile = line.substring(search.length());
} else if (line.startsWith(CANNOT_DECODE)) {
m_decodable = false;
} else if ((args = match(line, THUMB_SIZE)) != null) {
String sizes[] = args.split(" x ");
m_thumbWidth = Integer.decode(sizes[0]);
m_thumbHeight = Integer.decode(sizes[1]);
} else if ((args = match(line, FULL_SIZE)) != null) {
String sizes[] = args.split(" x ");
m_fullWidth = Integer.decode(sizes[0]);
m_fullHeight = Integer.decode(sizes[1]);
} else if ((args = match(line, IMAGE_SIZE)) != null) {
String sizes[] = args.split(" x ");
m_rawWidth = Integer.decode(sizes[0]);
m_rawHeight = Integer.decode(sizes[1]);
} else if ((args = match(line, OUTPUT_SIZE)) != null) {
String sizes[] = args.split(" x ");
m_width = Integer.decode(sizes[0]);
m_height = Integer.decode(sizes[1]);
} else if (line.startsWith(search = RAW_COLORS)) {
String rawColors = line.substring(search.length());
m_rawColors = Integer.decode(rawColors);
} else if (line.startsWith(search = FILTER_PATTERN)) {
String pattern = line.substring(search.length());
if (pattern.startsWith("BGGR"))
m_filters = 0x16161616;
else if (pattern.startsWith("GRBG"))
m_filters = 0x61616161;
else if (pattern.startsWith("GBRG"))
m_filters = 0x49494949;
else if (pattern.startsWith("RGGB"))
m_filters = 0x94949494;
else
m_filters = -1;
} else if (line.startsWith(search = DAYLIGHT_MULTIPLIERS)) {
String multipliers[] = line.substring(search.length()).split("\\s");
m_pre_mul[0] = Float.parseFloat(multipliers[0]);
m_pre_mul[1] = Float.parseFloat(multipliers[1]);
m_pre_mul[2] = Float.parseFloat(multipliers[2]);
m_pre_mul[3] = m_pre_mul[1];
} else if (line.startsWith(search = CAMERA_MULTIPLIERS)) {
String multipliers[] = line.substring(search.length()).split("\\s");
m_cam_mul[0] = Float.parseFloat(multipliers[0]);
m_cam_mul[1] = Float.parseFloat(multipliers[1]);
m_cam_mul[2] = Float.parseFloat(multipliers[2]);
m_cam_mul[3] = Float.parseFloat(multipliers[3]);
} else if (line.startsWith(CAMERA_RGB_PROFILE)) {
String rgb_cam[] = line.substring(CAMERA_RGB_PROFILE.length()).split("\\s");
m_rgb_cam = new float[9];
for (int i = 0; i < 9; i++) {
m_rgb_cam[i] = Float.parseFloat(rgb_cam[i]);
}
} else if (line.startsWith(CAMERA_XYZ_PROFILE)) {
String xyz_cam[] = line.substring(CAMERA_XYZ_PROFILE.length()).split("\\s");
m_xyz_cam = new float[9];
for (int i = 0; i < 9; i++) {
m_xyz_cam[i] = Float.parseFloat(xyz_cam[i]);
}
}
}
}
// Flush stderr just in case...
while ((line = readln(dcrawStdErr)) != null)
; // System.out.println(line);
if (p != null) {
dcrawStdOut.close();
try {
p.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
m_error = p.exitValue();
p.destroy();
} else
m_error = 0;
}
}
private static class ImageData {
final int width, height, bands, dataType;
final Object data;
ImageData(int width, int height, int bands, int dataType) {
this.width = width;
this.height = height;
this.bands = bands;
this.dataType = dataType;
this.data = dataType == DataBuffer.TYPE_BYTE
? new byte[bands * width * height]
: new short[bands * width * height];
}
}
private static ImageData readPPM(File file) throws IOException, BadImageFileException {
FileInputStream s = new FileInputStream(file);
ImageData imageData;
try {
String S1 = readln(s);
int width;
int height;
int bands;
int dataType;
if (S1.equals("P5") || S1.equals("P6")) {
bands = S1.equals("P5") ? 1 : 3;
String S2 = readln(s);
String S3 = readln(s);
String dimensions[] = S2.split("\\s");
width = Integer.parseInt(dimensions[0]);
height = Integer.parseInt(dimensions[1]);
dataType = S3.equals("255") ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
imageData = new ImageData(width, height, bands, dataType);
} else if (S1.equals("P7")) {
String WIDTH = "WIDTH ";
String HEIGHT = "HEIGHT ";
String DEPTH = "DEPTH ";
String MAXVAL = "MAXVAL ";
String TUPLTYPE = "TUPLTYPE ";
String ENDHDR = "ENDHDR";
String SWIDTH = readln(s);
width = Integer.parseInt(SWIDTH.substring(WIDTH.length()));
String SHEIGHT = readln(s);
height = Integer.parseInt(SHEIGHT.substring(HEIGHT.length()));
String SDEPTH = readln(s);
bands = Integer.parseInt(SDEPTH.substring(DEPTH.length()));
String SMAXVAL = readln(s);
dataType = SMAXVAL.substring(MAXVAL.length()).equals("65535")
? DataBuffer.TYPE_USHORT
: DataBuffer.TYPE_BYTE;
String STUPLTYPE = readln(s);
String SENDHDR = readln(s);
imageData = new ImageData(width, height, bands, dataType);
} else
return null;
int totalData = width * height * bands * (dataType == DataBuffer.TYPE_BYTE ? 1 : 2);
FileChannel c = s.getChannel();
if (file.length() != totalData + c.position()) {
c.close();
throw new BadImageFileException(file);
}
ByteBuffer bb = c.map(FileChannel.MapMode.READ_ONLY, c.position(), totalData);
if (dataType == DataBuffer.TYPE_USHORT) {
bb.order(ByteOrder.BIG_ENDIAN);
bb.asShortBuffer().get((short[]) imageData.data);
} else
bb.get((byte[]) imageData.data);
if (bb instanceof DirectBuffer)
((DirectBuffer) bb).cleaner().clean();
c.close();
} catch (Exception e) {
e.printStackTrace();
s.close();
throw new BadImageFileException(file, e);
} finally {
s.close();
}
return imageData;
}
private static final String DCRAW_OUTPUT = "Writing data to ";
public enum dcrawMode {full, preview, thumb}
public RenderedImage runDCRaw(dcrawMode mode) throws IOException, UnknownImageTypeException, BadImageFileException {
return runDCRaw(mode, false);
}
public synchronized RenderedImage runDCRaw(dcrawMode mode, boolean secondaryPixels)
throws IOException, UnknownImageTypeException, BadImageFileException
{
if (!m_decodable || (mode == dcrawMode.full && m_rawColors != 3))
throw new UnknownImageTypeException("Unsuported Camera");
RenderedImage result = null;
File of = null;
try {
if (mode == dcrawMode.preview) {
if (m_thumbWidth >= 1024 && m_thumbHeight >= 768) {
mode = dcrawMode.thumb;
}
}
long t1 = System.currentTimeMillis();
of = File.createTempFile("LZRAWTMP", ".ppm");
boolean four_colors = false;
final String makeModel = m_make + ' ' + m_model;
for (String s : four_color_cameras)
if (s.equalsIgnoreCase(makeModel)) {
four_colors = true;
break;
}
if (secondaryPixels)
runDCRawInfo(true);
String cmd[];
switch (mode) {
case full:
if (four_colors)
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-f", "-H", "1", "-t", "0", "-o", "0", "-4", m_fileName };
else if (m_filters == -1 || (m_make != null && m_make.equalsIgnoreCase("SIGMA")))
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-H", "1", "-t", "0", "-o", "0", "-4", m_fileName };
else if (secondaryPixels)
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-j", "-H", "1", "-t", "0", "-s", "1", "-d", "-4", m_fileName };
else
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-j", "-H", "1", "-t", "0", "-d", "-4", m_fileName };
break;
case preview:
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-t", "0", "-o", "1", "-w", "-h", m_fileName };
break;
case thumb:
cmd = new String[] { DCRAW_PATH, "-F", of.getAbsolutePath(), "-v", "-e", m_fileName };
break;
default:
throw new IllegalArgumentException("Unknown mode " + mode);
}
String ofName = null;
synchronized (DCRaw.class) {
Process p = null;
InputStream dcrawStdErr;
InputStream dcrawStdOut;
if (ForkDaemon.INSTANCE != null) {
ForkDaemon.INSTANCE.invoke(cmd);
dcrawStdErr = ForkDaemon.INSTANCE.getStdErr();
dcrawStdOut = ForkDaemon.INSTANCE.getStdOut();
} else {
p = Runtime.getRuntime().exec(cmd);
dcrawStdErr = new BufferedInputStream(p.getErrorStream());
dcrawStdOut = p.getInputStream();
}
String line, args;
// output expected on stderr
while ((line = readln(dcrawStdErr)) != null) {
// System.out.println(line);
if ((args = match(line, DCRAW_OUTPUT)) != null)
ofName = args.substring(0, args.indexOf(" ..."));
}
// Flush stdout just in case...
while ((line = readln(dcrawStdOut)) != null)
; // System.out.println(line);
if (p != null) {
dcrawStdErr.close();
try {
p.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
m_error = p.exitValue();
p.destroy();
} else
m_error = 0;
}
System.out.println("dcraw value: " + m_error);
if (m_error > 0) {
of.delete();
throw new BadImageFileException(of);
}
if (!ofName.equals(of.getPath())) {
of.delete();
of = new File(ofName);
}
if (of.getName().endsWith(".jpg") || of.getName().endsWith(".tiff")) {
if (of.getName().endsWith(".jpg")) {
try {
LCJPEGReader jpegReader = new LCJPEGReader(of.getPath());
result = jpegReader.getImage();
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
LCTIFFReader tiffReader = new LCTIFFReader(of.getPath());
result = tiffReader.getImage(null);
} catch (Exception e) {
e.printStackTrace();
}
}
long t2 = System.currentTimeMillis();
int totalData = result.getWidth() *
result.getHeight() *
result.getColorModel().getNumColorComponents() *
(result.getColorModel().getTransferType() == DataBuffer.TYPE_BYTE ? 1 : 2);
System.out.println("Read " + totalData + " bytes in " + (t2 - t1) + "ms");
} else {
ImageData imageData;
try {
imageData = readPPM(of);
} catch (Exception e) {
e.printStackTrace();
throw new BadImageFileException(of, e);
}
// do not change the initial image geometry
// m_width = Math.min(m_width, imageData.width);
// m_height = Math.min(m_height, imageData.height);
long t2 = System.currentTimeMillis();
int totalData = imageData.width *
imageData.height *
imageData.bands * (imageData.dataType == DataBuffer.TYPE_BYTE ? 1 : 2);
System.out.println("Read " + totalData + " bytes in " + (t2 - t1) + "ms");
final ColorModel cm;
if (mode == dcrawMode.full) {
if (imageData.bands == 1) {
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
false, false,
Transparency.OPAQUE, DataBuffer.TYPE_USHORT);
} else {
cm = JAIContext.colorModel_linear16;
}
} else {
if (imageData.bands == 3)
cm = new ComponentColorModel(JAIContext.sRGBColorSpace,
false, false,
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
else if (imageData.bands == 4)
cm = new ComponentColorModel(JAIContext.CMYKColorSpace,
false, false,
Transparency.OPAQUE, imageData.dataType);
else
throw new UnknownImageTypeException("Weird number of bands: " + imageData.bands);
}
final DataBuffer buf = imageData.dataType == DataBuffer.TYPE_BYTE
? new DataBufferByte((byte[]) imageData.data,
imageData.bands * imageData.width * imageData.height)
: new DataBufferUShort((short[]) imageData.data,
imageData.bands * imageData.width * imageData.height);
final WritableRaster raster = Raster.createInterleavedRaster(
buf, imageData.width, imageData.height,
imageData.bands * imageData.width, imageData.bands,
imageData.bands == 3 ? new int[]{ 0, 1, 2 } : new int[]{0}, null
);
result = new BufferedImage(cm, raster, false, null);
}
} catch (IOException e) {
if (of != null)
of.delete();
throw e;
} finally {
if (of != null)
of.delete();
}
return result;
}
public int getFilters() {
return m_filters;
}
public boolean getSecondaryPixels() {
return m_secondaryPixels;
}
/**
* {@inheritDoc}
*/
public float getShutterSpeed() {
return m_shutterSpeed;
}
public boolean decodable() {
return m_decodable;
}
public int rawColors() {
return m_rawColors;
}
////////// private ////////////////////////////////////////////////////////
private int m_error;
private boolean m_decodable = true;
private final String m_fileName;
private String m_make;
private String m_model;
private long m_captureDateTime;
private int m_width;
private int m_height;
private int m_fullWidth;
private int m_fullHeight;
private int m_thumbWidth;
private int m_thumbHeight;
private int m_rawWidth;
private int m_rawHeight;
private float m_shutterSpeed;
private float m_aperture;
private float m_focalLength;
private int m_iso;
private int m_filters;
private int m_rawColors;
private boolean m_secondaryPixels;
private float m_cam_mul[] = new float[4];
private float m_pre_mul[] = new float[4];
private float m_rgb_cam[];
private float m_xyz_cam[];
private float m_secondary_cam_mul[] = new float[4];
private static final String four_color_cameras[] = {
// "OLYMPUS E-3",
"OLYMPUS E-1",
"OLYMPUS E-300",
"OLYMPUS E-330",
"OLYMPUS E-500",
"OLYMPUS E-510",
"OLYMPUS E-520",
"OLYMPUS E-400",
"OLYMPUS E-410",
"OLYMPUS E-420",
"OLYMPUS E-20,E-20N,E-20P",
"OLYMPUS E-10",
"Leica Camera AG M8 Digital Camera"
// TODO: what about the Panasonic 4/3 and the Leica Digilux 3?
};
public native static void interpolateGreen(short[] srcData, short[] destData, int width, int height,
int srcLineStride, int destLineStride,
int srcOffset, int rOffset, int gOffset, int bOffset,
int gx, int gy, int ry );
public native static void interpolateRedBlue(short[] jdata, int width, int height, int lineStride,
int rOffset, int gOffset, int bOffset,
int rx0, int ry0, int bx0, int by0);
}
/* vim:set et sw=4 ts=4: */