package org.fluxtream.core.connectors.fluxtream_capture; import java.awt.Dimension; import java.io.IOException; import java.security.NoSuchAlgorithmException; import com.drew.imaging.ImageProcessingException; import org.fluxtream.core.connectors.Connector; 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.utils.HashUtils; import org.fluxtream.core.utils.ImageUtils; import org.fluxtream.core.aspects.FlxLogger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; /** * @author Chris Bartley (bartley@cmu.edu) */ public final class FluxtreamCapturePhoto { private static final FlxLogger logger = FlxLogger.getLogger(FluxtreamCapturePhoto.class); public static class PhotoUploadMetadata { private double capture_time_secs_utc = -1; @Nullable private String comment; @Nullable private String tags; public boolean isValid() { return capture_time_secs_utc >= 0; } public long getCaptureTimeMillisUtc() { // convert the capture time from seconds to milliseconds return (long)(capture_time_secs_utc * 1000); } public double getCaptureTimeSecsUtc() { return capture_time_secs_utc; } @Nullable public String getComment() { return comment; } @Nullable public String getTags() { return tags; } } private static final int THUMBNAIL_0_MAX_SIDE_LENGTH_IN_PIXELS = 150; private static final int THUMBNAIL_1_MAX_SIDE_LENGTH_IN_PIXELS = 300; private static final int THUMBNAIL_2_MAX_SIDE_LENGTH_IN_PIXELS = 500; private static final String KEY_VALUE_STORE_KEY_PART_DELIMITER = "."; private static final String KEY_VALUE_STORE_FILENAME_PART_DELIMITER = "_"; private static final String CONNECTOR_PRETTY_NAME = Connector.getConnector("fluxtream_capture").prettyName(); private static final String OBJECT_TYPE_NAME = "photo"; @NotNull public static String createPhotoStoreKey(final long guestId, @NotNull final String captureYYYYDDD, final long captureTimeMillisUtc, @NotNull final String photoHash) { return guestId + KEY_VALUE_STORE_KEY_PART_DELIMITER + CONNECTOR_PRETTY_NAME + KEY_VALUE_STORE_KEY_PART_DELIMITER + OBJECT_TYPE_NAME + KEY_VALUE_STORE_KEY_PART_DELIMITER + captureYYYYDDD + KEY_VALUE_STORE_KEY_PART_DELIMITER + captureTimeMillisUtc + KEY_VALUE_STORE_FILENAME_PART_DELIMITER + photoHash; } private final long guestId; @NotNull private final byte[] photoBytes; private final long captureTimeMillisUtc; @NotNull private final String captureYYYYDDD; @NotNull private final String photoHash; @NotNull private final String photoStoreKey; @NotNull private final byte[] thumbnail0; @NotNull private final byte[] thumbnail1; @NotNull private final byte[] thumbnail2; @NotNull private final Dimension thumbnail0Size; @NotNull private final Dimension thumbnail1Size; @NotNull private final Dimension thumbnail2Size; @NotNull private final ImageOrientation orientation; @NotNull private final ImageType imageType; @Nullable private final Geolocation geolocation; @Nullable private final String tags; @Nullable private final String comment; FluxtreamCapturePhoto(final long guestId, @NotNull final byte[] photoBytes, @NotNull final PhotoUploadMetadata photoUploadMetadata) throws IllegalArgumentException, NoSuchAlgorithmException, IOException, FluxtreamCapturePhotoStore.UnsupportedImageFormatException { // Get the image type. If this is null, then it's not a supported type. final ImageType tempImageType = ImageUtils.getImageType(photoBytes); if (tempImageType == null) { throw new FluxtreamCapturePhotoStore.UnsupportedImageFormatException("The photoBytes do not contain a supported image format"); } imageType = tempImageType; if (photoUploadMetadata.getCaptureTimeSecsUtc() < 0) { throw new IllegalArgumentException("The captureTimeMillisUtc must be non-negative"); } this.guestId = guestId; this.photoBytes = photoBytes; this.captureTimeMillisUtc = photoUploadMetadata.getCaptureTimeMillisUtc(); this.tags = photoUploadMetadata.getTags(); this.comment = photoUploadMetadata.getComment(); final DateTime captureTime = new DateTime(captureTimeMillisUtc, DateTimeZone.UTC); final String year = String.valueOf(captureTime.getYear()); final String dayOfYear = String.format("%03d", captureTime.getDayOfYear()); // pad with zeros so that it's always 3 characters captureYYYYDDD = year + dayOfYear; this.photoHash = HashUtils.computeSha256Hash(photoBytes); photoStoreKey = createPhotoStoreKey(guestId, captureYYYYDDD, captureTimeMillisUtc, photoHash); // Create the thumbnails: do so by creating the largest one first, and then creating the smaller // ones from the larger--this should be faster than creating each from the original image final Image thumbnail2Image = ImageUtils.createJpegThumbnail(photoBytes, THUMBNAIL_2_MAX_SIDE_LENGTH_IN_PIXELS); if (thumbnail2Image == null) { throw new IOException("Failed to create thumbnails"); } final Image thumbnail1Image = ImageUtils.createJpegThumbnail(thumbnail2Image.getBytes(), THUMBNAIL_1_MAX_SIDE_LENGTH_IN_PIXELS); if (thumbnail1Image == null) { throw new IOException("Failed to create thumbnails"); } final Image thumbnail0Image = ImageUtils.createJpegThumbnail(thumbnail1Image.getBytes(), THUMBNAIL_0_MAX_SIDE_LENGTH_IN_PIXELS); if (thumbnail0Image == null) { throw new IOException("Failed to create thumbnails"); } thumbnail0 = thumbnail0Image.getBytes(); thumbnail1 = thumbnail1Image.getBytes(); thumbnail2 = thumbnail2Image.getBytes(); thumbnail0Size = new Dimension(thumbnail0Image.getWidth(), thumbnail0Image.getHeight()); thumbnail1Size = new Dimension(thumbnail1Image.getWidth(), thumbnail1Image.getHeight()); thumbnail2Size = new Dimension(thumbnail2Image.getWidth(), thumbnail2Image.getHeight()); // get the image orientation, and default to ORIENTATION_1 if unspecified ImageOrientation orientationTemp; try { orientationTemp = ImageUtils.getOrientation(photoBytes); } catch (Exception e) { logger.error("Exception while trying to read the orientation data for user [" + guestId + "] photo [" + photoStoreKey + "]"); orientationTemp = null; } orientation = (orientationTemp == null) ? ImageOrientation.ORIENTATION_1 : orientationTemp; Geolocation geolocationTemp; try { geolocationTemp = ImageUtils.getGeolocation(photoBytes); } catch (ImageProcessingException e) { logger.error("ImageProcessingException while trying to read the geolocation data for user [" + guestId + "] photo [" + photoStoreKey + "]"); geolocationTemp = null; } geolocation = geolocationTemp; } public long getGuestId() { return guestId; } @NotNull public byte[] getPhotoBytes() { return photoBytes; } @NotNull public String getPhotoHash() { return photoHash; } public long getCaptureTimeMillisUtc() { return captureTimeMillisUtc; } @NotNull public String getCaptureYYYYDDD() { return captureYYYYDDD; } @NotNull public String getPhotoStoreKey() { return photoStoreKey; } @NotNull public byte[] getThumbnail0() { return thumbnail0; } @NotNull public byte[] getThumbnail1() { return thumbnail1; } @NotNull public byte[] getThumbnail2() { return thumbnail2; } @NotNull public Dimension getThumbnail0Size() { return thumbnail0Size; } @NotNull public Dimension getThumbnail1Size() { return thumbnail1Size; } @NotNull public Dimension getThumbnail2Size() { return thumbnail2Size; } @NotNull public ImageOrientation getOrientation() { return orientation; } @NotNull public ImageType getImageType() { return imageType; } @Nullable public Geolocation getGeolocation() { return geolocation; } @Nullable public String getTags() { return tags; } @Nullable public String getComment() { return comment; } }