package org.smartly.commons.image; import org.apache.commons.imaging.ImageReadException; import org.apache.commons.imaging.ImageWriteException; import org.apache.commons.imaging.Imaging; import org.apache.commons.imaging.ImagingConstants; import org.apache.commons.imaging.common.IBufferedImageFactory; import org.apache.commons.imaging.common.IImageMetadata; import org.apache.commons.imaging.common.RationalNumber; import org.apache.commons.imaging.common.bytesource.ByteSource; import org.apache.commons.imaging.common.bytesource.ByteSourceFile; import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; import org.apache.commons.imaging.formats.jpeg.JpegImageParser; import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter; import org.apache.commons.imaging.formats.jpeg.segments.GenericSegment; import org.apache.commons.imaging.formats.jpeg.segments.Segment; import org.apache.commons.imaging.formats.tiff.TiffField; import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants; import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; import org.smartly.commons.lang.Base64; import org.smartly.commons.util.ByteUtils; import org.smartly.commons.util.PathUtils; import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; import java.awt.image.ColorConvertOp; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.io.*; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * */ public class ImageIOUtils { public static final int COLOR_TYPE_RGB = 1; public static final int COLOR_TYPE_CMYK = 2; public static final int COLOR_TYPE_YCCK = 3; private static final String ISO_COATED_V2_300 = "/eci/ISOcoated_v2_300_eci.icc"; // -------------------------------------------------------------------- // p u b l i c // -------------------------------------------------------------------- /** * Read all formats included CYMK * * @param file * @return * @throws IOException * @throws ImageReadException */ public static BufferedImage readExt(final File file) throws IOException, ImageReadException { final ImageReaderExt reader = new ImageReaderExt(); return reader.readImage(file); } /** * Read standard formats * * @param file * @return * @throws ImageReadException * @throws IOException */ public static BufferedImage read(final File file) throws ImageReadException, IOException { final Map<String, Object> params = new HashMap<String, Object>(); // set optional parameters if you like params.put(ImagingConstants.BUFFERED_IMAGE_FACTORY, new ManagedImageBufferedImageFactory()); // params.put(ImagingConstants.PARAM_KEY_VERBOSE, Boolean.TRUE); // read image final BufferedImage image = Imaging.getBufferedImage(file, params); return image; } public static String readBase64(final File file) throws IOException, ImageReadException { final BufferedImage image = readExt(file); final String format = PathUtils.getFilenameExtension(file.getAbsolutePath(), false); return readBase64(image, format); } public static String readBase64(final BufferedImage image, final String format) throws IOException { return Base64.encodeBytes(ByteUtils.getBytes(image, format)); } public static ImageSize getImageSize(final File file) { final ImageSize size = new ImageSize(); try { final BufferedImage image = readExt(file); if (null != image) { size.setWidth(image.getWidth()); size.setHeight(image.getHeight()); } } catch (Throwable ignored) { } return size; } public static void metadataExample(final File file) throws ImageReadException, IOException { // get all metadata stored in EXIF format (ie. from JPEG or TIFF). final IImageMetadata metadata = Imaging.getMetadata(file); // System.out.println(metadata); if (metadata instanceof JpegImageMetadata) { final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; // Jpeg EXIF metadata is stored in a TIFF-based directory structure // and is identified with TIFF tags. // Here we look for the "x resolution" tag, but // we could just as easily search for any other tag. // // see the TiffConstants file for a list of TIFF tags. System.out.println("file: " + file.getPath()); // print out various interesting EXIF tags. printTagValue(jpegMetadata, TiffTagConstants.TIFF_TAG_XRESOLUTION); printTagValue(jpegMetadata, TiffTagConstants.TIFF_TAG_DATE_TIME); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_DATE_TIME_DIGITIZED); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_ISO); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_SHUTTER_SPEED_VALUE); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_APERTURE_VALUE); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_BRIGHTNESS_VALUE); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LATITUDE); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LONGITUDE); System.out.println(); // simple interface to GPS data final TiffImageMetadata exifMetadata = jpegMetadata.getExif(); if (null != exifMetadata) { final TiffImageMetadata.GPSInfo gpsInfo = exifMetadata.getGPS(); if (null != gpsInfo) { final String gpsDescription = gpsInfo.toString(); final double longitude = gpsInfo.getLongitudeAsDegreesEast(); final double latitude = gpsInfo.getLatitudeAsDegreesNorth(); System.out.println(" " + "GPS Description: " + gpsDescription); System.out.println(" " + "GPS Longitude (Degrees East): " + longitude); System.out.println(" " + "GPS Latitude (Degrees North): " + latitude); } } // more specific example of how to manually access GPS values final TiffField gpsLatitudeRefField = jpegMetadata .findEXIFValueWithExactMatch(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); final TiffField gpsLatitudeField = jpegMetadata .findEXIFValueWithExactMatch(GpsTagConstants.GPS_TAG_GPS_LATITUDE); final TiffField gpsLongitudeRefField = jpegMetadata .findEXIFValueWithExactMatch(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); final TiffField gpsLongitudeField = jpegMetadata .findEXIFValueWithExactMatch(GpsTagConstants.GPS_TAG_GPS_LONGITUDE); if (gpsLatitudeRefField != null && gpsLatitudeField != null && gpsLongitudeRefField != null && gpsLongitudeField != null) { // all of these values are strings. final String gpsLatitudeRef = (String) gpsLatitudeRefField.getValue(); final RationalNumber gpsLatitude[] = (RationalNumber[]) (gpsLatitudeField .getValue()); final String gpsLongitudeRef = (String) gpsLongitudeRefField .getValue(); final RationalNumber gpsLongitude[] = (RationalNumber[]) gpsLongitudeField .getValue(); final RationalNumber gpsLatitudeDegrees = gpsLatitude[0]; final RationalNumber gpsLatitudeMinutes = gpsLatitude[1]; final RationalNumber gpsLatitudeSeconds = gpsLatitude[2]; final RationalNumber gpsLongitudeDegrees = gpsLongitude[0]; final RationalNumber gpsLongitudeMinutes = gpsLongitude[1]; final RationalNumber gpsLongitudeSeconds = gpsLongitude[2]; // This will format the gps info like so: // // gpsLatitude: 8 degrees, 40 minutes, 42.2 seconds S // gpsLongitude: 115 degrees, 26 minutes, 21.8 seconds E System.out.println(" " + "GPS Latitude: " + gpsLatitudeDegrees.toDisplayString() + " degrees, " + gpsLatitudeMinutes.toDisplayString() + " minutes, " + gpsLatitudeSeconds.toDisplayString() + " seconds " + gpsLatitudeRef); System.out.println(" " + "GPS Longitude: " + gpsLongitudeDegrees.toDisplayString() + " degrees, " + gpsLongitudeMinutes.toDisplayString() + " minutes, " + gpsLongitudeSeconds.toDisplayString() + " seconds " + gpsLongitudeRef); } System.out.println(); final List<IImageMetadata.IImageMetadataItem> items = jpegMetadata.getItems(); for (int i = 0; i < items.size(); i++) { final IImageMetadata.IImageMetadataItem item = items.get(i); System.out.println(" " + "item: " + item); } System.out.println(); } } private static void printTagValue(final JpegImageMetadata jpegMetadata, final TagInfo tagInfo) { final TiffField field = jpegMetadata.findEXIFValueWithExactMatch(tagInfo); if (field == null) { System.out.println(tagInfo.name + ": " + "Not Found."); } else { System.out.println(tagInfo.name + ": " + field.getValueDescription()); } } /** * This example illustrates how to set the GPS values in JPEG EXIF metadata. * * @param jpegImageFile A source image file. * @param dst The output file. * @throws IOException * @throws ImageReadException * @throws ImageWriteException */ public void setExifGPSTag(final File jpegImageFile, final File dst, final double latitude, final double longitude) throws IOException, ImageReadException, ImageWriteException { OutputStream os = null; boolean canThrow = false; try { TiffOutputSet outputSet = null; // note that metadata might be null if no metadata is found. final IImageMetadata metadata = Imaging.getMetadata(jpegImageFile); final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; if (null != jpegMetadata) { // note that exif might be null if no Exif metadata is found. final TiffImageMetadata exif = jpegMetadata.getExif(); if (null != exif) { // TiffImageMetadata class is immutable (read-only). // TiffOutputSet class represents the Exif data to write. // // Usually, we want to update existing Exif metadata by // changing // the values of a few fields, or adding a field. // In these cases, it is easiest to use getOutputSet() to // start with a "copy" of the fields read from the image. outputSet = exif.getOutputSet(); } } // if file does not contain any exif metadata, we create an empty // set of exif metadata. Otherwise, we keep all of the other // existing tags. if (null == outputSet) { outputSet = new TiffOutputSet(); } { // Example of how to add/update GPS info to output set. // New York City // final double longitude = -74.0; // 74 degrees W (in Degrees East) // final double latitude = 40 + 43 / 60.0; // 40 degrees N (in Degrees // North) outputSet.setGPSInDegrees(longitude, latitude); } os = new FileOutputStream(dst); os = new BufferedOutputStream(os); new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os, outputSet); canThrow = true; } finally { try { os.close(); } catch (Throwable ignored) { } //IoUtils.closeQuietly(canThrow, os); } } // -------------------------------------------------------------------- // p r i v a t e // -------------------------------------------------------------------- private static void convertYcckToCmyk(final WritableRaster raster) { int height = raster.getHeight(); int width = raster.getWidth(); int stride = width * 4; int[] pixelRow = new int[stride]; for (int h = 0; h < height; h++) { raster.getPixels(0, h, width, 1, pixelRow); for (int x = 0; x < stride; x += 4) { int y = pixelRow[x]; int cb = pixelRow[x + 1]; int cr = pixelRow[x + 2]; int c = (int) (y + 1.402 * cr - 178.956); int m = (int) (y - 0.34414 * cb - 0.71414 * cr + 135.95984); y = (int) (y + 1.772 * cb - 226.316); if (c < 0) c = 0; else if (c > 255) c = 255; if (m < 0) m = 0; else if (m > 255) m = 255; if (y < 0) y = 0; else if (y > 255) y = 255; pixelRow[x] = 255 - c; pixelRow[x + 1] = 255 - m; pixelRow[x + 2] = 255 - y; } raster.setPixels(0, h, width, 1, pixelRow); } } private static void convertInvertedColors(final WritableRaster raster) { int height = raster.getHeight(); int width = raster.getWidth(); int stride = width * 4; int[] pixelRow = new int[stride]; for (int h = 0; h < height; h++) { raster.getPixels(0, h, width, 1, pixelRow); for (int x = 0; x < stride; x++) pixelRow[x] = 255 - pixelRow[x]; raster.setPixels(0, h, width, 1, pixelRow); } } private static BufferedImage convertCmykToRgb(final Raster cmykRaster, ICC_Profile cmykProfile) throws IOException { if (cmykProfile == null) cmykProfile = ICC_Profile.getInstance(ImageIOUtils.class.getResourceAsStream(ISO_COATED_V2_300)); if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) { byte[] profileData = cmykProfile.getData(); if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) { intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first cmykProfile = ICC_Profile.getInstance(profileData); } } ICC_ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile); BufferedImage rgbImage = new BufferedImage(cmykRaster.getWidth(), cmykRaster.getHeight(), BufferedImage.TYPE_INT_RGB); WritableRaster rgbRaster = rgbImage.getRaster(); ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace(); ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null); cmykToRgb.filter(cmykRaster, rgbRaster); return rgbImage; } private static void intToBigEndian(int value, byte[] array, int index) { array[index] = (byte) (value >> 24); array[index + 1] = (byte) (value >> 16); array[index + 2] = (byte) (value >> 8); array[index + 3] = (byte) (value); } // -------------------------------------------------------------------- // E M B E D D E D // -------------------------------------------------------------------- private static class ImageReaderExt { private int colorType = COLOR_TYPE_RGB; private boolean hasAdobeMarker = false; public BufferedImage readImage(final File file) throws IOException, ImageReadException { colorType = COLOR_TYPE_RGB; hasAdobeMarker = false; final ImageInputStream stream = ImageIO.createImageInputStream(file); final Iterator<ImageReader> iter = ImageIO.getImageReaders(stream); while (iter.hasNext()) { ImageReader reader = iter.next(); reader.setInput(stream); BufferedImage image; ICC_Profile profile = null; try { image = reader.read(0); } catch (IIOException e) { colorType = COLOR_TYPE_CMYK; checkAdobeMarker(file); profile = Imaging.getICCProfile(file); WritableRaster raster = (WritableRaster) reader.readRaster(0, null); if (colorType == COLOR_TYPE_YCCK) convertYcckToCmyk(raster); if (hasAdobeMarker) convertInvertedColors(raster); image = convertCmykToRgb(raster, profile); } return image; } return null; } public void checkAdobeMarker(final File file) throws IOException, ImageReadException { final JpegImageParser parser = new JpegImageParser(); final ByteSource byteSource = new ByteSourceFile(file); final List<Segment> segments = parser.readSegments(byteSource, new int[]{0xffee}, true); if (segments != null && segments.size() >= 1) { final Segment segment = segments.get(0); GenericSegment app14Segment = (GenericSegment) segment; byte[] data = app14Segment.bytes; if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e') { hasAdobeMarker = true; int transform = app14Segment.bytes[11] & 0xff; if (transform == 2) colorType = COLOR_TYPE_YCCK; } } } } private static class ManagedImageBufferedImageFactory implements IBufferedImageFactory { public BufferedImage getColorBufferedImage(final int width, final int height, final boolean hasAlpha) { final GraphicsEnvironment ge = GraphicsEnvironment .getLocalGraphicsEnvironment(); final GraphicsDevice gd = ge.getDefaultScreenDevice(); final GraphicsConfiguration gc = gd.getDefaultConfiguration(); return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT); } public BufferedImage getGrayscaleBufferedImage(final int width, final int height, final boolean hasAlpha) { return getColorBufferedImage(width, height, hasAlpha); } } }