| /* |
| * Copyright (c) 2014-2021 by Wen Yu |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the Eclipse |
| * Public License, v. 2.0 are satisfied: GNU General Public License, version 2 |
| * or any later version. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later |
| * |
| * Change History - most recent changes go on top of previous changes |
| * |
| * JPGMeta.java |
| * |
| * Who Date Description |
| * ==== ======= =================================================== |
| * WY 21Jun2019 Added code to return the removed metadata as a map |
| * WY 13Feb2017 Fixed bug with APP1 segment length too small |
| * WY 06Nov2016 Added support for Cardboard Camera image and audio |
| * WY 07Apr2016 Rewrite insertXMP() to leverage JpegXMP |
| * WY 30Mar2016 Rewrite writeComment() to leverage COMBuilder |
| * WY 06Jul2015 Added insertXMP(InputSream, OutputStream, XMP) |
| * WY 02Jul2015 Added support for APP14 segment reading |
| * WY 02Jul2015 Added support for APP12 segment reading |
| * WY 01Jul2015 Added support for non-standard XMP identifier |
| * WY 15Apr2015 Changed the argument type for insertIPTC() and insertIRB() |
| * WY 07Apr2015 Revised insertExif() |
| * WY 01Apr2015 Extract IPTC as stand-alone meta data from IRB if any |
| * WY 18Mar2015 Revised readAPP13(), insertIPTC() and insertIRB() |
| * to work with multiple APP13 segments |
| * WY 18Mar2015 Removed a few unused readAPPn methods |
| * WY 16Mar2015 Revised insertExif() to put EXIF in the position |
| * conforming to EXIF specification or the same place |
| * the original image EXIF exists |
| * WY 13Mar2015 initial creation |
| */ |
| |
| package pixy.meta.jpeg; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| |
| import pixy.image.tiff.IFD; |
| import pixy.image.tiff.TiffTag; |
| import pixy.image.jpeg.COMBuilder; |
| import pixy.image.jpeg.Component; |
| import pixy.image.jpeg.DHTReader; |
| import pixy.image.jpeg.DQTReader; |
| import pixy.image.jpeg.HTable; |
| import pixy.image.jpeg.Marker; |
| import pixy.image.jpeg.QTable; |
| import pixy.image.jpeg.SOFReader; |
| import pixy.image.jpeg.SOSReader; |
| import pixy.image.jpeg.Segment; |
| import pixy.image.jpeg.UnknownSegment; |
| import pixy.io.FileCacheRandomAccessInputStream; |
| import pixy.io.IOUtils; |
| import pixy.io.RandomAccessInputStream; |
| import pixy.string.Base64; |
| import pixy.string.StringUtils; |
| import pixy.string.XMLUtils; |
| import pixy.util.ArrayUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.w3c.dom.Document; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.CompressFormat; |
| import pixy.meta.Metadata; |
| import pixy.meta.MetadataType; |
| import pixy.meta.Thumbnail; |
| import pixy.meta.adobe.IRB; |
| import pixy.meta.adobe.ImageResourceID; |
| import pixy.meta.adobe._8BIM; |
| import pixy.meta.exif.Exif; |
| import pixy.meta.exif.ExifTag; |
| import pixy.meta.exif.ExifThumbnail; |
| import pixy.meta.adobe.ThumbnailResource; |
| import pixy.meta.icc.ICCProfile; |
| import pixy.meta.image.ImageMetadata; |
| import pixy.meta.image.Comments; |
| import pixy.meta.iptc.IPTC; |
| import pixy.meta.iptc.IPTCDataSet; |
| import pixy.meta.iptc.IPTCTag; |
| import pixy.meta.xmp.XMP; |
| import pixy.util.MetadataUtils; |
| |
| import static pixy.image.jpeg.JPGConsts.*; |
| |
| /** |
| * JPEG image tweaking tool |
| * |
| * @author Wen Yu, yuwen_66@yahoo.com |
| * @version 1.0 01/25/2013 |
| */ |
| public class JPGMeta { |
| |
| public static final EnumSet<Marker> APPnMarkers = EnumSet.range(Marker.APP0, Marker.APP15); |
| |
| // Obtain a logger instance |
| private static final Logger LOGGER = LoggerFactory.getLogger(JPGMeta.class); |
| |
| private static short copySegment(short marker, InputStream is, OutputStream os) throws IOException { |
| int length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, buf); |
| |
| return (IOUtils.readShortMM(is)); |
| } |
| |
| /** Copy a single SOS segment */ |
| @SuppressWarnings("unused") |
| private static short copySOS(InputStream is, OutputStream os) throws IOException { |
| // Need special treatment. |
| int nextByte = 0; |
| short marker = 0; |
| |
| while((nextByte = IOUtils.read(is)) != -1) { |
| if(nextByte == 0xff) { |
| nextByte = IOUtils.read(is); |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| if (nextByte != 0x00) { // This is a marker |
| marker = (short)((0xff<<8)|nextByte); |
| |
| switch (Marker.fromShort(marker)) { |
| case RST0: |
| case RST1: |
| case RST2: |
| case RST3: |
| case RST4: |
| case RST5: |
| case RST6: |
| case RST7: |
| IOUtils.writeShortMM(os, marker); |
| continue; |
| default: |
| } |
| break; |
| } |
| IOUtils.write(os, 0xff); |
| IOUtils.write(os, nextByte); |
| } else { |
| IOUtils.write(os, nextByte); |
| } |
| } |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| return marker; |
| } |
| |
| private static void copyToEnd(InputStream is, OutputStream os) throws IOException { |
| byte[] buffer = new byte[10240]; // 10k buffer |
| int bytesRead = -1; |
| |
| while((bytesRead = is.read(buffer)) != -1) { |
| os.write(buffer, 0, bytesRead); |
| } |
| } |
| |
| public static byte[] extractICCProfile(InputStream is) throws IOException { |
| ByteArrayOutputStream bo = new ByteArrayOutputStream(); |
| // Flag when we are done |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.EOI) { |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| int nextByte = 0; |
| while((nextByte = IOUtils.read(is)) == 0xff) {;} |
| marker = (short)((0xff<<8)|nextByte); |
| break; |
| case SOS: |
| //marker = skipSOS(is); |
| finished = true; |
| break; |
| case APP2: |
| readAPP2(is, bo); |
| marker = IOUtils.readShortMM(is); |
| break; |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| |
| return bo.toByteArray(); |
| } |
| |
| public static void extractICCProfile(InputStream is, String pathToICCProfile) throws IOException { |
| byte[] icc_profile = extractICCProfile(is); |
| |
| if(icc_profile != null && icc_profile.length > 0) { |
| String outpath = ""; |
| if(pathToICCProfile.endsWith("\\") || pathToICCProfile.endsWith("/")) |
| outpath = pathToICCProfile + "icc_profile"; |
| else |
| outpath = pathToICCProfile.replaceFirst("[.][^.]+$", ""); |
| OutputStream os = new FileOutputStream(outpath + ".icc"); |
| os.write(icc_profile); |
| os.close(); |
| } |
| } |
| |
| // Extract depth map from google phones and cardboard camera audio & stereo pair |
| public static void extractDepthMap(InputStream is, String pathToDepthMap) throws IOException { |
| Map<MetadataType, Metadata> meta = readMetadata(is); |
| XMP xmp = (XMP)meta.get(MetadataType.XMP); |
| if(xmp != null) { |
| Document xmpDocument = xmp.getMergedDocument(); |
| String depthMapMime = XMLUtils.getAttribute(xmpDocument, "rdf:Description", "GDepth:Mime"); |
| String depthData = "GDepth:Data"; |
| String audioMime = XMLUtils.getAttribute(xmpDocument, "rdf:Description", "GAudio:Mime"); |
| if(StringUtils.isNullOrEmpty(depthMapMime)) { |
| depthMapMime = XMLUtils.getAttribute(xmpDocument, "rdf:Description", "GImage:Mime"); |
| depthData = "GImage:Data"; |
| } |
| if(!StringUtils.isNullOrEmpty(depthMapMime)) { |
| String data = XMLUtils.getAttribute(xmpDocument, "rdf:Description", depthData); |
| if(!StringUtils.isNullOrEmpty(data)) { |
| String outpath = ""; |
| if(pathToDepthMap.endsWith("\\") || pathToDepthMap.endsWith("/")) |
| outpath = pathToDepthMap + "google_depthmap"; |
| else |
| outpath = pathToDepthMap.replaceFirst("[.][^.]+$", "") + "_depthmap"; |
| if(depthMapMime.equalsIgnoreCase("image/png")) { |
| outpath += ".png"; |
| } else if(depthMapMime.equalsIgnoreCase("image/jpeg")) { |
| outpath += ".jpg"; |
| } |
| try { |
| byte[] image = Base64.decodeToByteArray(data); |
| FileOutputStream fout = new FileOutputStream(new File(outpath)); |
| fout.write(image); |
| fout.close(); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| if(!StringUtils.isNullOrEmpty(audioMime)) { // Cardboard Camera Audio |
| String data = XMLUtils.getAttribute(xmpDocument, "rdf:Description", "GAudio:Data"); |
| if(!StringUtils.isNullOrEmpty(data)) { |
| String outpath = ""; |
| if(pathToDepthMap.endsWith("\\") || pathToDepthMap.endsWith("/")) |
| outpath = pathToDepthMap + "google_cardboard_audio"; |
| else |
| outpath = pathToDepthMap.replaceFirst("[.][^.]+$", "") + "_cardboard_audio"; |
| if(audioMime.equalsIgnoreCase("audio/mp4a-latm")) { |
| outpath += ".mp4"; |
| } |
| try { |
| byte[] image = Base64.decodeToByteArray(data); |
| FileOutputStream fout = new FileOutputStream(new File(outpath)); |
| fout.write(image); |
| fout.close(); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| } |
| |
| private static void extractMetadataFromAPPn(Collection<Segment> appnSegments, Map<MetadataType, Metadata> metadataMap) throws IOException { |
| // Used to read multiple segment ICCProfile |
| ByteArrayOutputStream iccProfileStream = null; |
| |
| // Used to read multiple segment Adobe APP13 |
| ByteArrayOutputStream eightBIMStream = null; |
| |
| // Used to read multiple segment XMP |
| byte[] extendedXMP = null; |
| String xmpGUID = ""; // 32 byte ASCII hex string |
| |
| Map<String, Thumbnail> thumbnails = new HashMap<String, Thumbnail>(); |
| |
| for(Segment segment : appnSegments) { |
| byte[] data = segment.getData(); |
| int length = segment.getLength(); |
| if(segment.getMarker() == Marker.APP0) { |
| if (data.length >= JFIF_ID.length() && new String(data, 0, JFIF_ID.length()).equals(JFIF_ID)) { |
| metadataMap.put(MetadataType.JPG_JFIF, new JFIF(ArrayUtils.subArray(data, JFIF_ID.length(), length - JFIF_ID.length() - 2))); |
| } |
| } else if(segment.getMarker() == Marker.APP1) { |
| // Check for EXIF |
| if(data.length >= EXIF_ID.length() && new String(data, 0, EXIF_ID.length()).equals(EXIF_ID)) { |
| // We found EXIF |
| JpegExif exif = new JpegExif(ArrayUtils.subArray(data, EXIF_ID.length(), length - EXIF_ID.length() - 2)); |
| metadataMap.put(MetadataType.EXIF, exif); |
| } else if(data.length >= XMP_ID.length() && new String(data, 0, XMP_ID.length()).equals(XMP_ID) || |
| data.length >= NON_STANDARD_XMP_ID.length() && new String(data, 0, NON_STANDARD_XMP_ID.length()).equals(NON_STANDARD_XMP_ID)) { |
| // We found XMP, add it to metadata list (We may later revise it if we have ExtendedXMP) |
| XMP xmp = new JpegXMP(ArrayUtils.subArray(data, XMP_ID.length(), length - XMP_ID.length() - 2)); |
| metadataMap.put(MetadataType.XMP, xmp); |
| // Retrieve XMP GUID if available |
| xmpGUID = XMLUtils.getAttribute(xmp.getXmpDocument(), "rdf:Description", "xmpNote:HasExtendedXMP"); |
| } else if(data.length >= XMP_EXT_ID.length() && new String(data, 0, XMP_EXT_ID.length()).equals(XMP_EXT_ID)) { |
| // We found ExtendedXMP, add the data to ExtendedXMP memory buffer |
| int i = XMP_EXT_ID.length(); |
| // 128-bit MD5 digest of the full ExtendedXMP serialization |
| byte[] guid = ArrayUtils.subArray(data, i, 32); |
| if(Arrays.equals(guid, xmpGUID.getBytes())) { // We have matched the GUID, copy it |
| i += 32; |
| long extendedXMPLength = IOUtils.readUnsignedIntMM(data, i); |
| i += 4; |
| if(extendedXMP == null) |
| extendedXMP = new byte[(int)extendedXMPLength]; |
| // Offset for the current segment |
| long offset = IOUtils.readUnsignedIntMM(data, i); |
| i += 4; |
| byte[] xmpBytes = ArrayUtils.subArray(data, i, length - XMP_EXT_ID.length() - 42); |
| System.arraycopy(xmpBytes, 0, extendedXMP, (int)offset, xmpBytes.length); |
| } |
| } |
| } else if(segment.getMarker() == Marker.APP2) { |
| // We're only interested in ICC_Profile |
| if (data.length >= ICC_PROFILE_ID.length() && new String(data, 0, ICC_PROFILE_ID.length()).equals(ICC_PROFILE_ID)) { |
| if(iccProfileStream == null) |
| iccProfileStream = new ByteArrayOutputStream(); |
| iccProfileStream.write(ArrayUtils.subArray(data, ICC_PROFILE_ID.length() + 2, length - ICC_PROFILE_ID.length() - 4)); |
| } |
| } else if(segment.getMarker() == Marker.APP12) { |
| if (data.length >= DUCKY_ID.length() && new String(data, 0, DUCKY_ID.length()).equals(DUCKY_ID)) { |
| metadataMap.put(MetadataType.JPG_DUCKY, new Ducky(ArrayUtils.subArray(data, DUCKY_ID.length(), length - DUCKY_ID.length() - 2))); |
| } |
| } else if(segment.getMarker() == Marker.APP13) { |
| if (data.length >= PHOTOSHOP_IRB_ID.length() && new String(data, 0, PHOTOSHOP_IRB_ID.length()).equals(PHOTOSHOP_IRB_ID)) { |
| if(eightBIMStream == null) |
| eightBIMStream = new ByteArrayOutputStream(); |
| eightBIMStream.write(ArrayUtils.subArray(data, PHOTOSHOP_IRB_ID.length(), length - PHOTOSHOP_IRB_ID.length() - 2)); |
| } |
| } else if(segment.getMarker() == Marker.APP14) { |
| if (data.length >= ADOBE_ID.length() && new String(data, 0, ADOBE_ID.length()).equals(ADOBE_ID)) { |
| metadataMap.put(MetadataType.JPG_ADOBE, new Adobe(ArrayUtils.subArray(data, ADOBE_ID.length(), length - ADOBE_ID.length() - 2))); |
| } |
| } |
| } |
| |
| // Now it's time to join multiple segments ICC_PROFILE and/or XMP |
| if(iccProfileStream != null) { // We have ICCProfile data |
| ICCProfile icc_profile = new ICCProfile(iccProfileStream.toByteArray()); |
| metadataMap.put(MetadataType.ICC_PROFILE, icc_profile); |
| } |
| |
| if(eightBIMStream != null) { |
| IRB irb = new IRB(eightBIMStream.toByteArray()); |
| metadataMap.put(MetadataType.PHOTOSHOP_IRB, irb); |
| _8BIM iptc = irb.get8BIM(ImageResourceID.IPTC_NAA.getValue()); |
| // Extract IPTC as stand-alone meta |
| if(iptc != null) { |
| metadataMap.put(MetadataType.IPTC, new IPTC(iptc.getData())); |
| } |
| } |
| |
| if(extendedXMP != null) { |
| XMP xmp = ((XMP)metadataMap.get(MetadataType.XMP)); |
| if(xmp != null) |
| xmp.setExtendedXMPData(extendedXMP); |
| } |
| |
| // Extract thumbnails to ImageMetadata |
| Metadata meta = metadataMap.get(MetadataType.EXIF); |
| if(meta != null) { |
| Exif exif = (Exif)meta; |
| if(!exif.isDataRead()) |
| exif.read(); |
| if(exif.containsThumbnail()) { |
| thumbnails.put("EXIF", exif.getThumbnail()); |
| } |
| } |
| |
| meta = metadataMap.get(MetadataType.PHOTOSHOP_IRB); |
| if(meta != null) { |
| IRB irb = (IRB)meta; |
| if(!irb.isDataRead()) |
| irb.read(); |
| if(irb.containsThumbnail()) { |
| thumbnails.put("PHOTOSHOP_IRB", irb.getThumbnail()); |
| } |
| } |
| |
| metadataMap.put(MetadataType.IMAGE, new ImageMetadata(thumbnails)); |
| } |
| |
| /** |
| * Extracts thumbnail images from JFIF/APP0, Exif APP1 and/or Adobe APP13 segment if any. |
| * |
| * @param is InputStream for the JPEG image. |
| * @param pathToThumbnail a path or a path and name prefix combination for the extracted thumbnails. |
| * @throws IOException |
| */ |
| public static void extractThumbnails(InputStream is, String pathToThumbnail) throws IOException { |
| // Flag when we are done |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.EOI) { |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| int nextByte = 0; |
| while((nextByte = IOUtils.read(is)) == 0xff) {;} |
| marker = (short)((0xff<<8)|nextByte); |
| break; |
| case SOS: |
| finished = true; |
| break; |
| case APP0: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] jfif_buf = new byte[length - 2]; |
| IOUtils.readFully(is, jfif_buf); |
| // EXIF segment |
| if(new String(jfif_buf, 0, JFIF_ID.length()).equals(JFIF_ID) || new String(jfif_buf, 0, JFXX_ID.length()).equals(JFXX_ID)) { |
| int thumbnailWidth = jfif_buf[12]&0xff; |
| int thumbnailHeight = jfif_buf[13]&0xff; |
| String outpath = ""; |
| if(pathToThumbnail.endsWith("\\") || pathToThumbnail.endsWith("/")) |
| outpath = pathToThumbnail + "jfif_thumbnail"; |
| else |
| outpath = pathToThumbnail.replaceFirst("[.][^.]+$", "") + "_jfif_t"; |
| |
| if(thumbnailWidth != 0 && thumbnailHeight != 0) { // There is a thumbnail |
| // Extract the thumbnail |
| //Create a BufferedImage |
| int size = 3*thumbnailWidth*thumbnailHeight; |
| int[] colors = MetadataUtils.toARGB(ArrayUtils.subArray(jfif_buf, 14, size)); |
| Bitmap bmp = Bitmap.createBitmap(colors, thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888); |
| FileOutputStream fout = new FileOutputStream(outpath + ".jpg"); |
| try { |
| bmp.compress(CompressFormat.JPEG, 100, fout); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| fout.close(); |
| } |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP1: |
| // EXIF identifier with trailing bytes [0x00,0x00]. |
| byte[] exif_buf = new byte[EXIF_ID.length()]; |
| length = IOUtils.readUnsignedShortMM(is); |
| IOUtils.readFully(is, exif_buf); |
| // EXIF segment. |
| if (Arrays.equals(exif_buf, EXIF_ID.getBytes())) { |
| exif_buf = new byte[length - 8]; |
| IOUtils.readFully(is, exif_buf); |
| Exif exif = new JpegExif(exif_buf); |
| if(exif.containsThumbnail()) { |
| String outpath = ""; |
| if(pathToThumbnail.endsWith("\\") || pathToThumbnail.endsWith("/")) |
| outpath = pathToThumbnail + "exif_thumbnail"; |
| else |
| outpath = pathToThumbnail.replaceFirst("[.][^.]+$", "") + "_exif_t"; |
| Thumbnail thumbnail = exif.getThumbnail(); |
| OutputStream fout = null; |
| if(thumbnail.getDataType() == ExifThumbnail.DATA_TYPE_KJpegRGB) {// JPEG format, save as JPEG |
| fout = new FileOutputStream(outpath + ".jpg"); |
| } else { // Uncompressed, save as TIFF |
| fout = new FileOutputStream(outpath + ".tif"); |
| } |
| fout.write(thumbnail.getCompressedImage()); |
| fout.close(); |
| } |
| } else { |
| IOUtils.skipFully(is, length - 8); |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP13: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] data = new byte[length - 2]; |
| IOUtils.readFully(is, data, 0, length - 2); |
| int i = 0; |
| |
| while(data[i] != 0) i++; |
| |
| if(new String(data, 0, i++).equals("Photoshop 3.0")) { |
| IRB irb = new IRB(ArrayUtils.subArray(data, i, data.length - i)); |
| if(irb.containsThumbnail()) { |
| Thumbnail thumbnail = irb.getThumbnail(); |
| // Create output path |
| String outpath = ""; |
| if(pathToThumbnail.endsWith("\\") || pathToThumbnail.endsWith("/")) |
| outpath = pathToThumbnail + "photoshop_thumbnail.jpg"; |
| else |
| outpath = pathToThumbnail.replaceFirst("[.][^.]+$", "") + "_photoshop_t.jpg"; |
| FileOutputStream fout = new FileOutputStream(outpath); |
| if(thumbnail.getDataType() == Thumbnail.DATA_TYPE_KJpegRGB) { |
| fout.write(thumbnail.getCompressedImage()); |
| } else { |
| Bitmap bmp = thumbnail.getRawImage(); |
| try { |
| bmp.compress(Bitmap.CompressFormat.JPEG, 100, fout); |
| } catch (Exception e) { |
| throw new IOException("Writing thumbnail failed!"); |
| } |
| } |
| fout.close(); |
| } |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| public static ICCProfile getICCProfile(InputStream is) throws IOException { |
| ICCProfile profile = null; |
| byte[] buf = extractICCProfile(is); |
| if(buf.length > 0) |
| profile = new ICCProfile(buf); |
| return profile; |
| } |
| |
| public static void insertComments(InputStream is, OutputStream os, List<String> comments) throws IOException { |
| boolean finished = false; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| // Write comment |
| for(String comment : comments) |
| writeComment(comment, os); |
| // Copy the rest of the data |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| // No more marker to read, we are done. |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| IOUtils.writeShortMM(os, marker); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| IOUtils.writeShortMM(os, marker); |
| int nextByte = 0; |
| while ((nextByte = IOUtils.read(is)) == 0xff) { |
| IOUtils.write(os, nextByte); |
| } |
| marker = (short) ((0xff << 8) | nextByte); |
| break; |
| default: |
| marker = copySegment(marker, is, os); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param is input image stream |
| * @param os output image stream |
| * @param exif Exif instance |
| * @param update True to keep the original data, otherwise false |
| * @throws Exception |
| */ |
| public static void insertExif(InputStream is, OutputStream os, Exif exif, boolean update) throws IOException { |
| // We need thumbnail image but don't have one, create one from the current image input stream |
| if(exif.isThumbnailRequired() && !exif.containsThumbnail()) { |
| is = new FileCacheRandomAccessInputStream(is); |
| // Insert thumbnail into EXIF wrapper |
| exif.setThumbnailImage(MetadataUtils.createThumbnail(is)); |
| } |
| Exif oldExif = null; |
| int app0Index = -1; |
| // Copy the original image and insert EXIF data |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) { |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| } |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| // Create a list to hold the temporary Segments |
| List<Segment> segments = new ArrayList<Segment>(); |
| |
| while (!finished) { // Read through and add the segments to a list until SOS |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| // Write the items in segments list excluding the old EXIF |
| for(int i = 0; i <= app0Index; i++) { |
| segments.get(i).write(os); |
| } |
| // Now we insert the EXIF data |
| IFD newExifSubIFD = exif.getExifIFD(); |
| IFD newGpsSubIFD = exif.getGPSIFD(); |
| IFD newImageIFD = exif.getImageIFD(); |
| IFD newInteropSubIFD = exif.getInteropIFD(); |
| ExifThumbnail newThumbnail = exif.getThumbnail(); |
| // Define new IFDs |
| IFD exifSubIFD = null; |
| IFD gpsSubIFD = null; |
| IFD interopSubIFD = null; |
| IFD imageIFD = null; |
| // Got to do something to keep the old data |
| if(update && oldExif != null) { |
| exif.setPreferredEndian(oldExif.getPreferredEndian()); |
| IFD oldImageIFD = oldExif.getImageIFD(); |
| IFD oldExifSubIFD = oldExif.getExifIFD(); |
| IFD oldGpsSubIFD = oldExif.getGPSIFD(); |
| IFD oldInteropSubIFD = oldExif.getInteropIFD(); |
| |
| ExifThumbnail thumbnail = oldExif.getThumbnail(); |
| |
| if(oldImageIFD != null) { |
| imageIFD = new IFD(); |
| imageIFD.addFields(oldImageIFD.getFields()); |
| } |
| if(thumbnail != null) { |
| if(newThumbnail == null) |
| newThumbnail = thumbnail; |
| } |
| if(oldExifSubIFD != null) { |
| exifSubIFD = new IFD(); |
| exifSubIFD.addFields(oldExifSubIFD.getFields()); |
| } |
| if(oldInteropSubIFD != null) { |
| interopSubIFD = new IFD(); |
| interopSubIFD.addFields(oldInteropSubIFD.getFields()); |
| } |
| if(oldGpsSubIFD != null) { |
| gpsSubIFD = new IFD(); |
| gpsSubIFD.addFields(oldGpsSubIFD.getFields()); |
| } |
| } |
| if(newImageIFD != null) { |
| if(imageIFD == null) |
| imageIFD = new IFD(); |
| imageIFD.addFields(newImageIFD.getFields()); |
| } |
| if(exifSubIFD != null) { |
| if(newExifSubIFD != null) |
| exifSubIFD.addFields(newExifSubIFD.getFields()); |
| } else |
| exifSubIFD = newExifSubIFD; |
| if(interopSubIFD != null) { |
| if(newInteropSubIFD != null) |
| interopSubIFD.addFields(newInteropSubIFD.getFields()); |
| } else |
| interopSubIFD = newInteropSubIFD; |
| if(gpsSubIFD != null) { |
| if(newGpsSubIFD != null) |
| gpsSubIFD.addFields(newGpsSubIFD.getFields()); |
| } else |
| gpsSubIFD = newGpsSubIFD; |
| // If we have ImageIFD, set Image IFD attached with EXIF and GPS |
| if(imageIFD != null) { |
| if(exifSubIFD != null) { |
| imageIFD.addChild(TiffTag.EXIF_SUB_IFD, exifSubIFD); |
| if(interopSubIFD != null) { |
| exifSubIFD.addChild(ExifTag.EXIF_INTEROPERABILITY_OFFSET, interopSubIFD); |
| } |
| } |
| if(gpsSubIFD != null) |
| imageIFD.addChild(TiffTag.GPS_SUB_IFD, gpsSubIFD); |
| exif.setImageIFD(imageIFD); |
| } else { // Otherwise, set EXIF and GPS IFD separately |
| exif.setExifIFD(exifSubIFD); |
| exif.setGPSIFD(gpsSubIFD); |
| exif.setInteropIFD(interopSubIFD); |
| } |
| exif.setThumbnail(newThumbnail); |
| // Now insert the new EXIF to the JPEG |
| exif.write(os); |
| // Copy the remaining segments |
| for(int i = app0Index + 1; i < segments.size(); i++) { |
| segments.get(i).write(os); |
| } |
| // Copy the leftover stuff |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| // We are done |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| segments.add(new Segment(emarker, 0, null)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP1: |
| // Read and remove the old EXIF data |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] exifBytes = new byte[length - 2]; |
| IOUtils.readFully(is, exifBytes); |
| // Add data to segment list |
| segments.add(new Segment(emarker, length, exifBytes)); |
| // Read the EXIF data. |
| if(exifBytes.length >= EXIF_ID.length() && new String(exifBytes, 0, EXIF_ID.length()).equals(EXIF_ID)) { // We assume EXIF data exist only in one APP1 |
| oldExif = new JpegExif(ArrayUtils.subArray(exifBytes, EXIF_ID.length(), length - EXIF_ID.length() - 2)); |
| segments.remove(segments.size() - 1); |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP0: |
| app0Index = segments.size(); |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| if(emarker == Marker.UNKNOWN) |
| segments.add(new UnknownSegment(marker, length, buf)); |
| else |
| segments.add(new Segment(emarker, length, buf)); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| // Close the input stream in case it's an instance of RandomAccessInputStream |
| if(is instanceof RandomAccessInputStream) |
| ((FileCacheRandomAccessInputStream)is).shallowClose(); |
| } |
| |
| /** |
| * Insert ICC_Profile as one or more APP2 segments |
| * |
| * @param is input stream for the original image |
| * @param os output stream to write the ICC_Profile |
| * @param data ICC_Profile data array to be inserted |
| * @throws IOException |
| */ |
| public static void insertICCProfile(InputStream is, OutputStream os, byte[] data) throws IOException { |
| // Copy the original image and insert ICC_Profile data |
| byte[] icc_profile_id = {0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x00}; |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| int app0Index = -1; |
| int app1Index = -1; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| // Create a list to hold the temporary Segments |
| List<Segment> segments = new ArrayList<Segment>(); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| int index = Math.max(app0Index, app1Index); |
| // Write the items in segments list excluding the APP13 |
| for(int i = 0; i <= index; i++) |
| segments.get(i).write(os); |
| writeICCProfile(os, data); |
| // Copy the remaining segments |
| for(int i = (index < 0 ? 0 : index + 1); i < segments.size(); i++) { |
| segments.get(i).write(os); |
| } |
| // Copy the rest of the data |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| // No more marker to read, we are done. |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| segments.add(new Segment(emarker, 0, null)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP2: // Remove old ICC_Profile |
| byte[] icc_profile_buf = new byte[12]; |
| length = IOUtils.readUnsignedShortMM(is); |
| if(length < 14) { // This is not an ICC_Profile segment, copy it |
| icc_profile_buf = new byte[length - 2]; |
| IOUtils.readFully(is, icc_profile_buf); |
| segments.add(new Segment(emarker, length, icc_profile_buf)); |
| } else { |
| IOUtils.readFully(is, icc_profile_buf); |
| // ICC_PROFILE segment. |
| if (Arrays.equals(icc_profile_buf, icc_profile_id)) { |
| IOUtils.skipFully(is, length - 14); |
| } else {// Not an ICC_Profile segment, copy it |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short)length); |
| IOUtils.write(os, icc_profile_buf); |
| byte[] temp = new byte[length - ICC_PROFILE_ID.length() - 2]; |
| IOUtils.readFully(is, temp); |
| segments.add(new Segment(emarker, length, ArrayUtils.concat(icc_profile_buf, temp))); |
| } |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP0: |
| app0Index = segments.size(); |
| case APP1: |
| app1Index = segments.size(); |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| if(emarker == Marker.UNKNOWN) |
| segments.add(new UnknownSegment(marker, length, buf)); |
| else |
| segments.add(new Segment(emarker, length, buf)); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| public static void insertICCProfile(InputStream is, OutputStream os, ICCProfile icc_profile) throws Exception { |
| insertICCProfile(is, os, icc_profile.getData()); |
| } |
| |
| /** |
| * Inserts a list of IPTCDataSet into a JPEG APP13 Photoshop IRB segment |
| * |
| * @param is InputStream for the original image |
| * @param os OutputStream for the image with IPTC inserted |
| * @param iptcs a collection of IPTCDataSet to be inserted |
| * @param update boolean if true, keep the original IPTC data; otherwise, replace it completely with the new IPTC data |
| * @throws IOException |
| */ |
| public static void insertIPTC(InputStream is, OutputStream os, Collection<IPTCDataSet> iptcs, boolean update) throws IOException { |
| // Copy the original image and insert Photoshop IRB data |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| int app0Index = -1; |
| int app1Index = -1; |
| |
| Map<Short, _8BIM> bimMap = null; |
| // Used to read multiple segment Adobe APP13 |
| ByteArrayOutputStream eightBIMStream = null; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) { |
| is.close(); |
| os.close(); |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| } |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| // Create a list to hold the temporary Segments |
| List<Segment> segments = new ArrayList<Segment>(); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| if(eightBIMStream != null) { |
| IRB irb = new IRB(eightBIMStream.toByteArray()); |
| // Shallow copy the map. |
| bimMap = new HashMap<Short, _8BIM>(irb.get8BIM()); |
| _8BIM iptcBIM = bimMap.remove(ImageResourceID.IPTC_NAA.getValue()); |
| if(iptcBIM != null && update) { // Keep the original values |
| IPTC iptc = new IPTC(iptcBIM.getData()); |
| // Shallow copy the map |
| Map<IPTCTag, List<IPTCDataSet>> dataSetMap = new HashMap<IPTCTag, List<IPTCDataSet>>(iptc.getDataSets()); |
| for(IPTCDataSet set : iptcs) |
| if(!set.allowMultiple()) |
| dataSetMap.remove(set.getName()); |
| for(List<IPTCDataSet> iptcList : dataSetMap.values()) |
| iptcs.addAll(iptcList); |
| } |
| } |
| int index = Math.max(app0Index, app1Index); |
| // Write the items in segments list excluding the APP13 |
| for(int i = 0; i <= index; i++) |
| segments.get(i).write(os); |
| ByteArrayOutputStream bout = new ByteArrayOutputStream(); |
| // Insert IPTC data as one of IRB 8BIM block |
| for(IPTCDataSet iptc : iptcs) |
| iptc.write(bout); |
| // Create 8BIM for IPTC |
| _8BIM newBIM = new _8BIM(ImageResourceID.IPTC_NAA.getValue(), "iptc", bout.toByteArray()); |
| if(bimMap != null) { |
| bimMap.put(newBIM.getID(), newBIM); // Add the IPTC_NAA 8BIM to the map |
| writeIRB(os, bimMap.values()); // Write the whole thing as one APP13 |
| } else { |
| writeIRB(os, newBIM); // Write the one and only one 8BIM as one APP13 |
| } |
| // Copy the remaining segments |
| for(int i = (index < 0 ? 0 : index + 1); i < segments.size(); i++) { |
| segments.get(i).write(os); |
| } |
| // Copy the rest of the data |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| // No more marker to read, we are done. |
| finished = true; |
| } else {// Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| segments.add(new Segment(emarker, 0, null)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP13: |
| if(eightBIMStream == null) |
| eightBIMStream = new ByteArrayOutputStream(); |
| readAPP13(is, eightBIMStream); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP0: |
| app0Index = segments.size(); |
| case APP1: |
| app1Index = segments.size(); |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| if(emarker == Marker.UNKNOWN) |
| segments.add(new UnknownSegment(marker, length, buf)); |
| else |
| segments.add(new Segment(emarker, length, buf)); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Inserts a collection of _8BIM into a JPEG APP13 Photoshop IRB segment |
| * |
| * @param is InputStream for the original image |
| * @param os OutputStream for the image with _8BIMs inserted |
| * @param bims a collection of _8BIM to be inserted |
| * @param update boolean if true, keep the other _8BIMs; otherwise, replace the whole IRB with the inserted _8BIMs |
| * @throws IOException |
| */ |
| public static void insertIRB(InputStream is, OutputStream os, Collection<_8BIM> bims, boolean update) throws IOException { |
| // Copy the original image and insert Photoshop IRB data |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| int app0Index = -1; |
| int app1Index = -1; |
| // Used to read multiple segment Adobe APP13 |
| ByteArrayOutputStream eightBIMStream = null; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) { |
| is.close(); |
| os.close(); |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| } |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| // Create a list to hold the temporary Segments |
| List<Segment> segments = new ArrayList<Segment>(); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| if(eightBIMStream != null) { |
| IRB irb = new IRB(eightBIMStream.toByteArray()); |
| // Shallow copy the map. |
| Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM()); |
| for(_8BIM bim : bims) // Replace the original data |
| bimMap.put(bim.getID(), bim); |
| // In case we have two ThumbnailResource IRB, remove the Photoshop4.0 one |
| if(bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue()) |
| && bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS5.getValue())) |
| bimMap.remove(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue()); |
| bims = bimMap.values(); |
| } |
| int index = Math.max(app0Index, app1Index); |
| // Write the items in segments list excluding the APP13 |
| for(int i = 0; i <= index; i++) |
| segments.get(i).write(os); |
| writeIRB(os, bims); |
| // Copy the remaining segments |
| for(int i = (index < 0 ? 0 : index + 1); i < segments.size(); i++) { |
| segments.get(i).write(os); |
| } |
| // Copy the rest of the data |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| // No more marker to read, we are done. |
| finished = true; |
| } else {// Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| segments.add(new Segment(emarker, 0, null)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP13: // We will keep the other IRBs from the original APP13 |
| if(update) { |
| if(eightBIMStream == null) |
| eightBIMStream = new ByteArrayOutputStream(); |
| readAPP13(is, eightBIMStream); |
| } else { |
| length = IOUtils.readUnsignedShortMM(is); |
| IOUtils.skipFully(is, length - 2); |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP0: |
| app0Index = segments.size(); |
| case APP1: |
| app1Index = segments.size(); |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| if(emarker == Marker.UNKNOWN) |
| segments.add(new UnknownSegment(marker, length, buf)); |
| else |
| segments.add(new Segment(emarker, length, buf)); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| public static void insertIRBThumbnail(InputStream is, OutputStream os, Bitmap thumbnail) throws IOException { |
| // Sanity check |
| if(thumbnail == null) throw new IllegalArgumentException("Input thumbnail is null"); |
| _8BIM bim = new ThumbnailResource(thumbnail); |
| insertIRB(is, os, Arrays.asList(bim), true); // Set true to keep other IRB blocks |
| } |
| |
| /** |
| * Insert a XMP instance into the image. |
| * The XMP instance must be able to fit into one APP1. |
| * |
| * @param is InputStream for the image. |
| * @param os OutputStream for the image. |
| * @param xmp XMP instance |
| * @throws IOException |
| */ |
| public static void insertXMP(InputStream is, OutputStream os, XMP xmp) throws IOException { |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| int app0Index = -1; |
| int exifIndex = -1; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) { |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| } |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| // Create a list to hold the temporary Segments |
| List<Segment> segments = new ArrayList<Segment>(); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.SOS) { |
| int index = Math.max(app0Index, exifIndex); |
| // Write the items in segments list excluding the old XMP |
| for(int i = 0; i <= index; i++) |
| segments.get(i).write(os); |
| // Now we insert the XMP data |
| xmp.write(os); |
| // Copy the remaining segments |
| for(int i = (index < 0 ? 0 : index + 1); i < segments.size(); i++) { |
| segments.get(i).write(os); |
| } |
| // Copy the leftover stuff |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); // Copy the rest of the data |
| finished = true; // No more marker to read, we are done. |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| segments.add(new Segment(emarker, 0, null)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP1: |
| // Read and remove the old XMP data |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // Remove XMP and ExtendedXMP segments. |
| if(temp.length >= XMP_EXT_ID.length() && new String(temp, 0, XMP_EXT_ID.length()).equals(XMP_EXT_ID)) { |
| ; |
| } else if (temp.length >= XMP_ID.length() && new String(temp, 0, XMP_ID.length()).equals(XMP_ID)) { |
| ; |
| } else { |
| segments.add(new Segment(emarker, length, temp)); |
| // If it's EXIF, we keep the index |
| if(temp.length >= EXIF_ID.length() && new String(temp, 0, EXIF_ID.length()).equals(EXIF_ID)) { |
| exifIndex = segments.size() - 1; |
| } |
| } |
| marker = IOUtils.readShortMM(is); |
| break; |
| case APP0: |
| app0Index = segments.size(); |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| if(emarker == Marker.UNKNOWN) |
| segments.add(new UnknownSegment(marker, length, buf)); |
| else |
| segments.add(new Segment(emarker, length, buf)); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Insert XMP into single APP1 or multiple segments. Support ExtendedXMP. |
| * The standard part of the XMP must be a valid XMP with packet wrapper and, |
| * should already include the GUID for the ExtendedXMP in case of ExtendedXMP. |
| * <p> |
| * When converted to bytes, the XMP part should be able to fit into one APP1. |
| * |
| * @param is InputStream for the image. |
| * @param os OutputStream for the image. |
| * @param xmp XML string for the XMP - Assuming in UTF-8 format. |
| * @param extendedXmp XML string for the extended XMP - Assuming in UTF-8 format |
| * |
| * @throws IOException |
| */ |
| public static void insertXMP(InputStream is, OutputStream os, String xmp, String extendedXmp) throws IOException { |
| insertXMP(is, os, new JpegXMP(xmp, extendedXmp)); |
| } |
| |
| private static String hTablesToString(List<HTable> hTables) { |
| final String[] HT_class_table = {"DC Component", "AC Component"}; |
| |
| StringBuilder hufTable = new StringBuilder(); |
| |
| hufTable.append("Huffman table information =>:\n"); |
| |
| for(HTable table : hTables ) |
| { |
| hufTable.append("Class: " + table.getClazz() + " (" + HT_class_table[table.getClazz()] + ")\n"); |
| hufTable.append("Huffman table #: " + table.getID() + "\n"); |
| |
| byte[] bits = table.getBits(); |
| byte[] values = table.getValues(); |
| |
| int count = 0; |
| |
| for (int i = 0; i < bits.length; i++) |
| { |
| count += (bits[i]&0xff); |
| } |
| |
| hufTable.append("Number of codes: " + count + "\n"); |
| |
| if (count > 256) |
| throw new RuntimeException("Invalid huffman code count: " + count); |
| |
| int j = 0; |
| |
| for (int i = 0; i < 16; i++) { |
| |
| hufTable.append("Codes of length " + (i+1) + " (" + (bits[i]&0xff) + " total): [ "); |
| |
| for (int k = 0; k < (bits[i]&0xff); k++) { |
| hufTable.append((values[j++]&0xff) + " "); |
| } |
| |
| hufTable.append("]\n"); |
| } |
| |
| hufTable.append("<<End of Huffman table information>>\n"); |
| } |
| |
| return hufTable.toString(); |
| } |
| |
| private static String qTablesToString(List<QTable> qTables) { |
| StringBuilder qtTables = new StringBuilder(); |
| |
| qtTables.append("Quantization table information =>:\n"); |
| |
| int count = 0; |
| |
| for(QTable table : qTables) { |
| int QT_precision = table.getPrecision(); |
| int[] qTable = table.getData(); |
| qtTables.append("precision of QT is " + QT_precision + "\n"); |
| qtTables.append("Quantization table #" + table.getID() + ":\n"); |
| |
| if(QT_precision == 0) { |
| for (int j = 0; j < 64; j++) |
| { |
| if (j != 0 && j%8 == 0) { |
| qtTables.append("\n"); |
| } |
| qtTables.append(qTable[j] + " "); |
| } |
| } else { // 16 bit big-endian |
| |
| for (int j = 0; j < 64; j++) { |
| if (j != 0 && j%8 == 0) { |
| qtTables.append("\n"); |
| } |
| qtTables.append(qTable[j] + " "); |
| } |
| } |
| |
| count++; |
| |
| qtTables.append("\n"); |
| qtTables.append("***************************\n"); |
| } |
| |
| qtTables.append("Total number of Quantation tables: " + count + "\n"); |
| qtTables.append("End of quantization table information\n"); |
| |
| return qtTables.toString(); |
| } |
| |
| private static String sofToString(SOFReader reader) { |
| StringBuilder sof = new StringBuilder(); |
| sof.append("SOF information =>\n"); |
| sof.append("Precision: " + reader.getPrecision() + "\n"); |
| sof.append("Image height: " + reader.getFrameHeight() +"\n"); |
| sof.append("Image width: " + reader.getFrameWidth() + "\n"); |
| sof.append("# of Components: " + reader.getNumOfComponents() + "\n"); |
| sof.append("(1 = grey scaled, 3 = color YCbCr or YIQ, 4 = color CMYK)\n"); |
| |
| for(Component component : reader.getComponents()) { |
| sof.append("\n"); |
| sof.append("Component ID: " + component.getId() + "\n"); |
| sof.append("Herizontal sampling factor: " + component.getHSampleFactor() + "\n"); |
| sof.append("Vertical sampling factor: " + component.getVSampleFactor() + "\n"); |
| sof.append("Quantization table #: " + component.getQTableNumber() + "\n"); |
| sof.append("DC table number: " + component.getDCTableNumber() + "\n"); |
| sof.append("AC table number: " + component.getACTableNumber() + "\n"); |
| } |
| |
| sof.append("<= End of SOF information"); |
| |
| return sof.toString(); |
| } |
| |
| private static void readAPP13(InputStream is, OutputStream os) throws IOException { |
| int length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| |
| if (new String(temp, 0, PHOTOSHOP_IRB_ID.length()).equals(PHOTOSHOP_IRB_ID)) { |
| os.write(ArrayUtils.subArray(temp, PHOTOSHOP_IRB_ID.length(), temp.length - PHOTOSHOP_IRB_ID.length())); |
| } |
| } |
| |
| private static void readAPP2(InputStream is, OutputStream os) throws IOException { |
| byte[] icc_profile_buf = new byte[ICC_PROFILE_ID.length()]; |
| int length = IOUtils.readUnsignedShortMM(is); |
| IOUtils.readFully(is, icc_profile_buf); |
| // ICC_PROFILE segment. |
| if (Arrays.equals(icc_profile_buf, ICC_PROFILE_ID.getBytes())) { |
| icc_profile_buf = new byte[length - ICC_PROFILE_ID.length() - 2]; |
| IOUtils.readFully(is, icc_profile_buf); |
| os.write(icc_profile_buf, 2, length - ICC_PROFILE_ID.length() - 4); |
| } else { |
| IOUtils.skipFully(is, length - ICC_PROFILE_ID.length() - 2); |
| } |
| } |
| |
| private static void readDHT(InputStream is, List<HTable> m_acTables, List<HTable> m_dcTables) throws IOException { |
| int len = IOUtils.readUnsignedShortMM(is); |
| byte buf[] = new byte[len - 2]; |
| IOUtils.readFully(is, buf); |
| |
| DHTReader reader = new DHTReader(new Segment(Marker.DHT, len, buf)); |
| |
| List<HTable> dcTables = reader.getDCTables(); |
| List<HTable> acTables = reader.getACTables(); |
| |
| m_acTables.addAll(acTables); |
| m_dcTables.addAll(dcTables); |
| } |
| |
| // Process define Quantization table |
| private static void readDQT(InputStream is, List<QTable> m_qTables) throws IOException { |
| int len = IOUtils.readUnsignedShortMM(is); |
| byte buf[] = new byte[len - 2]; |
| IOUtils.readFully(is, buf); |
| |
| DQTReader reader = new DQTReader(new Segment(Marker.DQT, len, buf)); |
| List<QTable> qTables = reader.getTables(); |
| m_qTables.addAll(qTables); |
| } |
| |
| public static Map<MetadataType, Metadata> readMetadata(InputStream is) throws IOException { |
| Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>(); |
| Map<String, Thumbnail> thumbnails = new HashMap<String, Thumbnail>(); |
| // Need to wrap the input stream with a BufferedInputStream to |
| // speed up reading SOS |
| is = new BufferedInputStream(is); |
| // Definitions |
| List<QTable> m_qTables = new ArrayList<QTable>(4); |
| List<HTable> m_acTables = new ArrayList<HTable>(4); |
| List<HTable> m_dcTables = new ArrayList<HTable>(4); |
| // Each SOFReader is associated with a single SOF segment |
| // Usually there is only one SOF segment, but for hierarchical |
| // JPEG, there could be more than one SOF |
| List<SOFReader> readers = new ArrayList<SOFReader>(); |
| // Used to read multiple segment ICCProfile |
| ByteArrayOutputStream iccProfileStream = null; |
| // Used to read multiple segment Adobe APP13 |
| ByteArrayOutputStream eightBIMStream = null; |
| // Used to read multiple segment XMP |
| byte[] extendedXMP = null; |
| String xmpGUID = ""; // 32 byte ASCII hex string |
| Comments comments = null; |
| |
| List<Segment> appnSegments = new ArrayList<Segment>(); |
| |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IllegalArgumentException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.EOI) { |
| finished = true; |
| } else {// Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case APP0: |
| case APP1: |
| case APP2: |
| case APP3: |
| case APP4: |
| case APP5: |
| case APP6: |
| case APP7: |
| case APP8: |
| case APP9: |
| case APP10: |
| case APP11: |
| case APP12: |
| case APP13: |
| case APP14: |
| case APP15: |
| byte[] appBytes = readSegmentData(is); |
| appnSegments.add(new Segment(emarker, appBytes.length + 2, appBytes)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case COM: |
| if(comments == null) comments = new Comments(); |
| comments.addComment(readSegmentData(is)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case DHT: |
| readDHT(is, m_acTables, m_dcTables); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case DQT: |
| readDQT(is, m_qTables); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case SOF0: |
| case SOF1: |
| case SOF2: |
| case SOF3: |
| case SOF5: |
| case SOF6: |
| case SOF7: |
| case SOF9: |
| case SOF10: |
| case SOF11: |
| case SOF13: |
| case SOF14: |
| case SOF15: |
| readers.add(readSOF(is, emarker)); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case SOS: |
| SOFReader reader = readers.get(readers.size() - 1); |
| marker = readSOS(is, reader); |
| LOGGER.debug("\n{}", sofToString(reader)); |
| break; |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone mark besides SOI, EOI, and RSTn. |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| int nextByte = 0; |
| while((nextByte = IOUtils.read(is)) == 0xff) {;} |
| marker = (short)((0xff<<8)|nextByte); |
| break; |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| IOUtils.skipFully(is, length - 2); |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| |
| is.close(); |
| |
| // Debugging |
| LOGGER.debug("\n{}", qTablesToString(m_qTables)); |
| LOGGER.debug("\n{}", hTablesToString(m_acTables)); |
| LOGGER.debug("\n{}", hTablesToString(m_dcTables)); |
| |
| for(Segment segment : appnSegments) { |
| byte[] data = segment.getData(); |
| length = segment.getLength(); |
| if(segment.getMarker() == Marker.APP0) { |
| if (new String(data, 0, JFIF_ID.length()).equals(JFIF_ID)) { |
| metadataMap.put(MetadataType.JPG_JFIF, new JFIF(ArrayUtils.subArray(data, JFIF_ID.length(), length - JFIF_ID.length() - 2))); |
| } |
| } else if(segment.getMarker() == Marker.APP1) { |
| // Check for EXIF |
| if(new String(data, 0, EXIF_ID.length()).equals(EXIF_ID)) { |
| // We found EXIF |
| JpegExif exif = new JpegExif(ArrayUtils.subArray(data, EXIF_ID.length(), length - EXIF_ID.length() - 2)); |
| metadataMap.put(MetadataType.EXIF, exif); |
| } else if(new String(data, 0, XMP_ID.length()).equals(XMP_ID) || |
| new String(data, 0, NON_STANDARD_XMP_ID.length()).equals(NON_STANDARD_XMP_ID)) { |
| // We found XMP, add it to metadata list (We may later revise it if we have ExtendedXMP) |
| XMP xmp = new JpegXMP(ArrayUtils.subArray(data, XMP_ID.length(), length - XMP_ID.length() - 2)); |
| metadataMap.put(MetadataType.XMP, xmp); |
| // Retrieve and remove XMP GUID if available |
| xmpGUID = XMLUtils.getAttribute(xmp.getXmpDocument(), "rdf:Description", "xmpNote:HasExtendedXMP"); |
| } else if(new String(data, 0, XMP_EXT_ID.length()).equals(XMP_EXT_ID)) { |
| // We found ExtendedXMP, add the data to ExtendedXMP memory buffer |
| int i = XMP_EXT_ID.length(); |
| // 128-bit MD5 digest of the full ExtendedXMP serialization |
| byte[] guid = ArrayUtils.subArray(data, i, 32); |
| if(Arrays.equals(guid, xmpGUID.getBytes())) { // We have matched the GUID, copy it |
| i += 32; |
| long extendedXMPLength = IOUtils.readUnsignedIntMM(data, i); |
| i += 4; |
| if(extendedXMP == null) |
| extendedXMP = new byte[(int)extendedXMPLength]; |
| // Offset for the current segment |
| long offset = IOUtils.readUnsignedIntMM(data, i); |
| i += 4; |
| byte[] xmpBytes = ArrayUtils.subArray(data, i, length - XMP_EXT_ID.length() - 42); |
| System.arraycopy(xmpBytes, 0, extendedXMP, (int)offset, xmpBytes.length); |
| } |
| } |
| } else if(segment.getMarker() == Marker.APP2) { |
| // We're only interested in ICC_Profile |
| if (new String(data, 0, ICC_PROFILE_ID.length()).equals(ICC_PROFILE_ID)) { |
| if(iccProfileStream == null) |
| iccProfileStream = new ByteArrayOutputStream(); |
| iccProfileStream.write(ArrayUtils.subArray(data, ICC_PROFILE_ID.length() + 2, length - ICC_PROFILE_ID.length() - 4)); |
| } |
| } else if(segment.getMarker() == Marker.APP12) { |
| if (new String(data, 0, DUCKY_ID.length()).equals(DUCKY_ID)) { |
| metadataMap.put(MetadataType.JPG_DUCKY, new Ducky(ArrayUtils.subArray(data, DUCKY_ID.length(), length - DUCKY_ID.length() - 2))); |
| } |
| } else if(segment.getMarker() == Marker.APP13) { |
| if (new String(data, 0, PHOTOSHOP_IRB_ID.length()).equals(PHOTOSHOP_IRB_ID)) { |
| if(eightBIMStream == null) |
| eightBIMStream = new ByteArrayOutputStream(); |
| eightBIMStream.write(ArrayUtils.subArray(data, PHOTOSHOP_IRB_ID.length(), length - PHOTOSHOP_IRB_ID.length() - 2)); |
| } |
| } else if(segment.getMarker() == Marker.APP14) { |
| if (new String(data, 0, ADOBE_ID.length()).equals(ADOBE_ID)) { |
| metadataMap.put(MetadataType.JPG_ADOBE, new Adobe(ArrayUtils.subArray(data, ADOBE_ID.length(), length - ADOBE_ID.length() - 2))); |
| } |
| } |
| } |
| |
| // Now it's time to join multiple segments ICC_PROFILE and/or XMP |
| if(iccProfileStream != null) { // We have ICCProfile data |
| ICCProfile icc_profile = new ICCProfile(iccProfileStream.toByteArray()); |
| metadataMap.put(MetadataType.ICC_PROFILE, icc_profile); |
| } |
| |
| if(eightBIMStream != null) { |
| IRB irb = new IRB(eightBIMStream.toByteArray()); |
| metadataMap.put(MetadataType.PHOTOSHOP_IRB, irb); |
| _8BIM iptc = irb.get8BIM(ImageResourceID.IPTC_NAA.getValue()); |
| // Extract IPTC as stand-alone meta |
| if(iptc != null) { |
| metadataMap.put(MetadataType.IPTC, new IPTC(iptc.getData())); |
| } |
| } |
| |
| if(extendedXMP != null) { |
| XMP xmp = ((XMP)metadataMap.get(MetadataType.XMP)); |
| if(xmp != null) |
| xmp.setExtendedXMPData(extendedXMP); |
| } |
| |
| if(comments != null) |
| metadataMap.put(MetadataType.COMMENT, comments); |
| |
| // Extract thumbnails to ImageMetadata |
| Metadata meta = metadataMap.get(MetadataType.EXIF); |
| if(meta != null) { |
| Exif exif = (Exif)meta; |
| if(!exif.isDataRead()) |
| exif.read(); |
| if(exif.containsThumbnail()) { |
| thumbnails.put("EXIF", exif.getThumbnail()); |
| } |
| } |
| |
| meta = metadataMap.get(MetadataType.PHOTOSHOP_IRB); |
| if(meta != null) { |
| IRB irb = (IRB)meta; |
| if(!irb.isDataRead()) |
| irb.read(); |
| if(irb.containsThumbnail()) { |
| thumbnails.put("PHOTOSHOP_IRB", irb.getThumbnail()); |
| } |
| } |
| |
| metadataMap.put(MetadataType.IMAGE, new ImageMetadata(thumbnails)); |
| |
| return metadataMap; |
| } |
| |
| private static byte[] readSegmentData(InputStream is) throws IOException { |
| int length = IOUtils.readUnsignedShortMM(is); |
| byte[] data = new byte[length - 2]; |
| IOUtils.readFully(is, data); |
| |
| return data; |
| } |
| |
| private static SOFReader readSOF(InputStream is, Marker marker) throws IOException { |
| int len = IOUtils.readUnsignedShortMM(is); |
| byte buf[] = new byte[len - 2]; |
| IOUtils.readFully(is, buf); |
| |
| Segment segment = new Segment(marker, len, buf); |
| SOFReader reader = new SOFReader(segment); |
| |
| return reader; |
| } |
| |
| // This method is very slow if not wrapped in some kind of cache stream but it works for multiple |
| // SOSs in case of progressive JPEG |
| private static short readSOS(InputStream is, SOFReader sofReader) throws IOException { |
| int len = IOUtils.readUnsignedShortMM(is); |
| byte buf[] = new byte[len - 2]; |
| IOUtils.readFully(is, buf); |
| |
| Segment segment = new Segment(Marker.SOS, len, buf); |
| new SOSReader(segment, sofReader); |
| |
| // Actual image data follow. |
| int nextByte = 0; |
| short marker = 0; |
| |
| while((nextByte = IOUtils.read(is)) != -1) { |
| if(nextByte == 0xff) { |
| nextByte = IOUtils.read(is); |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| if (nextByte != 0x00) { |
| marker = (short)((0xff<<8)|nextByte); |
| |
| switch (Marker.fromShort(marker)) { |
| case RST0: |
| case RST1: |
| case RST2: |
| case RST3: |
| case RST4: |
| case RST5: |
| case RST6: |
| case RST7: |
| continue; |
| default: |
| } |
| break; |
| } |
| } |
| } |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| return marker; |
| } |
| |
| // Remove APPn segment |
| public static void removeAPPn(Marker APPn, InputStream is, OutputStream os) throws IOException { |
| if(APPn.getValue() < (short)0xffe0 || APPn.getValue() > (short)0xffef) |
| throw new IllegalArgumentException("Input marker is not an APPn marker"); |
| // Flag when we are done |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if(Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.EOI) { |
| IOUtils.writeShortMM(os, Marker.EOI.getValue()); |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| IOUtils.writeShortMM(os, marker); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| IOUtils.writeShortMM(os, marker); |
| int nextByte = 0; |
| while((nextByte = IOUtils.read(is)) == 0xff) { |
| IOUtils.write(os, nextByte); |
| } |
| marker = (short)((0xff<<8)|nextByte); |
| break; |
| case SOS: |
| IOUtils.writeShortMM(os, marker); |
| // use copyToEnd instead for multiple SOS |
| //marker = copySOS(is, os); |
| copyToEnd(is, os); |
| finished = true; |
| break; |
| default: |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] buf = new byte[length - 2]; |
| IOUtils.readFully(is, buf); |
| |
| if(emarker != APPn) { // Copy the data |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short)length); |
| IOUtils.write(os, buf); |
| } |
| |
| marker = IOUtils.readShortMM(is); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Removes metadata specified by the input MetadataType set. |
| * |
| * @param metadataTypes a set containing all the MetadataTypes to be removed. |
| * @param is InputStream for the original image. |
| * @param os OutputStream for the image with metadata removed. |
| * @throws IOException |
| * @return A map of the removed metadata |
| */ |
| public static Map<MetadataType, Metadata> removeMetadata(InputStream is, OutputStream os, MetadataType ... metadataTypes) throws IOException { |
| return removeMetadata(new HashSet<MetadataType>(Arrays.asList(metadataTypes)), is, os); |
| } |
| |
| /** |
| * Removes metadata specified by the input MetadataType set. |
| * |
| * @param metadataTypes a set containing all the MetadataTypes to be removed. |
| * @param is InputStream for the original image. |
| * @param os OutputStream for the image with metadata removed. |
| * @throws IOException |
| * @return A map of the removed metadata |
| */ |
| public static Map<MetadataType, Metadata> removeMetadata(Set<MetadataType> metadataTypes, InputStream is, OutputStream os) throws IOException { |
| // Create a map to hold all the metadata and thumbnails |
| Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>(); |
| // In case IRB data are partially removed, we keep removed metadata here |
| Map<MetadataType, Metadata> extraMetadataMap = new HashMap<MetadataType, Metadata>(); |
| |
| Comments comments = null; |
| |
| List<Segment> appnSegments = new ArrayList<Segment>(); |
| |
| // Flag when we are done |
| boolean finished = false; |
| int length = 0; |
| short marker; |
| Marker emarker; |
| |
| // The very first marker should be the start_of_image marker! |
| if (Marker.fromShort(IOUtils.readShortMM(is)) != Marker.SOI) |
| throw new IOException("Invalid JPEG image, expected SOI marker not found!"); |
| |
| IOUtils.writeShortMM(os, Marker.SOI.getValue()); |
| |
| marker = IOUtils.readShortMM(is); |
| |
| while (!finished) { |
| if (Marker.fromShort(marker) == Marker.EOI) { |
| IOUtils.writeShortMM(os, Marker.EOI.getValue()); |
| finished = true; |
| } else { // Read markers |
| emarker = Marker.fromShort(marker); |
| |
| switch (emarker) { |
| case JPG: // JPG and JPGn shouldn't appear in the image. |
| case JPG0: |
| case JPG13: |
| case TEM: // The only stand alone marker besides SOI, EOI, and RSTn. |
| IOUtils.writeShortMM(os, marker); |
| marker = IOUtils.readShortMM(is); |
| break; |
| case PADDING: |
| IOUtils.writeShortMM(os, marker); |
| int nextByte = 0; |
| while ((nextByte = IOUtils.read(is)) == 0xff) { |
| IOUtils.write(os, nextByte); |
| } |
| marker = (short) ((0xff << 8) | nextByte); |
| break; |
| case SOS: // There should be no meta data after this segment |
| IOUtils.writeShortMM(os, marker); |
| copyToEnd(is, os); |
| finished = true; |
| break; |
| case COM: |
| if(metadataTypes.contains(MetadataType.COMMENT)) { |
| if(comments == null) comments = new Comments(); |
| comments.addComment(readSegmentData(is)); |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP0: |
| if(metadataTypes.contains(MetadataType.JPG_JFIF)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // Not JFIF segment |
| if (temp.length < JFIF_ID.length() || ! JFIF_ID.equals(new String(temp, 0, JFIF_ID.length()))) { |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } else { // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP1: |
| // We are only interested in EXIF and XMP |
| if(metadataTypes.contains(MetadataType.EXIF) || metadataTypes.contains(MetadataType.XMP)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // XMP or EXIF segment |
| if((metadataTypes.contains(MetadataType.XMP) && temp.length >= XMP_EXT_ID.length() && new String(temp, 0, XMP_EXT_ID.length()).equals(XMP_EXT_ID)) |
| || (metadataTypes.contains(MetadataType.XMP) && temp.length >= XMP_ID.length() && new String(temp, 0, XMP_ID.length()).equals(XMP_ID)) |
| || (metadataTypes.contains(MetadataType.XMP) && temp.length >= NON_STANDARD_XMP_ID.length() && new String(temp, 0, NON_STANDARD_XMP_ID.length()).equals(NON_STANDARD_XMP_ID)) |
| || (metadataTypes.contains(MetadataType.EXIF) && temp.length >= EXIF_ID.length() && new String(temp, 0, EXIF_ID.length()).equals(EXIF_ID))) { // EXIF |
| // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } else { // We don't want to remove any of them |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP2: |
| if(metadataTypes.contains(MetadataType.ICC_PROFILE)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // Not ICC_Profile segment |
| if (temp.length < ICC_PROFILE_ID.length() || ! ICC_PROFILE_ID.equals(new String(temp, 0, ICC_PROFILE_ID.length()))) { |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } else { // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP12: |
| if(metadataTypes.contains(MetadataType.JPG_DUCKY)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // Not Ducky segment |
| if (temp.length < DUCKY_ID.length() || ! DUCKY_ID.equals(new String(temp, 0, DUCKY_ID.length()))) { |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } else { // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP13: |
| if(metadataTypes.contains(MetadataType.PHOTOSHOP_IRB) || metadataTypes.contains(MetadataType.IPTC) |
| || metadataTypes.contains(MetadataType.XMP) || metadataTypes.contains(MetadataType.EXIF)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // PHOTOSHOP IRB segment |
| if (temp.length >= PHOTOSHOP_IRB_ID.length() && new String(temp, 0, PHOTOSHOP_IRB_ID.length()).equals(PHOTOSHOP_IRB_ID)) { |
| IRB irb = new IRB(ArrayUtils.subArray(temp, PHOTOSHOP_IRB_ID.length(), temp.length - PHOTOSHOP_IRB_ID.length())); |
| // Shallow copy the map. |
| Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM()); |
| if(!metadataTypes.contains(MetadataType.PHOTOSHOP_IRB)) { |
| if(metadataTypes.contains(MetadataType.IPTC)) { |
| // We only remove IPTC_NAA and keep the other IRB data untouched. |
| _8BIM bim = bimMap.remove(ImageResourceID.IPTC_NAA.getValue()); |
| if(bim != null) extraMetadataMap.put(MetadataType.IPTC, new IPTC(bim.getData())); |
| } |
| if(metadataTypes.contains(MetadataType.XMP)) { |
| // We only remove XMP and keep the other IRB data untouched. |
| _8BIM bim = bimMap.remove(ImageResourceID.XMP_METADATA.getValue()); |
| if(bim != null) extraMetadataMap.put(MetadataType.XMP, new JpegXMP(bim.getData())); |
| } |
| if(metadataTypes.contains(MetadataType.EXIF)) { |
| // We only remove EXIF and keep the other IRB data untouched. |
| _8BIM bim = bimMap.remove(ImageResourceID.EXIF_DATA1.getValue()); |
| if(bim != null) extraMetadataMap.put(MetadataType.EXIF, new JpegExif(bim.getData())); |
| // I can't find more information on this one, so remove it just in case. |
| bimMap.remove(ImageResourceID.EXIF_DATA3.getValue()); |
| } |
| // Write back the IRB |
| writeIRB(os, bimMap.values()); |
| } else { // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } |
| } else { |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| case APP14: |
| if(metadataTypes.contains(MetadataType.JPG_ADOBE)) { |
| length = IOUtils.readUnsignedShortMM(is); |
| byte[] temp = new byte[length - 2]; |
| IOUtils.readFully(is, temp); |
| // Not Adobe segment |
| if (temp.length < ADOBE_ID.length() || ! ADOBE_ID.equals(new String(temp, 0, ADOBE_ID.length()))) { |
| IOUtils.writeShortMM(os, marker); |
| IOUtils.writeShortMM(os, (short) length); |
| IOUtils.write(os, temp); |
| } else { // We put it into the Segment map for further use |
| appnSegments.add(new Segment(emarker, length, temp)); |
| } |
| marker = IOUtils.readShortMM(is); |
| } else |
| marker = copySegment(marker, is, os); |
| break; |
| default: |
| marker = copySegment(marker, is, os); |
| } |
| } |
| } |
| |
| extractMetadataFromAPPn(appnSegments, metadataMap); |
| |
| // If we are supposed to remove IPTC, check if we have removed it from IRB. If yes, add it |
| // to the removed map |
| if(metadataTypes.contains(MetadataType.IPTC) && metadataMap.get(MetadataType.IPTC) == null) { |
| Metadata meta = extraMetadataMap.get(MetadataType.IPTC); |
| if(meta != null) metadataMap.put(MetadataType.IPTC, meta); |
| } |
| // If we are supposed to remove XMP, check if we have removed it from IRB. If yes, add it |
| // to the removed map |
| if(metadataTypes.contains(MetadataType.XMP) && metadataMap.get(MetadataType.XMP) == null) { |
| Metadata meta = extraMetadataMap.get(MetadataType.XMP); |
| if(meta != null) metadataMap.put(MetadataType.XMP, meta); |
| } |
| // If we are supposed to remove EXIF, check if we have removed it from IRB. If yes, add it |
| // to the removed map |
| if(metadataTypes.contains(MetadataType.EXIF) && metadataMap.get(MetadataType.EXIF) == null) { |
| Metadata meta = extraMetadataMap.get(MetadataType.EXIF); |
| if(meta != null) metadataMap.put(MetadataType.EXIF, meta); |
| } |
| |
| if(comments != null) |
| metadataMap.put(MetadataType.COMMENT, comments); |
| |
| return metadataMap; |
| } |
| |
| @SuppressWarnings("unused") |
| private static short skipSOS(InputStream is) throws IOException { |
| int nextByte = 0; |
| short marker = 0; |
| |
| while((nextByte = IOUtils.read(is)) != -1) { |
| if(nextByte == 0xff) { |
| nextByte = IOUtils.read(is); |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| if (nextByte != 0x00) { // This is a marker |
| marker = (short)((0xff<<8)|nextByte); |
| |
| switch (Marker.fromShort(marker)) { |
| case RST0: |
| case RST1: |
| case RST2: |
| case RST3: |
| case RST4: |
| case RST5: |
| case RST6: |
| case RST7: |
| continue; |
| default: |
| } |
| break; |
| } |
| } |
| } |
| |
| if (nextByte == -1) { |
| throw new IOException("Premature end of SOS segment!"); |
| } |
| |
| return marker; |
| } |
| |
| private static void writeComment(String comment, OutputStream os) throws IOException { |
| new COMBuilder().comment(comment).build().write(os); |
| } |
| |
| /** |
| * Write ICC_Profile as one or more APP2 segments |
| * <p> |
| * Due to the JPEG segment length limit, we have |
| * to split ICC_Profile data and put them into |
| * different APP2 segments if the data can not fit |
| * into one segment. |
| * |
| * @param os output stream to write the ICC_Profile |
| * @param data ICC_Profile data |
| * @throws IOException |
| */ |
| private static void writeICCProfile(OutputStream os, byte[] data) throws IOException { |
| // ICC_Profile ID |
| int maxSegmentLen = 65535; |
| int maxICCDataLen = 65519; |
| int numOfSegment = data.length/maxICCDataLen; |
| int leftOver = data.length%maxICCDataLen; |
| int totalSegment = (numOfSegment == 0)? 1: ((leftOver == 0)? numOfSegment: (numOfSegment + 1)); |
| for(int i = 0; i < numOfSegment; i++) { |
| IOUtils.writeShortMM(os, Marker.APP2.getValue()); |
| IOUtils.writeShortMM(os, maxSegmentLen); |
| IOUtils.write(os, ICC_PROFILE_ID.getBytes()); |
| IOUtils.writeShortMM(os, totalSegment|(i+1)<<8); |
| IOUtils.write(os, data, i*maxICCDataLen, maxICCDataLen); |
| } |
| if(leftOver != 0) { |
| IOUtils.writeShortMM(os, Marker.APP2.getValue()); |
| IOUtils.writeShortMM(os, leftOver + 16); |
| IOUtils.write(os, ICC_PROFILE_ID.getBytes()); |
| IOUtils.writeShortMM(os, totalSegment|totalSegment<<8); |
| IOUtils.write(os, data, data.length - leftOver, leftOver); |
| } |
| } |
| |
| private static void writeIRB(OutputStream os, _8BIM ... bims) throws IOException { |
| if(bims != null && bims.length > 0) |
| writeIRB(os, Arrays.asList(bims)); |
| } |
| |
| private static void writeIRB(OutputStream os, Collection<_8BIM> bims) throws IOException { |
| if(bims != null && bims.size() > 0) { |
| IOUtils.writeShortMM(os, Marker.APP13.getValue()); |
| ByteArrayOutputStream bout = new ByteArrayOutputStream(); |
| for(_8BIM bim : bims) |
| bim.write(bout); |
| // Write segment length |
| IOUtils.writeShortMM(os, 14 + 2 + bout.size()); |
| // Write segment data |
| os.write(PHOTOSHOP_IRB_ID.getBytes()); |
| os.write(bout.toByteArray()); |
| } |
| } |
| |
| // Prevent from instantiation |
| private JPGMeta() {} |
| } |