/*
* $Id$
*
* Copyright (c) 2010 by Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.tools.image;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.TemporaryFileFactory;
import VASSAL.tools.lang.Reference;
/**
* Convert a {@link BufferedImage} to a different type by caching image
* data on disk.
*
* @since 3.2.0
* @author Joel Uckelman
*/
public class FileImageTypeConverter implements ImageTypeConverter {
protected final TemporaryFileFactory tfactory;
/**
* Create a converter.
*
* @param tmp the temporary file to use as a cache
*/
public FileImageTypeConverter(TemporaryFileFactory tfactory) {
if (tfactory == null) throw new IllegalArgumentException();
this.tfactory = tfactory;
}
/**
* {@inheritDoc}
*
* <b>WARNING: When this method is called, the sole reference to the image
* must be the one held by <code>ref</code> in order to allow the source
* image to be garbage collected after the image data is written to disk.</b>
*/
public BufferedImage convert(Reference<BufferedImage> ref, int type)
throws ImageIOException {
if (ref == null) throw new IllegalArgumentException();
// This is why we pass the image via a Reference:
//
// So long as the calling method holds no references to the image, we
// can ensure that our reference to the source image is the only one.
//
// This converter doesn't require the source and destination images to
// exist simultaneously, so when we finish with the source, we can null
// its reference. Because there are no other references to the source
// image, this will make it eligible for garbage collection.
//
// Hence, we can let the source image be gc'd before we create the
// destination image, potentially saving a huge amount of RAM.
//
BufferedImage src = ref.obj;
ref.obj = null;
// we can't create images of TYPE_CUSTOM
if (type == BufferedImage.TYPE_CUSTOM) throw new IllegalArgumentException();
File tmp = null;
try {
tmp = tfactory.create();
}
catch (IOException e) {
throw new ImageIOException("", e);
}
try {
// write the converted image data to a file
OutputStream out = null;
try {
out = new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream(tmp)));
write(src, out);
out.close();
}
catch (IOException e) {
throw new ImageIOException(tmp, e);
}
finally {
IOUtils.closeQuietly(out);
}
final int w = src.getWidth();
final int h = src.getHeight();
// ensure that src can be gc'd before we create dst
src = null;
final BufferedImage dst = new BufferedImage(w, h, type);
// read the converted image data back
InputStream in = null;
try {
in = new BufferedInputStream(
new GZIPInputStream(
new FileInputStream(tmp)));
read(in, dst);
in.close();
return dst;
}
catch (IOException e) {
throw new ImageIOException(tmp, e);
}
finally {
IOUtils.closeQuietly(in);
}
}
finally {
// clean up the temporary file
if (!tmp.delete()) {
throw new ImageIOException(tmp, "failed to delete");
}
}
}
protected void write(BufferedImage src, OutputStream out)
throws IOException {
final int w = src.getWidth();
final int h = src.getHeight();
final ByteBuffer bb = ByteBuffer.allocate(4*w);
final int[] row = new int[w];
for (int y = 0; y < h; ++y) {
// get the row in ARGB format
src.getRGB(0, y, w, 1, row, 0, w);
// copy the row to the byte buffer
bb.asIntBuffer().put(row);
// write the row to the stream
out.write(bb.array());
}
}
protected void read(InputStream in, BufferedImage dst) throws IOException {
final int w = dst.getWidth();
final int h = dst.getHeight();
final byte[] bytes = new byte[4*w];
final ByteBuffer bb = ByteBuffer.wrap(bytes);
final int[] row = new int[w];
for (int y = 0; y < h; ++y) {
// read the row from the stream
IOUtils.read(in, bytes);
// convert the bytes to an int[]
bb.asIntBuffer().get(row);
// write the row back to the image
dst.setRGB(0, y, w, 1, row, 0, w);
}
}
}