diff --git a/app/src/main/java/androidxc/exifinterface/media/ExifInterface.java b/app/src/main/java/androidxc/exifinterface/media/ExifInterface.java index 1ea73fe80..ecb8d1788 100644 --- a/app/src/main/java/androidxc/exifinterface/media/ExifInterface.java +++ b/app/src/main/java/androidxc/exifinterface/media/ExifInterface.java @@ -16,7 +16,6 @@ package androidxc.exifinterface.media; -import static androidxc.exifinterface.media.ExifInterfaceUtils.byteArrayToHexString; import static androidxc.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor; import static androidxc.exifinterface.media.ExifInterfaceUtils.closeQuietly; import static androidxc.exifinterface.media.ExifInterfaceUtils.convertToLongArray; @@ -24,6 +23,9 @@ import static androidxc.exifinterface.media.ExifInterfaceUtils.parseSubSeconds; import static androidxc.exifinterface.media.ExifInterfaceUtils.startsWith; +import static java.nio.ByteOrder.BIG_ENDIAN; +import static java.nio.ByteOrder.LITTLE_ENDIAN; + import android.annotation.SuppressLint; import android.content.res.AssetManager; import android.graphics.Bitmap; @@ -85,7 +87,11 @@ *

* Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF. *

- * Supported for writing: JPEG, PNG, WebP, DNG. + * Supported for writing: JPEG, PNG, WebP. + *

