From 559c028fc8d686f665f52545b5eb3a7992e65322 Mon Sep 17 00:00:00 2001 From: Jin Park Date: Tue, 5 Jul 2016 17:00:52 +0900 Subject: [PATCH] ExifInterface: Extract primary image length/width values The primary image may not contain the tags for ImageLength and ImageWidth values if it uses the JFIF specification. This CL searches the data to retrieve those necessary values. Bug: 29409358 Change-Id: I850768af38b7b723e93833a70a2238f3fe1cc29b --- api/current.txt | 1 - api/system-current.txt | 1 - api/test-current.txt | 1 - media/java/android/media/ExifInterface.java | 211 ++++++++++++++++++---------- 4 files changed, 136 insertions(+), 78 deletions(-) diff --git a/api/current.txt b/api/current.txt index bc519691f6aa..74568dc25b7b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -20116,7 +20116,6 @@ package android.media { method public java.lang.String getAttribute(java.lang.String); method public double getAttributeDouble(java.lang.String, double); method public int getAttributeInt(java.lang.String, int); - method public long[] getAttributeLongArray(java.lang.String); method public boolean getLatLong(float[]); method public byte[] getThumbnail(); method public long[] getThumbnailRange(); diff --git a/api/system-current.txt b/api/system-current.txt index 119ce414e2ed..2d1cc8ff1e9a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -21636,7 +21636,6 @@ package android.media { method public java.lang.String getAttribute(java.lang.String); method public double getAttributeDouble(java.lang.String, double); method public int getAttributeInt(java.lang.String, int); - method public long[] getAttributeLongArray(java.lang.String); method public boolean getLatLong(float[]); method public byte[] getThumbnail(); method public long[] getThumbnailRange(); diff --git a/api/test-current.txt b/api/test-current.txt index 7b95d1aa69b9..d0a78a0a82cc 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -20186,7 +20186,6 @@ package android.media { method public java.lang.String getAttribute(java.lang.String); method public double getAttributeDouble(java.lang.String, double); method public int getAttributeInt(java.lang.String, int); - method public long[] getAttributeLongArray(java.lang.String); method public boolean getLatLong(float[]); method public byte[] getThumbnail(); method public long[] getThumbnailRange(); diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 2ea2e86fd037..a36758962b1b 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -1323,26 +1323,6 @@ public class ExifInterface { } /** - * Returns the long array value of the specified tag. If there is no such tag - * in the image file or the value cannot be parsed as an array of long, return null. - * - * @param tag the name of the tag. - */ - public long[] getAttributeLongArray(String tag) { - ExifAttribute exifAttribute = getExifAttribute(tag); - if (exifAttribute == null) { - return null; - } - - try { - return (long[]) exifAttribute.getValue(mExifByteOrder); - } catch (NumberFormatException e) { - Log.w(TAG, "Invalid value for " + tag, e); - return null; - } - } - - /** * Set the value of the specified tag. * * @param tag the name of the tag. @@ -1553,7 +1533,7 @@ public class ExifInterface { } // Process JPEG input stream - getJpegAttributes(in); + getJpegAttributes(in, IFD_TIFF_HINT); } catch (IOException e) { // Ignore exceptions in order to keep the compatibility with the old versions of // ExifInterface. @@ -1899,8 +1879,16 @@ public class ExifInterface { } } - // Loads EXIF attributes from a JPEG input stream. - private void getJpegAttributes(InputStream inputStream) throws IOException { + /** + * Loads EXIF attributes from a JPEG input stream. + * + * @param inputStream The input stream that starts with the JPEG data. + * @param imageTypes The image type from which to retrieve metadata. Use IFD_TIFF_HINT for + * primary image, IFD_PREVIEW_HINT for preview image, and + * IFD_THUMBNAIL_HINT for thumbnail image. + * @throws IOException If the data contains invalid JPEG markers, offsets, or length values. + */ + private void getJpegAttributes(InputStream inputStream, int imageType) throws IOException { // See JPEG File Interchange Format Specification, "JFIF Specification" if (DEBUG) { Log.d(TAG, "getJpegAttributes starting with: " + inputStream); @@ -2006,9 +1994,9 @@ public class ExifInterface { if (dataInputStream.skipBytes(1) != 1) { throw new IOException("Invalid SOFx"); } - mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong( + mAttributes[imageType].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong( dataInputStream.readUnsignedShort(), mExifByteOrder)); - mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong( + mAttributes[imageType].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong( dataInputStream.readUnsignedShort(), mExifByteOrder)); length -= 5; break; @@ -2030,7 +2018,9 @@ public class ExifInterface { private void getRawAttributes(InputStream in) throws IOException { int bytesRead = 0; - byte[] exifBytes = new byte[in.available()]; + int totalBytes = in.available(); + byte[] exifBytes = new byte[totalBytes]; + in.mark(in.available()); in.read(exifBytes); ByteOrderAwarenessDataInputStream dataInputStream = @@ -2042,12 +2032,28 @@ public class ExifInterface { // Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6. readImageFileDirectory(dataInputStream, IFD_PREVIEW_HINT); + // Check if the preview image data should be a primary image data. + // The 0th IFD (first to be parsed) is presumed to be a preview image data, with a SubIFD + // that is a primary image data. + // But if the 0th IFD does not have a SubIFD, then it must be a primary image data since + // a primary image data must exist, but a preview image data does not have to. + if (mAttributes[IFD_TIFF_HINT].isEmpty() && !mAttributes[IFD_PREVIEW_HINT].isEmpty()) { + mAttributes[IFD_TIFF_HINT] = mAttributes[IFD_PREVIEW_HINT]; + mAttributes[IFD_PREVIEW_HINT] = new HashMap(); + } + + // Update TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH for primary image. + updatePrimaryImageSizeValues(in); + // Check if the preview image data should be a thumbnail image data. - // In a RAW file, there may be a Preview image, which is smaller than a Primary image but - // larger than a Thumbnail image. Normally, the Preview image can be considered a thumbnail - // image if its size meets the requirements. Therefore, when a Thumbnail image has not yet - // been found, we should check if the Preview image can be one. + // In a RAW file, there may be a preview image, which is smaller than a primary image but + // larger than a thumbnail image. Normally, the preview image can be considered a thumbnail + // image if its size meets the requirements. Therefore, when a thumbnail image has not yet + // been found, we should check if the preview image can be one. if (!mAttributes[IFD_PREVIEW_HINT].isEmpty() && mAttributes[IFD_THUMBNAIL_HINT].isEmpty()) { + // Update preview image size if necessary + retrieveJpegImageSize(in, IFD_PREVIEW_HINT); + if (isThumbnail(mAttributes[IFD_PREVIEW_HINT])) { mAttributes[IFD_THUMBNAIL_HINT] = mAttributes[IFD_PREVIEW_HINT]; mAttributes[IFD_PREVIEW_HINT] = new HashMap(); @@ -2057,8 +2063,6 @@ public class ExifInterface { // Process thumbnail. processThumbnail(dataInputStream, bytesRead, exifBytes.length); - // Update TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH. - updateImageSizeValues(); } // Stores a new JPEG image with EXIF attributes into a given output stream. @@ -2375,49 +2379,85 @@ public class ExifInterface { } } - // Processes Thumbnail based on Compression Value + /** + * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags. + * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes() + * to locate SOF(Start of Frame) marker and update the image length & width values. + * See JEITA CP-3451C Table 5 and Section 4.8.1. B. + */ + private void retrieveJpegImageSize(InputStream in, int imageType) throws IOException { + // Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values + ExifAttribute imageLengthAttribute = + (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_LENGTH); + ExifAttribute imageWidthAttribute = + (ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_WIDTH); + + if (imageLengthAttribute == null || imageWidthAttribute == null) { + // Find if offset for JPEG data exists + ExifAttribute jpegInterchangeFormatAttribute = + (ExifAttribute) mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT); + if (jpegInterchangeFormatAttribute != null) { + int jpegInterchangeFormat = + jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder); + + // Skip to the JPEG data offset + in.reset(); + in.mark(in.available()); + if (in.skip(jpegInterchangeFormat) != jpegInterchangeFormat) { + Log.d(TAG, "Invalid JPEG offset"); + } + + // Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags + getJpegAttributes(in, imageType); + } + } + } + + // Processes thumbnail based on Compression Value private void processThumbnail(ByteOrderAwarenessDataInputStream dataInputStream, int exifOffsetFromBeginning, int exifBytesLength) throws IOException { - if (mAttributes[IFD_THUMBNAIL_HINT].containsKey(TAG_COMPRESSION)) { - ExifAttribute compressionAttribute = - (ExifAttribute) mAttributes[IFD_THUMBNAIL_HINT].get(TAG_COMPRESSION); + HashMap thumbnailData = mAttributes[IFD_THUMBNAIL_HINT]; + ExifAttribute compressionAttribute = (ExifAttribute) thumbnailData.get(TAG_COMPRESSION); + if (compressionAttribute != null) { int compressionValue = compressionAttribute.getIntValue(mExifByteOrder); switch (compressionValue) { case DATA_UNCOMPRESSED: { - // TODO: add implementation for reading Uncompressed Thumbnail Data (b/28156704) + // TODO: add implementation for reading uncompressed thumbnail data (b/28156704) Log.d(TAG, "Uncompressed thumbnail data cannot be processed"); break; } case DATA_JPEG: { - String jpegInterchangeFormatString = - getAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name); - String jpegInterchangeFormatLengthString = - getAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name); - if (jpegInterchangeFormatString != null - && jpegInterchangeFormatLengthString != null) { - try { - int jpegInterchangeFormat = - Integer.parseInt(jpegInterchangeFormatString); - int jpegInterchangeFormatLength = - Integer.parseInt(jpegInterchangeFormatLengthString); - retrieveJPEGThumbnail(dataInputStream, jpegInterchangeFormat, - jpegInterchangeFormatLength, exifOffsetFromBeginning, - exifBytesLength); - } catch (NumberFormatException e) { - // Ignore corrupted format/formatLength values - } + ExifAttribute jpegInterchangeFormatAttribute = + (ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT); + ExifAttribute jpegInterchangeFormatLengthAttribute = + (ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + if (jpegInterchangeFormatAttribute != null + && jpegInterchangeFormatLengthAttribute != null) { + int jpegInterchangeFormat = + jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder); + int jpegInterchangeFormatLength = + jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder); + retrieveJPEGThumbnail(dataInputStream, jpegInterchangeFormat, + jpegInterchangeFormatLength, exifOffsetFromBeginning, + exifBytesLength); } break; } case DATA_JPEG_COMPRESSED: { - long[] stripOffsetsArray = getAttributeLongArray(TAG_STRIP_OFFSETS); - long[] stripByteCountsArray = getAttributeLongArray(TAG_STRIP_BYTE_COUNTS); - if (stripOffsetsArray != null && stripByteCountsArray != null) { + ExifAttribute stripOffsetsAttribute = + (ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS); + ExifAttribute stripByteCountsAttribute = + (ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS); + if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) { + long[] stripOffsetsArray = + (long[]) stripOffsetsAttribute.getValue(mExifByteOrder); + long[] stripByteCountsArray = + (long[]) stripByteCountsAttribute.getValue(mExifByteOrder); if (stripOffsetsArray.length == 1) { int stripOffsetsSum = (int) Arrays.stream(stripOffsetsArray).sum(); - int stripByteCountSum = (int) Arrays.stream(stripByteCountsArray).sum(); + int stripByteCountsSum = (int) Arrays.stream(stripByteCountsArray).sum(); retrieveJPEGThumbnail(dataInputStream, stripOffsetsSum, - stripByteCountSum, exifOffsetFromBeginning, + stripByteCountsSum, exifOffsetFromBeginning, exifBytesLength); } else { // TODO: implement method to read multiple strips (b/29737797) @@ -2433,7 +2473,7 @@ public class ExifInterface { } } - // Retrieves Thumbnail for JPEG Compression + // Retrieves thumbnail for JPEG Compression private void retrieveJPEGThumbnail(ByteOrderAwarenessDataInputStream dataInputStream, int thumbnailOffset, int thumbnailLength, int exifOffsetFromBeginning, int exifBytesLength) throws IOException { @@ -2471,7 +2511,6 @@ public class ExifInterface { private boolean isThumbnail(HashMap map) throws IOException { ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH); ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH); - if (imageLengthAttribute != null && imageWidthAttribute != null) { int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder); int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder); @@ -2483,14 +2522,22 @@ public class ExifInterface { } /** - * Raw images often store extra pixels around the edges of the final image, which results in - * larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags. + * If image is uncompressed, ImageWidth/Length tags are used to store size info. + * However, uncompressed images often store extra pixels around the edges of the final image, + * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags. * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE - * and then TAG_PIXEL_X_DIMENSION & TAG_PIXEL_Y_DIMENSION. - * See DNG Specification 1.4.0.0. Section 4 (DefaultCropSize) & JEITA CP-3451 p26. + * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize) + * + * If image is JPEG compressed, PixelXDimension/PixelYDimension tags are used for size info. + * However, an image may have padding at the right end or bottom end of the image to make sure + * that the values are multiples of 64. If so, the increased value will be saved in the + * SOF(Start of Frame). In order to assure that valid image size values are stored, this method + * checks TAG_PIXEL_X_DIMENSION & TAG_PIXEL_Y_DIMENSION and updates values if necessary. + * See JEITA CP-3451C Table 5 and Section 4.8.1. B. * */ - private void updateImageSizeValues() throws IOException { - // Checks for the NewSubfileType tag and returns if the image is not original resolution. + private void updatePrimaryImageSizeValues(InputStream in) throws IOException { + // Checks for the NewSubfileType tag and returns if the image is not original resolution, + // which means that it is not the primary imiage ExifAttribute newSubfileTypeAttribute = (ExifAttribute) mAttributes[IFD_TIFF_HINT].get(TAG_NEW_SUBFILE_TYPE); if (newSubfileTypeAttribute != null) { @@ -2501,13 +2548,17 @@ public class ExifInterface { } } + // Uncompressed image valid image size values ExifAttribute defaultCropSizeAttribute = (ExifAttribute) mAttributes[IFD_TIFF_HINT].get(TAG_DEFAULT_CROP_SIZE); + // Compressed image valid image size values ExifAttribute pixelXDimAttribute = (ExifAttribute) mAttributes[IFD_EXIF_HINT].get(TAG_PIXEL_X_DIMENSION); ExifAttribute pixelYDimAttribute = (ExifAttribute) mAttributes[IFD_EXIF_HINT].get(TAG_PIXEL_Y_DIMENSION); + if (defaultCropSizeAttribute != null) { + // Update for uncompressed image ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute; if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) { Rational[] defaultCropSizeValue = @@ -2526,9 +2577,15 @@ public class ExifInterface { } mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute); mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute); - } else if (pixelXDimAttribute != null && pixelYDimAttribute != null) { - mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, pixelXDimAttribute); - mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, pixelYDimAttribute); + } else { + // Update for JPEG image + if (pixelXDimAttribute != null && pixelYDimAttribute != null) { + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH, pixelXDimAttribute); + mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH, pixelYDimAttribute); + } else { + // Update image size values from SOF marker if necessary + retrieveJpegImageSize(in, IFD_TIFF_HINT); + } } } @@ -2582,9 +2639,9 @@ public class ExifInterface { ExifAttribute.createULong(0, mExifByteOrder)); } if (mHasThumbnail) { - mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + mAttributes[IFD_THUMBNAIL_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, ExifAttribute.createULong(0, mExifByteOrder)); - mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name, + mAttributes[IFD_THUMBNAIL_HINT].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name, ExifAttribute.createULong(mThumbnailLength, mExifByteOrder)); } @@ -2612,7 +2669,7 @@ public class ExifInterface { } if (mHasThumbnail) { int thumbnailOffset = position; - mAttributes[IFD_TIFF_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, + mAttributes[IFD_THUMBNAIL_HINT].put(JPEG_INTERCHANGE_FORMAT_TAG.name, ExifAttribute.createULong(thumbnailOffset, mExifByteOrder)); mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset; position += mThumbnailLength; @@ -2818,8 +2875,12 @@ public class ExifInterface { } public void seek(long byteCount) throws IOException { - mPosition = 0L; - reset(); + if (mPosition > byteCount) { + mPosition = 0L; + reset(); + } else { + byteCount -= mPosition; + } if (skip(byteCount) != byteCount) { throw new IOException("Couldn't seek up to the byteCount"); } -- 2.11.0