VASSAL.tools.image.tilecache.TileUtils.java Source code

Java tutorial

Introduction

Here is the source code for VASSAL.tools.image.tilecache.TileUtils.java

Source

/*
 * $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;
    }
}