/* * $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.tilecache; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; 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.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.Arrays; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.apache.commons.codec.digest.DigestUtils; import VASSAL.tools.image.ImageIOException; import VASSAL.tools.image.ImageNotFoundException; import VASSAL.tools.io.IOUtils; /** * A class for reading and writing image tiles. * * The VASSAL tile format consists of the 18-byte header, followed by gzipped * 4-bpp image data. The header is the signature 'VASSAL' (6 bytes), the tile * width (4 bytes), the tile height (4 bytes), and the image type (4 bytes). * * @since 3.2.0 * @author Joel Uckelman */ public class TileUtils { private TileUtils() {} /** * Reads an image tile file. * * @param src the path of the tile file * @return the tile image * * @throws ImageIOException if the read fails * @throws ImageNotFoundException if the file isn't found */ public static BufferedImage read(String src) throws ImageIOException { return read(new File(src)); } /** * Reads an image tile file. * * @param src the path of the tile file * @return the tile image * * @throws ImageIOException if the read fails * @throws ImageNotFoundException if the file isn't found */ public static BufferedImage read(File src) throws ImageIOException { InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(src)); final BufferedImage img = read(in); in.close(); return img; } catch (FileNotFoundException e) { throw new ImageNotFoundException(src, e); } catch (IOException e) { throw new ImageIOException(src, e); } finally { IOUtils.closeQuietly(in); } } /** * Reads an image tile. * * @param in a stream containing the tile data * @return the tile image * * @throws IOException if the read fails */ public static BufferedImage read(InputStream in) throws IOException { ByteBuffer bb; // read the header final byte[] header = readHeader(in); bb = ByteBuffer.wrap(header); // validate the signature final byte[] sig = new byte[6]; bb.get(sig); checkSignature(sig); // get the dimensions and type final int w = bb.getInt(); final int h = bb.getInt(); final int type = bb.getInt(); // read the image data final byte[] cdata = IOUtils.toByteArray(in); // decompress the image data InputStream zin = null; try { zin = new GZIPInputStream(new ByteArrayInputStream(cdata)); bb = ByteBuffer.wrap(IOUtils.toByteArray(zin)); zin.close(); } finally { IOUtils.closeQuietly(zin); } // build the image final BufferedImage img = new BufferedImage(w, h, type); // FIXME: This might decelerate the image? If so, then we should // make a copy. final DataBufferInt db = (DataBufferInt) img.getRaster().getDataBuffer(); final int[] data = db.getData(); final IntBuffer ib = bb.asIntBuffer(); ib.get(data); /* if (ib.hasRemaining()) { // buffer contains garbage at the end! throw new IOException("found " + (4*ib.remaining()) + " more bytes!"); } */ return img; } /** * Reads the tile header from the stream. * * @param in the stream * @return the header * * @throws IOException if the read fails or there is too little data */ static byte[] readHeader(InputStream in) throws IOException { // read the header final byte[] header = new byte[18]; if (IOUtils.read(in, header) != header.length) { throw new IOException("header too short!"); } return header; } /** * Checks that the given byte array equals the tile signature. * * @param sig the byte array to check * * @throws IOException if the byte array is not the tile signature */ static void checkSignature(byte[] sig) throws IOException { if (!Arrays.equals(sig, "VASSAL".getBytes())) { throw new IOException( "bad signature: got \"" + new String(sig) + "\", expected \"VASSAL\"" ); } } /** * Reads the dimensions of the tile in an image tile file. * * @param src the path of the tile file * @return the dimensions * * @throws ImageIOException if the read fails * @throws ImageNotFoundException if the file isn't found */ public static Dimension size(String src) throws ImageIOException { return size(new File(src)); } /** * Reads the dimensions of the tile in an image tile file. * * @param src the path of the tile file * @return the dimensions * * @throws ImageIOException if the read fails * @throws ImageNotFoundException if the file isn't found */ public static Dimension size(File src) throws ImageIOException { InputStream in = null; try { // NB: We don't buffer here because we're reading only 18 bytes. in = new FileInputStream(src); final Dimension d = size(in); in.close(); return d; } catch (FileNotFoundException e) { throw new ImageNotFoundException(src, e); } catch (IOException e) { throw new ImageIOException(src, e); } finally { IOUtils.closeQuietly(in); } } /** * Reads the dimensions of the tile from a stream. * * @param in the stream * @return the dimensions * * @throws IOException if the read fails */ public static Dimension size(InputStream in) throws IOException { ByteBuffer bb; // read the header final byte[] header = readHeader(in); bb = ByteBuffer.wrap(header); // validate the signature final byte[] sig = new byte[6]; bb.get(sig); checkSignature(sig); // get the dimensions return new Dimension(bb.getInt(), bb.getInt()); } /** * Write a tile image to a tile file. * * @param tile the image * @param dst the tile file * * @throws ImageIOException if the write fails */ public static void write(BufferedImage tile, String dst) throws ImageIOException { write(tile, new File(dst)); } /** * Write a tile image to a tile file. * * @param tile the image * @param dst the tile file * * @throws ImageIOException if the write fails */ public static void write(BufferedImage tile, File dst) throws ImageIOException { OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(dst)); write(tile, out); out.close(); } catch (IOException e) { throw new ImageIOException(dst, e); } finally { IOUtils.closeQuietly(out); } } /** * Write a tile image to a stream. * * @param tile the image * @param out the stream * * @throws ImageIOException if the write fails */ public static void write(BufferedImage tile, OutputStream out) throws IOException { ByteBuffer bb; // write the header bb = ByteBuffer.allocate(18); bb.put("VASSAL".getBytes()) .putInt(tile.getWidth()) .putInt(tile.getHeight()) .putInt(tile.getType()); out.write(bb.array()); // write the tile data final DataBufferInt db = (DataBufferInt) tile.getRaster().getDataBuffer(); final int[] data = db.getData(); bb = ByteBuffer.allocate(4*data.length); bb.asIntBuffer().put(data); final GZIPOutputStream zout = new GZIPOutputStream(out); zout.write(bb.array()); zout.finish(); } /** * Calculates the number of tiles needed to cover an image, summed over * all sizes from 1:1 to the vanishing point. * * @param i the image dimensions * @param t the tile dimensions * @return the number of tiles needed to cover the image * * @throws IllegalArgumentException if any argument is nonpositive */ public static int tileCount(Dimension i, Dimension t) { return tileCount(i.width, i.height, t.width, t.height); } /** * Calculates the number of tiles needed to cover an image, summed over * all sizes from 1:1 to the vanishing point. * * @param iw the image width * @param ih the image height * @param tw the tile width * @param th the tile height * @return the number of tiles needed to cover the image * * @throws IllegalArgumentException if any argument is nonpositive */ public static int tileCount(int iw, int ih, int tw, int th) { // TODO: Find a closed-form expression for this, if there is one. int tcount = 0; for (int div = 1; iw/div > 0 && ih/div > 0; div <<= 1) { tcount += tileCountAtScale(iw, ih, tw, th, div); } return tcount; } /** * Calculates the number of tiles needed to cover an image at a given * scale. * * @param i the image dimensions * @param t the tile dimensions * @param div the scale divisor * @return the number of tiles needed to cover the image * * @throws IllegalArgumentException if any argument is nonpositive */ public static int tileCountAtScale(Dimension i, Dimension t, int div) { return tileCountAtScale(i.width, i.height, t.width, t.height, div); } /** * Calculates the number of tiles needed to cover an image at a given * scale. * * @param iw the image width * @param ih the image height * @param tw the tile width * @param th the tile height * @param div the scale divisor * @return the number of tiles needed to cover the image * * @throws IllegalArgumentException if any argument is nonpositive */ public static int tileCountAtScale(int iw, int ih, int tw, int th, int div) { if (iw < 1) throw new IllegalArgumentException("iw = " + iw + " < 1"); if (ih < 1) throw new IllegalArgumentException("ih = " + ih + " < 1"); if (tw < 1) throw new IllegalArgumentException("tw = " + tw + " < 1"); if (th < 1) throw new IllegalArgumentException("th = " + th + " < 1"); if (div < 1) throw new IllegalArgumentException("div = " + div + " < 1"); final int cols = (int) Math.ceil((double) (iw/div) / tw); final int rows = (int) Math.ceil((double) (ih/div) / th); return cols*rows; } /** * Gets the name of a tile file. * * @param iname the image name * @param tileX the X coordinate of the tile * @param tileY the Y coordinate of the tile * @param div the scale divisor * @return the name of the tile file */ public static String tileName(String iname, int tileX, int tileY, int div) { final String sha = DigestUtils.shaHex( iname + "(" + tileX + "," + tileY + "@1:" + div ); return sha.substring(0, 1) + '/' + sha.substring(0, 2) + '/' + sha; } }