com.drew.imaging.jpeg.JpegSegmentReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metadata-extractor Show documentation
Show all versions of metadata-extractor Show documentation
Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image and video files.
/*
* Copyright 2002-2012 Drew Noakes
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* More information about this project is available at:
*
* http://drewnoakes.com/code/exif/
* http://code.google.com/p/metadata-extractor/
*/
package com.drew.imaging.jpeg;
import com.drew.lang.annotations.NotNull;
import com.drew.lang.annotations.Nullable;
import java.io.*;
/**
* Performs read functions of Jpeg files, returning specific file segments.
* @author Drew Noakes http://drewnoakes.com
*/
public class JpegSegmentReader
{
// TODO add a findAvailableSegments() method
// TODO add more segment identifiers
// TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'
@NotNull
private final JpegSegmentData _segmentData;
/**
* Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
*/
private static final byte SEGMENT_SOS = (byte)0xDA;
/**
* Private, because one wouldn't search for it.
*/
private static final byte MARKER_EOI = (byte)0xD9;
/** APP0 Jpeg segment identifier -- JFIF data (also JFXX apparently). */
public static final byte SEGMENT_APP0 = (byte)0xE0;
/** APP1 Jpeg segment identifier -- where Exif data is kept. XMP data is also kept in here, though usually in a second instance. */
public static final byte SEGMENT_APP1 = (byte)0xE1;
/** APP2 Jpeg segment identifier. */
public static final byte SEGMENT_APP2 = (byte)0xE2;
/** APP3 Jpeg segment identifier. */
public static final byte SEGMENT_APP3 = (byte)0xE3;
/** APP4 Jpeg segment identifier. */
public static final byte SEGMENT_APP4 = (byte)0xE4;
/** APP5 Jpeg segment identifier. */
public static final byte SEGMENT_APP5 = (byte)0xE5;
/** APP6 Jpeg segment identifier. */
public static final byte SEGMENT_APP6 = (byte)0xE6;
/** APP7 Jpeg segment identifier. */
public static final byte SEGMENT_APP7 = (byte)0xE7;
/** APP8 Jpeg segment identifier. */
public static final byte SEGMENT_APP8 = (byte)0xE8;
/** APP9 Jpeg segment identifier. */
public static final byte SEGMENT_APP9 = (byte)0xE9;
/** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */
public static final byte SEGMENT_APPA = (byte)0xEA;
/** APPB (App11) Jpeg segment identifier. */
public static final byte SEGMENT_APPB = (byte)0xEB;
/** APPC (App12) Jpeg segment identifier. */
public static final byte SEGMENT_APPC = (byte)0xEC;
/** APPD (App13) Jpeg segment identifier -- IPTC data in here. */
public static final byte SEGMENT_APPD = (byte)0xED;
/** APPE (App14) Jpeg segment identifier. */
public static final byte SEGMENT_APPE = (byte)0xEE;
/** APPF (App15) Jpeg segment identifier. */
public static final byte SEGMENT_APPF = (byte)0xEF;
/** Start Of Image segment identifier. */
public static final byte SEGMENT_SOI = (byte)0xD8;
/** Define Quantization Table segment identifier. */
public static final byte SEGMENT_DQT = (byte)0xDB;
/** Define Huffman Table segment identifier. */
public static final byte SEGMENT_DHT = (byte)0xC4;
/** Start-of-Frame Zero segment identifier. */
public static final byte SEGMENT_SOF0 = (byte)0xC0;
/** Jpeg comment segment identifier. */
public static final byte SEGMENT_COM = (byte)0xFE;
/**
* Creates a JpegSegmentReader for a specific file.
* @param file the Jpeg file to read segments from
*/
@SuppressWarnings({ "ConstantConditions" })
public JpegSegmentReader(@NotNull File file) throws JpegProcessingException, IOException
{
if (file==null)
throw new NullPointerException();
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
_segmentData = readSegments(new BufferedInputStream(inputStream), false);
} finally {
if (inputStream != null)
inputStream.close();
}
}
/**
* Creates a JpegSegmentReader for a byte array.
* @param fileContents the byte array containing Jpeg data
*/
@SuppressWarnings({ "ConstantConditions" })
public JpegSegmentReader(@NotNull byte[] fileContents) throws JpegProcessingException
{
if (fileContents==null)
throw new NullPointerException();
BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents));
_segmentData = readSegments(stream, false);
}
/**
* Creates a JpegSegmentReader for an InputStream.
* @param inputStream the InputStream containing Jpeg data
*/
@SuppressWarnings({ "ConstantConditions" })
public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException
{
if (inputStream==null)
throw new NullPointerException();
BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
? (BufferedInputStream)inputStream
: new BufferedInputStream(inputStream);
_segmentData = readSegments(bufferedInputStream, waitForBytes);
}
/**
* Reads the first instance of a given Jpeg segment, returning the contents as
* a byte array.
* @param segmentMarker the byte identifier for the desired segment
* @return the byte array if found, else null
*/
@Nullable
public byte[] readSegment(byte segmentMarker)
{
return readSegment(segmentMarker, 0);
}
/**
* Reads the Nth instance of a given Jpeg segment, returning the contents as a byte array.
*
* @param segmentMarker the byte identifier for the desired segment
* @param occurrence the occurrence of the specified segment within the jpeg file
* @return the byte array if found, else null
*/
@Nullable
public byte[] readSegment(byte segmentMarker, int occurrence)
{
return _segmentData.getSegment(segmentMarker, occurrence);
}
/**
* Returns all instances of a given Jpeg segment. If no instances exist, an empty sequence is returned.
*
* @param segmentMarker a number which identifies the type of Jpeg segment being queried
* @return zero or more byte arrays, each holding the data of a Jpeg segment
*/
@NotNull
public Iterable readSegments(byte segmentMarker)
{
return _segmentData.getSegments(segmentMarker);
}
/**
* Returns the number of segments having the specified JPEG segment marker.
* @param segmentMarker the JPEG segment identifying marker.
* @return the count of matching segments.
*/
public final int getSegmentCount(byte segmentMarker)
{
return _segmentData.getSegmentCount(segmentMarker);
}
/**
* Returns the JpegSegmentData object used by this reader.
* @return the JpegSegmentData object.
*/
@NotNull
public final JpegSegmentData getSegmentData()
{
return _segmentData;
}
@NotNull
private JpegSegmentData readSegments(@NotNull final BufferedInputStream jpegInputStream, boolean waitForBytes) throws JpegProcessingException
{
JpegSegmentData segmentData = new JpegSegmentData();
try {
int offset = 0;
// first two bytes should be jpeg magic number
byte[] headerBytes = new byte[2];
if (jpegInputStream.read(headerBytes, 0, 2)!=2)
throw new JpegProcessingException("not a jpeg file");
final boolean hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8;
if (!hasValidHeader)
throw new JpegProcessingException("not a jpeg file");
offset += 2;
do {
// need four bytes from stream for segment header before continuing
if (!checkForBytesOnStream(jpegInputStream, 4, waitForBytes))
throw new JpegProcessingException("stream ended before segment header could be read");
// next byte is 0xFF
byte segmentIdentifier = (byte)(jpegInputStream.read() & 0xFF);
if ((segmentIdentifier & 0xFF) != 0xFF) {
throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF));
}
offset++;
// next byte is
byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF);
offset++;
// next 2-bytes are : [high-byte] [low-byte]
byte[] segmentLengthBytes = new byte[2];
if (jpegInputStream.read(segmentLengthBytes, 0, 2) != 2)
throw new JpegProcessingException("Jpeg data ended unexpectedly.");
offset += 2;
int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF);
// segment length includes size bytes, so subtract two
segmentLength -= 2;
if (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes))
throw new JpegProcessingException("segment size would extend beyond file stream length");
if (segmentLength < 0)
throw new JpegProcessingException("segment size would be less than zero");
byte[] segmentBytes = new byte[segmentLength];
if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength)
throw new JpegProcessingException("Jpeg data ended unexpectedly.");
offset += segmentLength;
if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) {
// The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
// have to search for the two bytes: 0xFF 0xD9 (EOI).
// It comes last so simply return at this point
return segmentData;
} else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) {
// the 'End-Of-Image' segment -- this should never be found in this fashion
return segmentData;
} else {
segmentData.addSegment(thisSegmentMarker, segmentBytes);
}
} while (true);
} catch (IOException ioe) {
throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
} finally {
try {
if (jpegInputStream != null) {
jpegInputStream.close();
}
} catch (IOException ioe) {
throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
}
}
}
private boolean checkForBytesOnStream(@NotNull BufferedInputStream stream, int bytesNeeded, boolean waitForBytes) throws IOException
{
// NOTE waiting is essential for network streams where data can be delayed, but it is not necessary for byte[] or filesystems
if (!waitForBytes)
return bytesNeeded <= stream.available();
int count = 40; // * 100ms = approx 4 seconds
while (count > 0) {
if (bytesNeeded <= stream.available())
return true;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// continue
}
count--;
}
return false;
}
}