All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.digitalcollections.turbojpeg.imageio.TurboJpegImageReader Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 0.6.8
Show newest version
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)
   *
   * @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 || rotation == 270) {
      int w = region.width;
      region.width = region.height;
      region.height = w;
      int ow = originalWidth;
      originalWidth = originalHeight;
      originalHeight = ow;
    }
    if (rotation == 270) {
      int x = region.x;
      region.x = region.y;
      region.y = x;
    }
    if (rotation == 90) {
      int x = region.x;
      region.x = originalWidth - region.y - region.width;
      region.y = x;
    }
    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 (region.x + region.width == getWidth(0)) {
          region.width = 0;
        }
        if (region.y + region.height == getHeight(0)) {
          region.height = 0;
        }
        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 {
    return (region.x == 0 && region.y == 0 && region.width == 0 && region.height == 0);
  }

  @Override
  public IIOMetadata getStreamMetadata() throws IOException {
    return null;
  }

  @Override
  public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
    return null;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy