package org.fluxtream.core.utils;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.lang.GeoLocation;
import com.drew.lang.Rational;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.GpsDescriptor;
import com.drew.metadata.exif.GpsDirectory;
import org.fluxtream.core.domain.Geolocation;
import org.fluxtream.core.images.Image;
import org.fluxtream.core.images.ImageOrientation;
import org.fluxtream.core.images.ImageType;
import org.fluxtream.core.images.JpegImage;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.fluxtream.core.aspects.FlxLogger;
import org.imgscalr.Scalr;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* <p>
* <code>ImageUtils</code> provides helpful methods for dealing with images.
* </p>
*
* @author Chris Bartley (bartley@cmu.edu)
*/
public final class ImageUtils {
private static final FlxLogger LOG = FlxLogger.getLogger(ImageUtils.class);
private static final class GeolocationImpl implements Geolocation {
@Nullable
private final Double latitude;
@Nullable
private final Double longitude;
@Nullable
private final Float heading;
@Nullable
private final String headingRef;
@Nullable
private final Float altitude;
@Nullable
private final Integer altitudeRef;
@Nullable
private final Float precision;
@Nullable
private final String gpsDatestamp;
@Nullable
private final String gpsTimestamp;
private GeolocationImpl(@NotNull final GpsDirectory gpsDirectory) {
final String latitudeRef = gpsDirectory.getString(GpsDirectory.TAG_GPS_LATITUDE_REF);
final String longitudeRef = gpsDirectory.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF);
final Rational[] latitudeRationals = gpsDirectory.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
final Rational[] longitudeRationals = gpsDirectory.getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
latitude = (latitudeRationals == null) ? null : GeoLocation.degreesMinutesSecondsToDecimal(latitudeRationals[0], latitudeRationals[1], latitudeRationals[2], "S".equals(latitudeRef));
longitude = (longitudeRationals == null) ? null : GeoLocation.degreesMinutesSecondsToDecimal(longitudeRationals[0], longitudeRationals[1], longitudeRationals[2], "W".equals(longitudeRef));
final Rational headingRational = gpsDirectory.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION);
heading = (headingRational == null) ? null : headingRational.floatValue();
headingRef = gpsDirectory.getString(GpsDirectory.TAG_GPS_IMG_DIRECTION_REF);
final Rational altitudeRational = gpsDirectory.getRational(GpsDirectory.TAG_GPS_ALTITUDE);
altitude = (altitudeRational == null) ? null : altitudeRational.floatValue();
Integer altitudeRefInteger;
try {
altitudeRefInteger = gpsDirectory.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF);
}
catch (MetadataException e) {
altitudeRefInteger = null;
}
altitudeRef = altitudeRefInteger;
final Rational precisionRational = gpsDirectory.getRational(GpsDirectory.TAG_GPS_DOP);
precision = (precisionRational == null) ? null : precisionRational.floatValue();
gpsDatestamp = gpsDirectory.getString(GpsDirectory.TAG_GPS_DATE_STAMP);
final GpsDescriptor gpsDescriptor = new GpsDescriptor(gpsDirectory);
gpsTimestamp = gpsDescriptor.getGpsTimeStampDescription();
}
@Override
@Nullable
public Double getLatitude() {
return latitude;
}
@Override
@Nullable
public Double getLongitude() {
return longitude;
}
@Override
@Nullable
public Float getHeading() {
return heading;
}
@Override
@Nullable
public String getHeadingRef() {
return headingRef;
}
@Override
@Nullable
public Float getAltitude() {
return altitude;
}
@Override
@Nullable
public Integer getAltitudeRef() {
return altitudeRef;
}
@Override
@Nullable
public Float getGpsPrecision() {
return precision;
}
@Override
@Nullable
public String getGpsDatestamp() {
return gpsDatestamp;
}
@Override
@Nullable
public String getGpsTimestamp() {
return gpsTimestamp;
}
}
/**
* Tries to read the given <code>imageBytes</code> and returns <code>true</code> if it's a GIF, JPEG, or PNG image;
* returns <code>false</code> otherwise. Returns <code>false</code> if the given byte array is <code>null</code> or
* empty.
*/
public static boolean isSupportedImage(@Nullable final byte[] imageBytes) {
return getImageType(imageBytes) != null;
}
/**
* Tries to create a JPEG thumbnail of the given image with the given desired dimensions. Returns <code>null</code>
* if the given byte array is <code>null</code> or empty, if the desired <code>lengthOfLongestSideInPixels</code> is
* zero or negative, or if the bytes cannot be read as an image. This method will attempt to read orientation info
* from the EXIF and, if necessary, rotate/flip the thumbnail appropriately. Note that if the image has an alpha
* channel, it will be discarded before generating the thumbnail.
*
* @throws IOException if a problem occurs while reading the image or generating the thumbnail
*/
@Nullable
public static Image createJpegThumbnail(@Nullable final byte[] imageBytes, final int lengthOfLongestSideInPixels) throws IOException {
if (imageBytes != null && imageBytes.length > 0 && lengthOfLongestSideInPixels > 0) {
ImageOrientation orientation = ImageOrientation.getOrientation(new ByteArrayInputStream(imageBytes));
if (orientation == null) {
orientation = ImageOrientation.ORIENTATION_1;
}
try {
BufferedImage image = convertToBufferedImage(imageBytes);
if (image != null) {
// drop the alpha channel, if one exists
if (image.getColorModel().hasAlpha()) {
image = dropAlphaChannel(image);
}
return JpegImage.create(orientation.transform(Scalr.resize(image, Scalr.Method.AUTOMATIC, Scalr.Mode.AUTOMATIC, lengthOfLongestSideInPixels)));
}
}
catch (Exception e) {
final String message = "Exception while trying to create a thumbnail";
LOG.error(message, e);
throw new IOException(e);
}
}
return null;
}
// I stole this from: https://github.com/thebuzzmedia/imgscalr/issues/82#issuecomment-11776976
@NotNull
private static BufferedImage dropAlphaChannel(@NotNull final BufferedImage srcImage) {
final BufferedImage convertedImg = new BufferedImage(srcImage.getWidth(), srcImage.getHeight(), BufferedImage.TYPE_INT_RGB);
convertedImg.getGraphics().drawImage(srcImage, 0, 0, null);
return convertedImg;
}
@Nullable
public static Geolocation getGeolocation(@Nullable final byte[] imageBytes) throws ImageProcessingException, IOException {
if (imageBytes != null && imageBytes.length > 0) {
final Metadata metadata = ImageMetadataReader.readMetadata(new BufferedInputStream(new ByteArrayInputStream(imageBytes)), false);
if (metadata != null) {
final GpsDirectory gpsDirectory = metadata.getDirectory(GpsDirectory.class);
if (gpsDirectory != null) {
return new GeolocationImpl(gpsDirectory);
}
}
}
return null;
}
@Nullable
public static ImageOrientation getOrientation(@Nullable final byte[] imageBytes) {
if (imageBytes != null && imageBytes.length > 0) {
return ImageOrientation.getOrientation(new ByteArrayInputStream(imageBytes));
}
return null;
}
@Nullable
public static BufferedImage convertToBufferedImage(@Nullable final byte[] imageBytes) throws IOException {
if (imageBytes != null && imageBytes.length > 0) {
return ImageIO.read(new ByteArrayInputStream(imageBytes));
}
return null;
}
@Nullable
public static byte[] convertToJpegByteArray(@Nullable final BufferedImage image) throws IOException {
if (image != null) {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (ImageIO.write(image, "JPG", byteArrayOutputStream)) {
return byteArrayOutputStream.toByteArray();
}
}
return null;
}
/**
* Returns the {@link ImageType} of the given image, or <code>null</code> if the type is unknown, not supported, or
* if an error occurs while trying to read the image. Returns <code>null</code> if the given image byte array is
* <code>null</code> or empty.
*/
@Nullable
public static ImageType getImageType(@Nullable final byte[] imageBytes) {
if ((imageBytes != null) && (imageBytes.length > 0)) {
try {
final ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(imageBytes));
final Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(iis);
if (imageReaders != null) {
while (imageReaders.hasNext()) {
final ImageReader reader = imageReaders.next();
final ImageType imageType = ImageType.findByFormatName(reader.getFormatName());
if (imageType != null) {
return imageType;
}
}
}
}
catch (IOException e) {
LOG.error("IOException while trying to read the image type, returning null");
}
}
return null;
}
private ImageUtils() {
// private to prevent instantiation
}
}