+ * Note: JPEG and HEIF files may contain XMP data either inside the Exif data chunk or outside of + * it. This class will search both locations for XMP data, but if XMP data exist both inside and + * outside Exif, will favor the XMP data inside Exif over the one outside. */ public class ExifInterface { private static final String TAG = "ExifInterface"; @@ -3722,7 +3728,6 @@ boolean isFormatCompatible(int format) { new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT), new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT), new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL), - new ExifTag(TAG_XMP, 700, IFD_FORMAT_BYTE), new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING), new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG), new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG), @@ -3890,7 +3895,7 @@ boolean isFormatCompatible(int format) { @SuppressWarnings("unchecked") private final HashMap[] mAttributes = new HashMap[EXIF_TAGS.length]; private Set mAttributesOffsets = new HashSet<>(EXIF_TAGS.length); - private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN; + private ByteOrder mExifByteOrder = BIG_ENDIAN; private boolean mHasThumbnail; private boolean mHasThumbnailStrips; private boolean mAreThumbnailStripsConsecutive; @@ -4578,11 +4583,8 @@ private void loadAttributes(@NonNull InputStream in) { // Check file type if (!mIsExifDataOnly) { - if (!(in instanceof ByteArrayInputStream)) { - in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE); - } - - mMimeType = getMimeType(in); + in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE); + mMimeType = getMimeType((BufferedInputStream) in); } if (shouldSupportSeek(mMimeType)) { @@ -4667,7 +4669,7 @@ private void printAttributes() { * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write * and make a single call rather than multiple calls for each attribute. *

- * This method is supported for JPEG, PNG, WebP, and DNG formats. + * This method is supported for JPEG, PNG, and WebP formats. *

* Note: after calling this method, any attempts to obtain range information * from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()} @@ -4681,7 +4683,7 @@ private void printAttributes() { public void saveAttributes() throws IOException { if (!isSupportedFormatForSavingAttributes(mMimeType)) { throw new IOException("ExifInterface only supports saving attributes for JPEG, PNG, " - + "WebP, and DNG formats."); + + "and WebP formats."); } if (mSeekableFileDescriptor == null && mFilename == null) { throw new IOException( @@ -4750,10 +4752,6 @@ public void saveAttributes() throws IOException { savePngAttributes(bufferedIn, bufferedOut); } else if (mMimeType == IMAGE_TYPE_WEBP) { saveWebpAttributes(bufferedIn, bufferedOut); - } else if (mMimeType == IMAGE_TYPE_DNG || mMimeType == IMAGE_TYPE_UNKNOWN) { - ByteOrderedDataOutputStream dataOutputStream = - new ByteOrderedDataOutputStream(bufferedOut, ByteOrder.BIG_ENDIAN); - writeExifSegment(dataOutputStream); } } catch (Exception e) { try { @@ -4794,7 +4792,7 @@ public void saveAttributes() throws IOException { public void saveAttributes(InputStream original, OutputStream out) throws IOException { if (!isSupportedFormatForSavingAttributes(mMimeType)) { throw new IOException("ExifInterface only supports saving attributes for JPEG, PNG, " - + "WebP, and DNG formats."); + + "and DNG formats."); } if (mHasThumbnail && mHasThumbnailStrips && !mAreThumbnailStripsConsecutive) { @@ -4815,10 +4813,6 @@ public void saveAttributes(InputStream original, OutputStream out) throws IOExce savePngAttributes(original, out); } else if (mMimeType == IMAGE_TYPE_WEBP) { saveWebpAttributes(original, out); - } else if (mMimeType == IMAGE_TYPE_DNG || mMimeType == IMAGE_TYPE_UNKNOWN) { - ByteOrderedDataOutputStream dataOutputStream = - new ByteOrderedDataOutputStream(out, ByteOrder.BIG_ENDIAN); - writeExifSegment(dataOutputStream); } // Discard the thumbnail in memory @@ -4896,15 +4890,11 @@ public byte[] getThumbnailBytes() { throw new FileNotFoundException(); } - if (in.skip(mThumbnailOffset + mOffsetToExifData) - != mThumbnailOffset + mOffsetToExifData) { - throw new IOException("Corrupted image"); - } + ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in); + inputStream.skipFully(mThumbnailOffset + mOffsetToExifData); // TODO: Need to handle potential OutOfMemoryError byte[] buffer = new byte[mThumbnailLength]; - if (in.read(buffer) != mThumbnailLength) { - throw new IOException("Corrupted image"); - } + inputStream.readFully(buffer); mThumbnailBytes = buffer; return buffer; } catch (Exception e) { @@ -5375,7 +5365,7 @@ private String convertDecimalDegree(double decimalDegree) { } // Checks the type of image file - private int getMimeType(InputStream in) throws IOException { + private int getMimeType(BufferedInputStream in) throws IOException { in.mark(SIGNATURE_CHECK_SIZE); byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE]; in.read(signatureCheckBytes); @@ -5435,7 +5425,7 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException { long chunkSize = signatureInputStream.readInt(); byte[] chunkType = new byte[4]; - signatureInputStream.read(chunkType); + signatureInputStream.readFully(chunkType); if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) { return false; @@ -5470,7 +5460,9 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException { boolean isMif1 = false; boolean isHeic = false; for (long i = 0; i < chunkDataSize / 4; ++i) { - if (signatureInputStream.read(brand) != brand.length) { + try { + signatureInputStream.readFully(brand); + } catch (EOFException e) { return false; } if (i == 1) { @@ -5620,7 +5612,7 @@ private void getJpegAttributes(ByteOrderedDataInputStream in, int offsetToJpeg, Log.d(TAG, "getJpegAttributes starting with: " + in); } // JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html - in.setByteOrder(ByteOrder.BIG_ENDIAN); + in.setByteOrder(BIG_ENDIAN); int bytesRead = 0; @@ -5695,9 +5687,7 @@ private void getJpegAttributes(ByteOrderedDataInputStream in, int offsetToJpeg, case MARKER_COM: { byte[] bytes = new byte[length]; - if (in.read(bytes) != length) { - throw new IOException("Invalid exif"); - } + in.readFully(bytes); length = 0; if (getAttribute(TAG_USER_COMMENT) == null) { mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString( @@ -5804,16 +5794,16 @@ private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException byte[] offsetToJpegBytes = new byte[4]; byte[] jpegLengthBytes = new byte[4]; byte[] cfaHeaderOffsetBytes = new byte[4]; - in.read(offsetToJpegBytes); - in.read(jpegLengthBytes); - in.read(cfaHeaderOffsetBytes); + in.readFully(offsetToJpegBytes); + in.readFully(jpegLengthBytes); + in.readFully(cfaHeaderOffsetBytes); int offsetToJpeg = ByteBuffer.wrap(offsetToJpegBytes).getInt(); int jpegLength = ByteBuffer.wrap(jpegLengthBytes).getInt(); int cfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt(); byte[] jpegBytes = new byte[jpegLength]; in.skipFully(offsetToJpeg - in.position()); - in.read(jpegBytes); + in.readFully(jpegBytes); // Retrieve JPEG image metadata ByteOrderedDataInputStream jpegInputStream = new ByteOrderedDataInputStream(jpegBytes); @@ -5823,7 +5813,7 @@ private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException in.skipFully(cfaHeaderOffset - in.position()); // Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists - in.setByteOrder(ByteOrder.BIG_ENDIAN); + in.setByteOrder(BIG_ENDIAN); int numberOfDirectoryEntry = in.readInt(); if (DEBUG) { Log.d(TAG, "numberOfDirectoryEntry: " + numberOfDirectoryEntry); @@ -5981,9 +5971,7 @@ public long getSize() throws IOException { } in.seek(offset); byte[] identifier = new byte[6]; - if (in.read(identifier) != 6) { - throw new IOException("Can't read identifier"); - } + in.readFully(identifier); offset += 6; length -= 6; if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { @@ -5992,14 +5980,11 @@ public long getSize() throws IOException { // TODO: Need to handle potential OutOfMemoryError byte[] bytes = new byte[length]; - if (in.read(bytes) != length) { - throw new IOException("Can't read exif"); - } + in.readFully(bytes); // Save offset to EXIF data for handling thumbnail and attribute offsets. mOffsetToExifData = offset; readExifSegment(bytes, IFD_TYPE_PRIMARY); } - if (DEBUG) { Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); } @@ -6154,7 +6139,7 @@ private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException // PNG uses Big Endian by default. // See PNG (Portable Network Graphics) Specification, Version 1.2, // 2.1. Integers and byte order - in.setByteOrder(ByteOrder.BIG_ENDIAN); + in.setByteOrder(BIG_ENDIAN); int bytesRead = 0; @@ -6178,10 +6163,7 @@ private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException bytesRead += 4; byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH]; - if (in.read(type) != type.length) { - throw new IOException("Encountered invalid length while parsing PNG chunk" - + "type"); - } + in.readFully(type); bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH; // The first chunk must be the IHDR chunk @@ -6196,11 +6178,7 @@ private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) { // TODO: Need to handle potential OutOfMemoryError byte[] data = new byte[length]; - if (in.read(data) != length) { - throw new IOException("Failed to read given length for given PNG chunk " - + "type: " + byteArrayToHexString(type)); - } - + in.readFully(data); // Compare CRC values for potential data corruption. int dataCrcValue = in.readInt(); // Cyclic Redundancy Code used to check for corruption of the data @@ -6241,7 +6219,7 @@ private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException } // WebP uses little-endian by default. // See Section "Terminology & Basics" - in.setByteOrder(ByteOrder.LITTLE_ENDIAN); + in.setByteOrder(LITTLE_ENDIAN); in.skipFully(WEBP_SIGNATURE_1.length); // File size corresponds to the size of the entire file from offset 8. @@ -6265,11 +6243,8 @@ private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException // Chunk Size is odd. // See Section "RIFF File Format" byte[] code = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH]; - if (in.read(code) != code.length) { - throw new IOException("Encountered invalid length while parsing WebP chunk" - + "type"); - } - bytesRead += 4; + in.readFully(code); + bytesRead += WEBP_CHUNK_TYPE_BYTE_LENGTH; int chunkSize = in.readInt(); bytesRead += 4; @@ -6277,10 +6252,7 @@ private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException if (Arrays.equals(WEBP_CHUNK_TYPE_EXIF, code)) { // TODO: Need to handle potential OutOfMemoryError byte[] payload = new byte[chunkSize]; - if (in.read(payload) != chunkSize) { - throw new IOException("Failed to read given length for given PNG chunk " - + "type: " + byteArrayToHexString(code)); - } + in.readFully(payload); // Save offset to EXIF data for handling thumbnail and attribute offsets. mOffsetToExifData = bytesRead; readExifSegment(payload, IFD_TYPE_PRIMARY); @@ -6321,7 +6293,7 @@ private void saveJpegAttributes(InputStream inputStream, OutputStream outputStre } ByteOrderedDataInputStream dataInputStream = new ByteOrderedDataInputStream(inputStream); ByteOrderedDataOutputStream dataOutputStream = - new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN); + new ByteOrderedDataOutputStream(outputStream, BIG_ENDIAN); if (dataInputStream.readByte() != MARKER) { throw new IOException("Invalid marker"); } @@ -6365,9 +6337,7 @@ private void saveJpegAttributes(InputStream inputStream, OutputStream outputStre } byte[] identifier = new byte[6]; if (length >= 6) { - if (dataInputStream.read(identifier) != 6) { - throw new IOException("Invalid exif"); - } + dataInputStream.readFully(identifier); if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { // Skip the original EXIF APP1 segment. dataInputStream.skipFully(length - 6); @@ -6428,7 +6398,7 @@ private void savePngAttributes(InputStream inputStream, OutputStream outputStrea } ByteOrderedDataInputStream dataInputStream = new ByteOrderedDataInputStream(inputStream); ByteOrderedDataOutputStream dataOutputStream = - new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN); + new ByteOrderedDataOutputStream(outputStream, BIG_ENDIAN); // Copy PNG signature bytes copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length); @@ -6465,8 +6435,7 @@ private void savePngAttributes(InputStream inputStream, OutputStream outputStrea // the chunk type bytes and the chunk data bytes. exifByteArrayOutputStream = new ByteArrayOutputStream(); ByteOrderedDataOutputStream exifDataOutputStream = - new ByteOrderedDataOutputStream(exifByteArrayOutputStream, - ByteOrder.BIG_ENDIAN); + new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN); // Store Exif data in separate byte array writeExifSegment(exifDataOutputStream); @@ -6527,9 +6496,9 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre + ", outputStream: " + outputStream + ")"); } ByteOrderedDataInputStream totalInputStream = - new ByteOrderedDataInputStream(inputStream, ByteOrder.LITTLE_ENDIAN); + new ByteOrderedDataInputStream(inputStream, LITTLE_ENDIAN); ByteOrderedDataOutputStream totalOutputStream = - new ByteOrderedDataOutputStream(outputStream, ByteOrder.LITTLE_ENDIAN); + new ByteOrderedDataOutputStream(outputStream, LITTLE_ENDIAN); // WebP signature copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length); @@ -6541,8 +6510,7 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre try { nonHeaderByteArrayOutputStream = new ByteArrayOutputStream(); ByteOrderedDataOutputStream nonHeaderOutputStream = - new ByteOrderedDataOutputStream(nonHeaderByteArrayOutputStream, - ByteOrder.LITTLE_ENDIAN); + new ByteOrderedDataOutputStream(nonHeaderByteArrayOutputStream, LITTLE_ENDIAN); if (mOffsetToExifData != 0) { // EXIF chunk exists in the original file @@ -6556,6 +6524,11 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre // Skip input stream to the end of the EXIF chunk totalInputStream.skipFully(WEBP_CHUNK_TYPE_BYTE_LENGTH); int exifChunkLength = totalInputStream.readInt(); + // RIFF chunks have a single padding byte at the end if the declared chunk size is + // odd. + if (exifChunkLength % 2 != 0) { + exifChunkLength++; + } totalInputStream.skipFully(exifChunkLength); // Write new EXIF chunk to output stream @@ -6563,17 +6536,14 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre } else { // EXIF chunk does not exist in the original file byte[] firstChunkType = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH]; - if (totalInputStream.read(firstChunkType) != firstChunkType.length) { - throw new IOException("Encountered invalid length while parsing WebP chunk " - + "type"); - } + totalInputStream.readFully(firstChunkType); if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8X)) { // Original file already includes other extra data int size = totalInputStream.readInt(); // WebP files have a single padding byte at the end if the chunk size is odd. byte[] data = new byte[(size % 2) == 1 ? size + 1 : size]; - totalInputStream.read(data); + totalInputStream.readFully(data); // Set the EXIF flag to 1 data[0] = (byte) (data[0] | (1 << 3)); @@ -6599,10 +6569,14 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre while (true) { byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH]; - @SuppressWarnings("unused") - int read = inputStream.read(type); - if (!Arrays.equals(type, WEBP_CHUNK_TYPE_ANMF)) { - // Either we have reached EOF or the start of a non-ANMF chunk + boolean animationFinished = false; + try { + totalInputStream.readFully(type); + animationFinished = !Arrays.equals(type, WEBP_CHUNK_TYPE_ANMF); + } catch (EOFException e) { + animationFinished = true; + } + if (animationFinished) { writeExifSegment(nonHeaderOutputStream); break; } @@ -6627,19 +6601,18 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre int widthAndHeight = 0; int width = 0; int height = 0; - int alpha = 0; + boolean alpha = false; // Save VP8 frame data for later byte[] vp8Frame = new byte[3]; if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)) { - totalInputStream.read(vp8Frame); + totalInputStream.readFully(vp8Frame); // Check signature byte[] vp8Signature = new byte[3]; - if (totalInputStream.read(vp8Signature) != vp8Signature.length - || !Arrays.equals(WEBP_VP8_SIGNATURE, vp8Signature)) { - throw new IOException("Encountered error while checking VP8 " - + "signature"); + totalInputStream.readFully(vp8Signature); + if (!Arrays.equals(WEBP_VP8_SIGNATURE, vp8Signature)) { + throw new IOException("Error checking VP8 signature"); } // Retrieve image width/height @@ -6651,18 +6624,17 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre // Check signature byte vp8lSignature = totalInputStream.readByte(); if (vp8lSignature != WEBP_VP8L_SIGNATURE) { - throw new IOException("Encountered error while checking VP8L " - + "signature"); + throw new IOException("Error checking VP8L signature"); } // Retrieve image width/height widthAndHeight = totalInputStream.readInt(); - // VP8L stores width - 1 and height - 1 values. See "2 RIFF Header" of - // "WebP Lossless Bitstream Specification" - width = ((widthAndHeight << 18) >> 18) + 1; - height = ((widthAndHeight << 4) >> 18) + 1; - // Retrieve alpha bit - alpha = widthAndHeight & (1 << 3); + // VP8L stores 14-bit 'width - 1' and 'height - 1' values. See "RIFF Header" + // of "WebP Lossless Bitstream Specification". + width = (widthAndHeight & 0x3FFF) + 1; // Read bits 0 - 13 + height = ((widthAndHeight & 0xFFFC000) >>> 14) + 1; // Read bits 14 - 27 + // Retrieve alpha bit 28 + alpha = (widthAndHeight & 1 << 28) != 0; bytesToRead -= (1 /* VP8L signature */ + 4); } @@ -6670,10 +6642,12 @@ private void saveWebpAttributes(InputStream inputStream, OutputStream outputStre nonHeaderOutputStream.write(WEBP_CHUNK_TYPE_VP8X); nonHeaderOutputStream.writeInt(WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH); byte[] data = new byte[WEBP_CHUNK_TYPE_VP8X_DEFAULT_LENGTH]; + // ALPHA flag + if (alpha) { + data[0] = (byte) (data[0] | (1 << 4)); + } // EXIF flag data[0] = (byte) (data[0] | (1 << 3)); - // ALPHA flag - data[0] = (byte) (data[0] | (alpha << 4)); // VP8X stores Width - 1 and Height - 1 values width -= 1; height -= 1; @@ -6723,12 +6697,7 @@ private void copyChunksUpToGivenChunkType(ByteOrderedDataInputStream inputStream byte[] secondGivenType) throws IOException { while (true) { byte[] type = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH]; - if (inputStream.read(type) != type.length) { - throw new IOException("Encountered invalid length while copying WebP chunks up to" - + "chunk type " + new String(firstGivenType, ASCII) - + ((secondGivenType == null) ? "" : " or " + new String(secondGivenType, - ASCII))); - } + inputStream.readFully(type); copyWebPChunk(inputStream, outputStream, type); if (Arrays.equals(type, firstGivenType) || (secondGivenType != null && Arrays.equals(type, secondGivenType))) { @@ -6794,12 +6763,12 @@ private ByteOrder readByteOrder(ByteOrderedDataInputStream dataInputStream) if (DEBUG) { Log.d(TAG, "readExifSegment: Byte Align II"); } - return ByteOrder.LITTLE_ENDIAN; + return LITTLE_ENDIAN; case BYTE_ALIGN_MM: if (DEBUG) { Log.d(TAG, "readExifSegment: Byte Align MM"); } - return ByteOrder.BIG_ENDIAN; + return BIG_ENDIAN; default: throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder)); } @@ -6832,7 +6801,7 @@ private void parseTiffHeaders(ByteOrderedDataInputStream dataInputStream) throws private void readImageFileDirectory(SeekableByteOrderedDataInputStream dataInputStream, @IfdType int ifdType) throws IOException { // Save offset of current IFD to prevent reading an IFD that is already read. - mAttributesOffsets.add(dataInputStream.mPosition); + mAttributesOffsets.add(dataInputStream.position()); // See TIFF 6.0 Section 2: TIFF Structure, Figure 1. short numberOfDirectoryEntry = dataInputStream.readShort(); @@ -6967,9 +6936,11 @@ private void readImageFileDirectory(SeekableByteOrderedDataInputStream dataInput } // Check if the next IFD offset - // 1. Is a non-negative value, and + // 1. Is a non-negative value (within the length of the input, if known), and // 2. Does not point to a previously read IFD. - if (offset > 0L) { + if (offset > 0L + && (dataInputStream.length() == ByteOrderedDataInputStream.LENGTH_UNSET + || offset < dataInputStream.length())) { if (!mAttributesOffsets.contains((int) offset)) { dataInputStream.seek(offset); readImageFileDirectory(dataInputStream, nextIfdType); @@ -6981,7 +6952,12 @@ private void readImageFileDirectory(SeekableByteOrderedDataInputStream dataInput } } else { if (DEBUG) { - Log.d(TAG, "Skip jump into the IFD since its offset is invalid: " + offset); + String message = + "Skip jump into the IFD since its offset is invalid: " + offset; + if (dataInputStream.length() != ByteOrderedDataInputStream.LENGTH_UNSET) { + message += " (total length: " + dataInputStream.length() + ")"; + } + Log.d(TAG, message); } } @@ -7079,7 +7055,7 @@ private void retrieveJpegImageSize(SeekableByteOrderedDataInputStream in, int im // Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags in.seek(jpegInterchangeFormat); byte[] jpegBytes = new byte[jpegInterchangeFormatLength]; - in.read(jpegBytes); + in.readFully(jpegBytes); getJpegAttributes(new ByteOrderedDataInputStream(jpegBytes), jpegInterchangeFormat, imageType); } @@ -7139,8 +7115,8 @@ private void handleThumbnailFromJfif(ByteOrderedDataInputStream in, HashMap thum // TODO: Need to handle potential OutOfMemoryError // Save the thumbnail in memory if the input doesn't support reading again. byte[] thumbnailBytes = new byte[thumbnailLength]; - in.skip(thumbnailOffset); - in.read(thumbnailBytes); + in.skipFully(thumbnailOffset); + in.readFully(thumbnailBytes); mThumbnailBytes = thumbnailBytes; } mThumbnailOffset = thumbnailOffset; @@ -7209,14 +7185,18 @@ private void handleThumbnailFromStrips(ByteOrderedDataInputStream in, HashMap th Log.d(TAG, "Invalid strip offset value"); return; } - if (in.skip(bytesToSkip) != bytesToSkip) { + try { + in.skipFully(bytesToSkip); + } catch (EOFException e) { Log.d(TAG, "Failed to skip " + bytesToSkip + " bytes."); return; } bytesRead += bytesToSkip; // TODO: Need to handle potential OutOfMemoryError byte[] stripBytes = new byte[stripByteCount]; - if (in.read(stripBytes) != stripByteCount) { + try { + in.readFully(stripBytes); + } catch (EOFException e) { Log.d(TAG, "Failed to read " + stripByteCount + " bytes."); return; } @@ -7535,6 +7515,11 @@ private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throw switch (mMimeType) { case IMAGE_TYPE_JPEG: + if (totalSize > 0xFFFF) { + throw new IllegalStateException( + "Size of exif data (" + totalSize + " bytes) exceeds the max size of a " + + "JPEG APP1 segment (65536 bytes)"); + } // Write JPEG specific data (APP1 size, APP1 identifier) dataOutputStream.writeUnsignedShort(totalSize); dataOutputStream.write(IDENTIFIER_EXIF_APP1); @@ -7552,8 +7537,7 @@ private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throw } // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1. - dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN - ? BYTE_ALIGN_MM : BYTE_ALIGN_II); + dataOutputStream.writeShort(mExifByteOrder == BIG_ENDIAN ? BYTE_ALIGN_MM : BYTE_ALIGN_II); dataOutputStream.setByteOrder(mExifByteOrder); dataOutputStream.writeUnsignedShort(START_CODE); dataOutputStream.writeUnsignedInt(IFD_OFFSET); @@ -7622,7 +7606,7 @@ private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throw } // Reset the byte order to big endian in order to write remaining parts of the JPEG file. - dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); + dataOutputStream.setByteOrder(BIG_ENDIAN); return totalSize; } @@ -7753,21 +7737,22 @@ public void seek(long position) throws IOException { // An input stream class that can parse both little and big endian order data. private static class ByteOrderedDataInputStream extends InputStream implements DataInput { - private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN; - private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN; - final DataInputStream mDataInputStream; - private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - int mPosition; + public static final int LENGTH_UNSET = -1; + protected final DataInputStream mDataInputStream; + protected int mPosition; + + private ByteOrder mByteOrder; private byte[] mSkipBuffer; + private int mLength; ByteOrderedDataInputStream(byte[] bytes) throws IOException { - this(new ByteArrayInputStream(bytes), ByteOrder.BIG_ENDIAN); + this(new ByteArrayInputStream(bytes), BIG_ENDIAN); + this.mLength = bytes.length; } ByteOrderedDataInputStream(InputStream in) throws IOException { - this(in, ByteOrder.BIG_ENDIAN); + this(in, BIG_ENDIAN); } ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) throws IOException { @@ -7775,6 +7760,9 @@ private static class ByteOrderedDataInputStream extends InputStream implements D mDataInputStream.mark(0); mPosition = 0; mByteOrder = byteOrder; + this.mLength = in instanceof ByteOrderedDataInputStream + ? ((ByteOrderedDataInputStream) in).length() + : LENGTH_UNSET; } public void setByteOrder(ByteOrder byteOrder) { @@ -7982,6 +7970,12 @@ public void mark(int readlimit) { public void reset() { throw new UnsupportedOperationException("Reset is currently unsupported"); } + + /** Return the total length (in bytes) of the underlying stream if known, otherwise + * {@link #LENGTH_UNSET}. */ + public int length() { + return mLength; + } } // An output stream to write EXIF data area, which can be written in either little or big endian @@ -8015,22 +8009,22 @@ public void writeByte(int val) throws IOException { } public void writeShort(short val) throws IOException { - if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + if (mByteOrder == LITTLE_ENDIAN) { mOutputStream.write((val >>> 0) & 0xFF); mOutputStream.write((val >>> 8) & 0xFF); - } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + } else if (mByteOrder == BIG_ENDIAN) { mOutputStream.write((val >>> 8) & 0xFF); mOutputStream.write((val >>> 0) & 0xFF); } } public void writeInt(int val) throws IOException { - if (mByteOrder == ByteOrder.LITTLE_ENDIAN) { + if (mByteOrder == LITTLE_ENDIAN) { mOutputStream.write((val >>> 0) & 0xFF); mOutputStream.write((val >>> 8) & 0xFF); mOutputStream.write((val >>> 16) & 0xFF); mOutputStream.write((val >>> 24) & 0xFF); - } else if (mByteOrder == ByteOrder.BIG_ENDIAN) { + } else if (mByteOrder == BIG_ENDIAN) { mOutputStream.write((val >>> 24) & 0xFF); mOutputStream.write((val >>> 16) & 0xFF); mOutputStream.write((val >>> 8) & 0xFF); @@ -8039,10 +8033,18 @@ public void writeInt(int val) throws IOException { } public void writeUnsignedShort(int val) throws IOException { + if (val > 0xFFFF) { + throw new IllegalArgumentException("val is larger than the maximum value of a " + + "16-bit unsigned integer"); + } writeShort((short) val); } public void writeUnsignedInt(long val) throws IOException { + if (val > 0xFFFF_FFFFL) { + throw new IllegalArgumentException("val is larger than the maximum value of a " + + "32-bit unsigned integer"); + } writeInt((int) val); } } @@ -8115,8 +8117,7 @@ private static boolean shouldSupportSeek(int mimeType) { private static boolean isSupportedFormatForSavingAttributes(int mimeType) { if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG - || mimeType == IMAGE_TYPE_WEBP || mimeType == IMAGE_TYPE_DNG - || mimeType == IMAGE_TYPE_UNKNOWN) { + || mimeType == IMAGE_TYPE_WEBP) { return true; } return false;