de.digitalcollections.turbojpeg.imageio.TurboJpegImageReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of imageio-turbojpeg Show documentation
Show all versions of imageio-turbojpeg Show documentation
ImageIO plugin for reading and writing JPEG images via libjpeg-turbo/turbojpeg.
Requires the libjpeg-turbo and turbojpeg shared native libraries to be installed on the system.
package de.digitalcollections.turbojpeg.imageio;
import de.digitalcollections.turbojpeg.Info;
import de.digitalcollections.turbojpeg.TurboJpeg;
import de.digitalcollections.turbojpeg.TurboJpegException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.stream.Stream;
import static java.awt.image.BufferedImage.*;
public class TurboJpegImageReader extends ImageReader {
private final static Logger LOGGER = LoggerFactory.getLogger(TurboJpegImageReader.class);
private final TurboJpeg lib;
private ByteBuffer jpegData;
private Info info;
protected TurboJpegImageReader(ImageReaderSpi originatingProvider, TurboJpeg lib) {
super(originatingProvider);
this.lib = lib;
}
@Override
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
if (input == null) {
return;
}
if (input instanceof ImageInputStream) {
try {
jpegData = bufferFromStream((ImageInputStream) input);
info = lib.getInfo(jpegData.array());
} catch (IOException e) {
throw new IllegalArgumentException("Failed to read input.");
} catch (TurboJpegException e) {
throw new IllegalArgumentException("Failed to read JPEG info.");
}
} else {
throw new IllegalArgumentException("Bad input.");
}
}
private void checkIndex(int imageIndex) {
if (imageIndex >= info.getAvailableSizes().size()) {
throw new IndexOutOfBoundsException("bad index");
}
}
private ByteBuffer bufferFromStream(ImageInputStream stream) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
final byte[] buf = new byte[8192];
int n;
while (0 < (n = stream.read(buf))) {
bos.write(buf, 0, n);
}
return ByteBuffer.wrap(bos.toByteArray());
}
@Override
public ImageReadParam getDefaultReadParam() {
return new TurboJpegImageReadParam();
}
/** The number of images corresponds to the number of different resolutions that can be directly decoded. */
@Override
public int getNumImages(boolean allowSearch) throws IOException {
return info.getAvailableSizes().size();
}
@Override
public int getWidth(int imageIndex) throws IOException {
checkIndex(imageIndex);
return info.getAvailableSizes().get(imageIndex).width;
}
@Override
public int getHeight(int imageIndex) throws IOException {
return info.getAvailableSizes().get(imageIndex).height;
}
@Override
public Iterator getImageTypes(int imageIndex) throws IOException {
return Stream.of(TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR, TYPE_4BYTE_ABGR_PRE, TYPE_BYTE_GRAY)
.map(ImageTypeSpecifier::createFromBufferedImageType)
.iterator();
}
/** Since TurboJPEG can only crop to values divisible by the MCU size, we may need to
* expand the cropping area to get a suitable rectangle.
* Thus, cropping becomes a two-stage process:
* - Crop to to nearest MCU boundaries (TurboJPEG)
* - Crop to the actual region (Java)
*
* Additionally, since TurboJPEG applies rotation **before** cropping, but the ImageIO API is based on the
* assumption that rotation occurs **after** cropping, we have to transform the cropping region accordingly.
*
* @param mcuSize The size of the MCUs
* @param region The source region to be cropped
* @return The region that needs to be cropped from the image cropped to the expanded rectangle
*/
private Rectangle adjustRegion(Dimension mcuSize, Rectangle region, int rotation) throws IOException {
if (region == null) {
return null;
}
boolean modified = false;
int originalWidth = getWidth(0);
int originalHeight = getHeight(0);
if (rotation == 90) {
int x = region.x;
region.x = originalHeight - region.height - region.y;
region.y = x;
}
if (rotation == 180) {
region.x = originalWidth - region.width - region.x;
region.y = originalHeight - region.height - region.y;
}
if (rotation == 270) {
int x = region.x;
region.x = region.y;
region.y = originalWidth - region.width - x;
}
if (rotation == 90 || rotation == 270) {
int w = region.width;
region.width = region.height;
region.height = w;
}
Rectangle extraCrop = new Rectangle(
0, 0,
region.width == 0 ? originalWidth - region.x : region.width,
region.height == 0 ? originalHeight - region.y : region.height);
if (region.x % mcuSize.width != 0) {
extraCrop.x = region.x % mcuSize.width;
region.x -= extraCrop.x;
if (region.width > 0) {
region.width += extraCrop.x;
}
modified = true;
}
if (region.y % mcuSize.height != 0) {
extraCrop.y = region.y % mcuSize.height;
region.y -= extraCrop.y;
if (region.height > 0) {
region.height += extraCrop.y;
}
modified = true;
}
if (region.width % mcuSize.width != 0) {
region.width = (int) (mcuSize.width*(Math.ceil(region.getWidth() / mcuSize.width)));
modified = true;
}
if (region.height % mcuSize.height != 0) {
region.height = (int) (mcuSize.height*(Math.ceil(region.getHeight() / mcuSize.height)));
modified = true;
}
if (modified) {
return extraCrop;
} else {
return null;
}
}
/** While the regular cropping parameters are applied to the unscaled source image, the additional extra cropping
* on the Java side of things is applied to the decoded and possibly scaled image. Thus, we need to scale down the
* extra cropping rectangle. */
private void adjustExtraCrop(int imageIndex, Info croppedInfo, Rectangle rectangle) {
double factor = croppedInfo.getAvailableSizes().get(imageIndex).getWidth() / croppedInfo.getAvailableSizes().get(0).getWidth();
if (factor < 1) {
rectangle.x = (int) Math.round(factor * rectangle.x);
rectangle.y = (int) Math.round(factor * rectangle.y);
rectangle.width = (int) Math.round(factor * rectangle.width);
rectangle.height = (int) Math.round(factor * rectangle.height);
}
int maxWidth = croppedInfo.getAvailableSizes().get(imageIndex).width;
int maxHeight = croppedInfo.getAvailableSizes().get(imageIndex).height;
if (rectangle.x + rectangle.width > maxWidth) {
rectangle.width = maxWidth - rectangle.x;
}
if (rectangle.y + rectangle.height > maxHeight) {
rectangle.height = maxHeight - rectangle.y;
}
}
/** The incoming cropping request always targets a specific resolution (i.e. downscaled if targetIndex > 0).
* However, TurobJPEG requires the cropping region to target the source resolution. Thus, we need to upscale
* the region passed by the user if the index != 0
*
* @param targetIndex Index of the targeted image resolution
* @param sourceRegion Region relative to the targeted image resolution, will be modified
* @throws IOException
*/
private void scaleRegion(int targetIndex, Rectangle sourceRegion) throws IOException {
if (targetIndex == 0) {
return;
}
int nativeWidth = getWidth(0);
int nativeHeight = getHeight(0);
double scaleFactor = (double) nativeWidth / (double) getWidth(targetIndex);
sourceRegion.x = (int) Math.ceil(scaleFactor * sourceRegion.x);
sourceRegion.y = (int) Math.ceil(scaleFactor * sourceRegion.y);
sourceRegion.width = Math.min((int) Math.ceil(scaleFactor * sourceRegion.width), nativeWidth - sourceRegion.x);
sourceRegion.height = Math.min((int) Math.ceil(scaleFactor * sourceRegion.height), nativeHeight - sourceRegion.y);
}
@Override
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
checkIndex(imageIndex);
ByteBuffer data = jpegData;
try {
int rotation = 0;
Rectangle region = null;
Rectangle extraCrop = null;
if (param instanceof TurboJpegImageReadParam) {
rotation = ((TurboJpegImageReadParam) param).getRotationDegree();
}
if (param != null && param.getSourceRegion() != null) {
region = param.getSourceRegion();
scaleRegion(imageIndex, region);
if (!isRegionFullImage(imageIndex, region)) {
extraCrop = adjustRegion(info.getMCUSize(), region, rotation);
} else {
region = null;
}
}
if (region != null || rotation != 0) {
data = lib.transform(data.array(), info, region, rotation);
}
Info transformedInfo = lib.getInfo(data.array());
BufferedImage img = lib.decode(
data.array(), transformedInfo, transformedInfo.getAvailableSizes().get(imageIndex));
if (extraCrop != null) {
adjustExtraCrop(imageIndex, transformedInfo, extraCrop);
img = img.getSubimage(extraCrop.x, extraCrop.y, extraCrop.width, extraCrop.height);
}
return img;
} catch (TurboJpegException e) {
throw new IOException(e);
}
}
private boolean isRegionFullImage(int imageIndex, Rectangle region) throws IOException {
int nativeWidth = getWidth(imageIndex);
int nativeHeight = getHeight(imageIndex);
return region.x == 0 && region.y == 0
&& (region.width == 0 || region.width == nativeWidth)
&& (region.height == 0 || region.height == nativeHeight);
}
@Override
public IIOMetadata getStreamMetadata() throws IOException {
return null;
}
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return null;
}
}