/*
* #%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.services;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import loci.common.ByteArrayHandle;
import loci.common.DataTools;
import loci.common.RandomAccessInputStream;
import loci.common.Region;
import loci.common.services.DependencyException;
import loci.common.services.Service;
import loci.common.services.ServiceException;
import org.libjpegturbo.turbojpeg.TJ;
import org.libjpegturbo.turbojpeg.TJDecompressor;
import org.scijava.nativelib.NativeLibraryUtil;
/**
* Based upon the NDPI to OME-TIFF converter by Matthias Baldauf:
*
* http://matthias-baldauf.at/software/ndpi_converter/
*
* @author Melissa Linkert <melissa at glencoesoftware.com>
*/
public class JPEGTurboServiceImpl implements JPEGTurboService {
// -- Constants --
private static final String NATIVE_LIB_CLASS =
"org.scijava.nativelib.NativeLibraryUtil";
private static final int SOF0 = 0xffc0;
private static final int DRI = 0xffdd;
private static final int SOS = 0xffda;
private static final int RST0 = 0xffd0;
private static final int RST7 = 0xffd7;
private static final int EOI = 0xffd9;
// -- Fields --
private transient Logger logger;
private int imageWidth;
private int imageHeight;
private long offset;
private RandomAccessInputStream in;
private int restartInterval = 1;
private long sos;
private long imageDimensions;
private int tileDim;
private int xTiles;
private int yTiles;
private ArrayList<Long> restartMarkers = new ArrayList<Long>();
private byte[] header;
private static boolean libraryLoaded = false;
// -- Constructor --
public JPEGTurboServiceImpl() {
logger = Logger.getLogger(NATIVE_LIB_CLASS);
logger.setLevel(Level.SEVERE);
if (!libraryLoaded) {
NativeLibraryUtil.loadNativeLibrary(TJ.class, "turbojpeg");
libraryLoaded = true;
}
}
// -- JPEGTurboService API methods --
@Override
public void setRestartMarkers(long[] markers) {
restartMarkers.clear();
if (markers != null) {
for (long marker : markers) {
restartMarkers.add(marker);
}
}
}
@Override
public long[] getRestartMarkers() {
long[] markers = new long[restartMarkers.size()];
for (int i=0; i<markers.length; i++) {
markers[i] = restartMarkers.get(i);
}
return markers;
}
@Override
public void initialize(RandomAccessInputStream jpeg, int width, int height)
throws ServiceException, IOException
{
in = jpeg;
imageWidth = width;
imageHeight = height;
offset = jpeg.getFilePointer();
in.skipBytes(2);
int marker = in.readShort() & 0xffff;
boolean inImage = false;
while (!inImage && in.getFilePointer() + 2 < in.length()) {
int length = in.readShort() & 0xffff;
long end = in.getFilePointer() + length - 2;
if (marker == DRI) {
restartInterval = in.readShort() & 0xffff;
}
else if (marker == SOF0) {
imageDimensions = in.getFilePointer() + 1;
}
else if (marker == SOS) {
sos = end;
inImage = true;
if (restartMarkers.size() == 0) {
restartMarkers.add(sos);
}
else {
long diff = sos - restartMarkers.get(0);
for (int i=0; i<restartMarkers.size(); i++) {
long original = restartMarkers.get(i);
original += diff;
restartMarkers.set(i, original);
}
break;
}
}
if (end < in.length() && !inImage) {
in.seek(end);
marker = in.readShort() & 0xffff;
}
}
if (restartMarkers.size() == 1) {
in.seek(restartMarkers.get(0));
byte[] buf = new byte[10 * 1024 * 1024];
in.read(buf, 0, 4);
while (in.getFilePointer() < in.length()) {
int n = in.read(buf, 4,
(int) Math.min(buf.length - 4, in.length() - in.getFilePointer()));
n += 4;
for (int i=0; i<n-1; i++) {
marker = DataTools.bytesToShort(buf, i, 2, false) & 0xffff;
if (marker >= RST0 && marker <= RST7) {
restartMarkers.add(in.getFilePointer() - n + i + 2);
i += restartInterval;
}
}
// refill buffer
System.arraycopy(buf, n - 4, buf, 0, 4);
}
}
tileDim = restartInterval * 8;
xTiles = imageWidth / tileDim;
yTiles = imageHeight / tileDim;
if (xTiles * tileDim != imageWidth) {
xTiles++;
}
if (yTiles * tileDim != imageHeight) {
yTiles++;
}
}
@Override
public byte[] getTile(byte[] buf, int xCoordinate, int yCoordinate,
int width, int height)
throws IOException
{
Region image = new Region(xCoordinate, yCoordinate, width, height);
int bufX = 0;
int bufY = 0;
int outputRowLen = width * 3;
Region intersection = null;
Region tileBoundary = new Region(0, 0, 0, 0);
byte[] tile = null;
for (int row=0; row<yTiles; row++) {
tileBoundary.height = row < yTiles - 1 ? tileDim : imageHeight - (tileDim*row);
tileBoundary.y = row * tileDim;
for (int col=0; col<xTiles; col++) {
tileBoundary.x = col * tileDim;
tileBoundary.width = col < xTiles - 1 ? tileDim : imageWidth - (tileDim*col);
if (tileBoundary.intersects(image)) {
intersection = image.intersection(tileBoundary);
tile = getTile(col, row);
int rowLen =
3 * (int) Math.min(tileBoundary.width, intersection.width);
int outputOffset = bufY * outputRowLen + bufX;
int intersectionX = 0;
if (tileBoundary.x < image.x) {
intersectionX = image.x - tileBoundary.x;
}
for (int trow=0; trow<intersection.height; trow++) {
int realRow = trow + intersection.y - tileBoundary.y;
int inputOffset =
3 * (realRow * tileBoundary.width + intersectionX);
System.arraycopy(tile, inputOffset, buf, outputOffset, rowLen);
outputOffset += outputRowLen;
}
bufX += rowLen;
}
}
if (intersection != null) {
bufX = 0;
bufY += intersection.height;
}
if (bufY >= height) {
break;
}
}
return buf;
}
@Override
public byte[] getTile(int tileX, int tileY) throws IOException {
if (header == null) {
header = getFixedHeader();
}
long dataLength = header.length + 2;
int start = tileX + (tileY * xTiles * restartInterval);
for (int row=0; row<restartInterval; row++) {
int end = start + 1;
if (end < restartMarkers.size()) {
long startOffset = restartMarkers.get(start);
long endOffset = restartMarkers.get(end);
dataLength += (endOffset - startOffset);
}
start += xTiles;
}
byte[] data = new byte[(int) dataLength];
int offset = 0;
System.arraycopy(header, 0, data, offset, header.length);
offset += header.length;
start = tileX + (tileY * xTiles * restartInterval);
for (int row=0; row<restartInterval; row++) {
int end = start + 1;
if (end < restartMarkers.size()) {
long startOffset = restartMarkers.get(start);
long endOffset = restartMarkers.get(end);
in.seek(startOffset);
in.read(data, offset, (int) (endOffset - startOffset - 2));
offset += (int) (endOffset - startOffset - 2);
DataTools.unpackBytes(0xffd0 + (row % 8), data, offset, 2, false);
offset += 2;
}
start += xTiles;
}
DataTools.unpackBytes(EOI, data, offset, 2, false);
// and here we actually decompress it...
try {
int pixelType = TJ.PF_RGB;
int pixelSize = TJ.getPixelSize(pixelType);
TJDecompressor decoder = new TJDecompressor(data);
byte[] decompressed = decoder.decompress(tileDim, tileDim * pixelSize,
tileDim, pixelType, pixelType);
data = null;
decoder.close();
return decompressed;
}
catch (Exception e) {
IOException ioe = new IOException(e.getMessage());
ioe.initCause(e);
throw ioe;
}
}
@Override
public void close() throws IOException {
logger = null;
imageWidth = 0;
imageHeight = 0;
if (in != null) {
in.close();
}
in = null;
offset = 0;
restartMarkers.clear();
restartInterval = 1;
sos = 0;
imageDimensions = 0;
tileDim = 0;
xTiles = 0;
yTiles = 0;
header = null;
}
// -- Helper methods --
private byte[] getFixedHeader() throws IOException {
in.seek(offset);
byte[] header = new byte[(int) (sos - offset)];
in.read(header);
int index = (int) (imageDimensions - offset);
DataTools.unpackBytes(tileDim, header, index, 2, false);
DataTools.unpackBytes(tileDim, header, index + 2, 2, false);
return header;
}
}