/*
* #%L
* BSD implementations of Bio-Formats readers and writers
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package loci.formats.codec;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.ImageProducer;
import java.io.IOException;
import java.io.InputStream;
import java.util.Hashtable;
import loci.common.ByteArrayHandle;
import loci.common.RandomAccessInputStream;
import loci.common.Region;
import loci.formats.FormatException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*/
public class JPEGTileDecoder {
// -- Constants --
protected static final Logger LOGGER =
LoggerFactory.getLogger(JPEGTileDecoder.class);
// -- Fields --
private TileConsumer consumer;
private TileCache tiles;
private RandomAccessInputStream in;
// -- JPEGTileDecoder API methods --
public void initialize(String id, int imageWidth) {
try {
initialize(new RandomAccessInputStream(id), imageWidth);
}
catch (IOException e) {
LOGGER.debug("", e);
}
}
public void initialize(RandomAccessInputStream in, int imageWidth) {
initialize(in, 0, 0, imageWidth);
}
public void initialize(RandomAccessInputStream in, int y, int h,
int imageWidth)
{
this.in = in;
tiles = new TileCache(y, h);
preprocess(this.in);
try {
Toolkit toolkit = Toolkit.getDefaultToolkit();
byte[] data = new byte[this.in.available()];
this.in.readFully(data);
Image image = toolkit.createImage(data);
ImageProducer producer = image.getSource();
consumer = new TileConsumer(producer, y, h);
producer.startProduction(consumer);
while (producer.isConsumer(consumer));
}
catch (IOException e) { }
}
/**
* Pre-process the stream to make sure that the
* image width and height are non-zero. Returns an array containing
* the image width and height.
*/
public int[] preprocess(RandomAccessInputStream in) {
int[] dims = new int[2];
try {
long fp = in.getFilePointer();
boolean littleEndian = in.isLittleEndian();
in.order(false);
while (in.getFilePointer() < in.length() - 1) {
int code = in.readShort() & 0xffff;
int length = in.readShort() & 0xffff;
long pointer = in.getFilePointer();
if (length > 0xff00 || code < 0xff00) {
in.seek(pointer - 3);
continue;
}
if (code == 0xffc0) {
in.skipBytes(1);
int height = in.readShort() & 0xffff;
int width = in.readShort() & 0xffff;
if (height == 0 || width == 0) {
throw new RuntimeException(
"Width or height > 65500 is not supported.");
}
dims[0] = width;
dims[1] = height;
break;
}
else if (pointer + length - 2 < in.length()) {
in.seek(pointer + length - 2);
}
else {
break;
}
}
in.seek(fp);
in.order(littleEndian);
}
catch (IOException e) { }
return dims;
}
public byte[] getScanline(int y) {
try {
return tiles.get(0, y, consumer.getWidth(), 1);
}
catch (FormatException e) {
LOGGER.debug("", e);
}
catch (IOException e) {
LOGGER.debug("", e);
}
return null;
}
public int getWidth() {
return consumer.getWidth();
}
public int getHeight() {
return consumer.getHeight();
}
public void close() {
try {
if (in != null) {
in.close();
}
}
catch (IOException e) {
LOGGER.debug("", e);
}
tiles = null;
consumer = null;
}
// -- Helper classes --
class TileConsumer implements ImageConsumer {
private int width, height;
private ImageProducer producer;
private int yy = 0, hh = 0;
public TileConsumer(ImageProducer producer) {
this.producer = producer;
}
public TileConsumer(ImageProducer producer, int y, int h) {
this(producer);
this.yy = y;
this.hh = h;
}
// -- TileConsumer API methods --
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
// -- ImageConsumer API methods --
@Override
public void imageComplete(int status) {
producer.removeConsumer(this);
}
@Override
public void setDimensions(int width, int height) {
this.width = width;
this.height = height;
if (hh <= 0) hh = height;
}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model,
byte[] pixels, int off, int scanSize)
{
LOGGER.debug("Storing row {} of {} ({}%)", new Object[] {y, height,
((double) y / height) * 100.0});
if (y >= (yy + hh)) {
imageComplete(0);
return;
}
else if (y < yy) return;
try {
tiles.add(pixels, x, y, w, h);
}
catch (FormatException e) {
LOGGER.debug("", e);
}
catch (IOException e) {
LOGGER.debug("", e);
}
}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model,
int[] pixels, int off, int scanSize)
{
LOGGER.debug("Storing row {} of {} ({}%)", new Object[] {y, (yy + hh),
((double) y / (yy + hh)) * 100.0});
if (y >= (yy + hh)) {
imageComplete(0);
return;
}
else if (y < yy) return;
try {
tiles.add(pixels, x, y, w, h);
}
catch (FormatException e) {
LOGGER.debug("", e);
}
catch (IOException e) {
LOGGER.debug("", e);
}
}
@Override
public void setProperties(Hashtable props) { }
@Override
public void setColorModel(ColorModel model) { }
@Override
public void setHints(int hintFlags) { }
}
class TileCache {
private static final int ROW_COUNT = 128;
private Hashtable<Region, byte[]> compressedTiles =
new Hashtable<Region, byte[]>();
private JPEGCodec codec = new JPEGCodec();
private CodecOptions options = new CodecOptions();
private ByteVector toCompress = new ByteVector();
private int row = 0;
private Region lastRegion = null;
private byte[] lastTile = null;
private int yy = 0, hh = 0;
public TileCache(int yy, int hh) {
options.interleaved = true;
options.littleEndian = false;
this.yy = yy;
this.hh = hh;
}
public void add(byte[] pixels, int x, int y, int w, int h)
throws FormatException, IOException
{
toCompress.add(pixels);
row++;
if ((y % ROW_COUNT) == ROW_COUNT - 1 || y == getHeight() - 1 ||
y == yy + hh - 1)
{
Region r = new Region(x, y - row + 1, w, row);
options.width = w;
options.height = row;
options.channels = 1;
options.bitsPerSample = 8;
options.signed = false;
byte[] compressed = codec.compress(toCompress.toByteArray(), options);
compressedTiles.put(r, compressed);
toCompress.clear();
}
}
public void add(int[] pixels, int x, int y, int w, int h)
throws FormatException, IOException
{
byte[] buf = new byte[pixels.length * 3];
for (int i=0; i<pixels.length; i++) {
buf[i * 3] = (byte) ((pixels[i] & 0xff0000) >> 16);
buf[i * 3 + 1] = (byte) ((pixels[i] & 0xff00) >> 8);
buf[i * 3 + 2] = (byte) (pixels[i] & 0xff);
}
toCompress.add(buf);
row++;
if ((y % ROW_COUNT) == ROW_COUNT - 1 || y == getHeight() - 1 ||
y == yy + hh - 1)
{
Region r = new Region(x, y - row + 1, w, row);
options.width = w;
options.height = row;
options.channels = 3;
options.bitsPerSample = 8;
options.signed = false;
byte[] compressed = codec.compress(toCompress.toByteArray(), options);
compressedTiles.put(r, compressed);
toCompress.clear();
row = 0;
}
}
public byte[] get(int x, int y, int w, int h)
throws FormatException, IOException
{
Region[] keys = compressedTiles.keySet().toArray(new Region[0]);
Region r = new Region(x, y, w, h);
for (Region key : keys) {
if (key.intersects(r)) {
r = key;
}
}
if (!r.equals(lastRegion)) {
lastRegion = r;
byte[] compressed = null;
compressed = compressedTiles.get(r);
if (compressed == null) return null;
lastTile = codec.decompress(compressed, options);
}
int pixel = options.channels * (options.bitsPerSample / 8);
byte[] buf = new byte[w * h * pixel];
for (int i=0; i<h; i++) {
System.arraycopy(lastTile, r.width * pixel * (i + y - r.y) + (x - r.x),
buf, i * w * pixel, pixel * w);
}
return buf;
}
}
}