/*
* $Id$
*
* Copyright (c) 2009-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.imageop;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang.builder.HashCodeBuilder;
import VASSAL.tools.image.GeneralFilter;
import VASSAL.tools.image.ImageUtils;
/**
* An {@link ImageOp} which scales its source and cobbles scaled tiles
* from the tile cache.
*
* @since 3.2.0
* @author Joel Uckelman
*/
public class ScaleOpTiledBitmapImpl extends ScaleOpBitmapImpl {
public ScaleOpTiledBitmapImpl(ImageOp sop, double scale) {
this(sop, scale, defaultHints);
}
public ScaleOpTiledBitmapImpl(ImageOp sop, double scale,
RenderingHints hints)
{
super(sop, scale, hints);
}
@Override
protected ImageOp createTileOp(int tileX, int tileY) {
final double iscale = 1.0/scale;
final boolean invPow2 = Math.floor(iscale) == iscale &&
(((int) iscale) & (((int) iscale)-1)) == 0;
if (invPow2 && sop instanceof SourceOp) {
final String name = ((SourceOp) sop).getName();
return new SourceOpDiskCacheBitmapImpl(name, tileX, tileY, scale);
}
else {
return new TileOp(this, tileX, tileY);
}
}
private static class TileOp extends AbstractTileOpImpl {
private final ImageOp rsop;
private final ImageOp[] sop;
private final int dx0, dy0, dx1, dy1, dw, dh;
private final int sx0, sy0, sx1, sy1, sw, sh;
private final Dimension dd;
private final Dimension sd;
private final double scale;
private final float xscale, yscale;
private final int tx;
private final int ty;
@SuppressWarnings("unused")
private final RenderingHints hints;
private final int hash;
private static final GeneralFilter.Filter downFilter =
new GeneralFilter.Lanczos3Filter();
private static final GeneralFilter.Filter upFilter =
new GeneralFilter.MitchellFilter();
public TileOp(ScaleOpTiledBitmapImpl rop, int tileX, int tileY) {
if (rop == null) throw new IllegalArgumentException();
if (tileX < 0 || tileX >= rop.getNumXTiles() ||
tileY < 0 || tileY >= rop.getNumYTiles())
throw new IndexOutOfBoundsException();
tx = tileX;
ty = tileY;
scale = rop.getScale();
hints = rop.getHints();
// destination sizes and coordinates
dd = rop.getSize();
dx0 = tileX * rop.getTileWidth();
dy0 = tileY * rop.getTileHeight();
dw = Math.min(rop.getTileWidth(), dd.width - dx0);
dh = Math.min(rop.getTileHeight(), dd.height - dy0);
dx1 = dx0 + dw - 1;
dy1 = dy0 + dh - 1;
size = new Dimension(dw, dh);
if (scale >= 1.0) {
// we are upscaling
rsop = rop.sop;
}
else {
// cobble this tile from pyramid tiles at the lub size
final double nscale =
1.0/(1 << (int) Math.floor(Math.log(1.0/scale)/Math.log(2)));
rsop = new ScaleOpTiledBitmapImpl(rop.sop, nscale);
}
sd = rsop.getSize();
// We want dx0 * xscale = sx0, unless that makes xscale = 0.
xscale = sd.width == 1 ? dd.width : (float)(dd.width-1)/(sd.width-1);
// We want dy0 * yscale = sy0, unless that makes yscale = 0.
yscale = sd.height == 1 ? dd.height : (float)(dd.height-1)/(sd.height-1);
final float fw = scale < 1.0f ?
downFilter.getSamplingRadius() : upFilter.getSamplingRadius();
sx0 = Math.max(0, (int) Math.floor((dx0-fw)/xscale));
sy0 = Math.max(0, (int) Math.floor((dy0-fw)/yscale));
sx1 = Math.min(sd.width-1, (int) Math.ceil((dx1+fw)/xscale));
sy1 = Math.min(sd.height-1, (int) Math.ceil((dy1+fw)/yscale));
sw = sx1 - sx0 + 1;
sh = sy1 - sy0 + 1;
final Rectangle sr = new Rectangle(sx0, sy0, sw, sh);
final Point[] stiles = rsop.getTileIndices(sr);
sop = new ImageOp[stiles.length];
for (int i = 0; i < stiles.length; ++i) {
sop[i] = rsop.getTileOp(stiles[i]);
}
hash = new HashCodeBuilder().append(sop)
.append(dx0)
.append(dy0)
.append(dw)
.append(dh)
.append(tx)
.append(ty)
.toHashCode();
}
public List<VASSAL.tools.opcache.Op<?>> getSources() {
return Arrays.<VASSAL.tools.opcache.Op<?>>asList(sop);
}
public BufferedImage eval() throws Exception {
if (dw < 1 || dh < 1) return ImageUtils.NULL_IMAGE;
// cobble
final Point[] tiles =
rsop.getTileIndices(new Rectangle(sx0, sy0, sw, sh));
final int tw = rsop.getTileWidth();
final int th = rsop.getTileHeight();
BufferedImage src;
final boolean src_trans =
ImageUtils.isTransparent(rsop.getTile(tiles[0], null));
// make sure we have an int-type image
// and match the transparency of the first tile
switch (ImageUtils.getCompatibleImageType(rsop.getTile(tiles[0], null))) {
case BufferedImage.TYPE_INT_RGB:
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
case BufferedImage.TYPE_INT_BGR:
src = ImageUtils.createCompatibleImage(sw, sh, src_trans);
break;
default:
src = new BufferedImage(
sw, sh, src_trans ?
BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_RGB
);
}
final Graphics2D g = src.createGraphics();
for (Point tile : tiles) {
g.drawImage(rsop.getTile(tile, null),
tile.x*tw-sx0, tile.y*th-sy0, null);
}
g.dispose();
final int src_data[] =
((DataBufferInt) src.getRaster().getDataBuffer()).getData();
final WritableRaster dstR = src.getColorModel()
.createCompatibleWritableRaster(dw, dh);
final int dst_data[] = ((DataBufferInt) dstR.getDataBuffer()).getData();
final int src_type;
if (!src_trans) {
src_type = GeneralFilter.OPAQUE;
}
else if (src.isAlphaPremultiplied()) {
src_type = GeneralFilter.TRANS_PREMULT;
}
else {
src_type = GeneralFilter.TRANS_UNPREMULT;
}
GeneralFilter.resample(
src_data, true, sx0, sy0, sx1, sy1, sw, sh, src_type, sd.width, sd.height,
// src_data, sx0, sy0, sx1, sy1, sw, sh, src_type, sd.width, sd.height,
dst_data, dx0, dy0, dx1, dy1, dw, dh, dd.width, dd.height,
xscale, yscale, scale < 1.0f ? downFilter : upFilter
);
return ImageUtils.toCompatibleImage(new BufferedImage(
src.getColorModel(),
dstR,
src.isAlphaPremultiplied(),
null
));
}
protected void fixSize() { }
/** {@inheritDoc} */
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || o.getClass() != this.getClass()) return false;
final TileOp op = (TileOp) o;
return dx0 == op.dx0 &&
dy0 == op.dy0 &&
dw == op.dw &&
dh == op.dh &&
tx == op.tx &&
ty == op.ty &&
scale == op.scale &&
Arrays.equals(sop, op.sop);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return hash;
}
/** {@inheritDoc} */
@Override
public String toString() {
return getClass().getName() +
"[sop=" + Arrays.toString(sop) + ",scale=" + scale +
",dx0=" + dx0 + ",dy0=" + dy0 + ",dw=" + dw + ",dy=" + dh + "]";
}
}
/** {@inheritDoc} */
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || o.getClass() != this.getClass()) return false;
final ScaleOpTiledBitmapImpl op = (ScaleOpTiledBitmapImpl) o;
return scale == op.scale &&
sop.equals(op.sop) &&
hints.equals(op.hints);
}
}