Changeset 8132 in josm for trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
- Timestamp:
- 2015-03-10T01:17:39+01:00 (10 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging.jpeg; 22 22 23 import com.drew.lang.SequentialReader; 24 import com.drew.lang.StreamReader; 23 25 import com.drew.lang.annotations.NotNull; 24 26 import com.drew.lang.annotations.Nullable; 25 27 26 import java.io.*; 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.util.HashSet; 32 import java.util.Set; 27 33 28 34 /** 29 * Performs read functions of Jpeg files, returning specific file segments. 30 * @author Drew Noakes http://drewnoakes.com 35 * Performs read functions of JPEG files, returning specific file segments. 36 * <p> 37 * JPEG files are composed of a sequence of consecutive JPEG 'segments'. Each is identified by one of a set of byte 38 * values, modelled in the {@link JpegSegmentType} enumeration. Use <code>readSegments</code> to read out the some 39 * or all segments into a {@link JpegSegmentData} object, from which the raw JPEG segment byte arrays may be accessed. 40 * 41 * @author Drew Noakes https://drewnoakes.com 31 42 */ 32 43 public class JpegSegmentReader 33 44 { 34 // TODO add a findAvailableSegments() method35 // TODO add more segment identifiers36 // TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'37 38 @NotNull39 private final JpegSegmentData _segmentData;40 41 45 /** 42 46 * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet). 43 47 */ 44 private static final byte SEGMENT_SOS = (byte)0xDA; 48 private static final byte SEGMENT_SOS = (byte) 0xDA; 45 49 46 50 /** 47 51 * Private, because one wouldn't search for it. 48 52 */ 49 private static final byte MARKER_EOI = (byte)0xD9; 50 51 /** APP0 Jpeg segment identifier -- JFIF data (also JFXX apparently). */ 52 public static final byte SEGMENT_APP0 = (byte)0xE0; 53 /** APP1 Jpeg segment identifier -- where Exif data is kept. XMP data is also kept in here, though usually in a second instance. */ 54 public static final byte SEGMENT_APP1 = (byte)0xE1; 55 /** APP2 Jpeg segment identifier. */ 56 public static final byte SEGMENT_APP2 = (byte)0xE2; 57 /** APP3 Jpeg segment identifier. */ 58 public static final byte SEGMENT_APP3 = (byte)0xE3; 59 /** APP4 Jpeg segment identifier. */ 60 public static final byte SEGMENT_APP4 = (byte)0xE4; 61 /** APP5 Jpeg segment identifier. */ 62 public static final byte SEGMENT_APP5 = (byte)0xE5; 63 /** APP6 Jpeg segment identifier. */ 64 public static final byte SEGMENT_APP6 = (byte)0xE6; 65 /** APP7 Jpeg segment identifier. */ 66 public static final byte SEGMENT_APP7 = (byte)0xE7; 67 /** APP8 Jpeg segment identifier. */ 68 public static final byte SEGMENT_APP8 = (byte)0xE8; 69 /** APP9 Jpeg segment identifier. */ 70 public static final byte SEGMENT_APP9 = (byte)0xE9; 71 /** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */ 72 public static final byte SEGMENT_APPA = (byte)0xEA; 73 /** APPB (App11) Jpeg segment identifier. */ 74 public static final byte SEGMENT_APPB = (byte)0xEB; 75 /** APPC (App12) Jpeg segment identifier. */ 76 public static final byte SEGMENT_APPC = (byte)0xEC; 77 /** APPD (App13) Jpeg segment identifier -- IPTC data in here. */ 78 public static final byte SEGMENT_APPD = (byte)0xED; 79 /** APPE (App14) Jpeg segment identifier. */ 80 public static final byte SEGMENT_APPE = (byte)0xEE; 81 /** APPF (App15) Jpeg segment identifier. */ 82 public static final byte SEGMENT_APPF = (byte)0xEF; 83 /** Start Of Image segment identifier. */ 84 public static final byte SEGMENT_SOI = (byte)0xD8; 85 /** Define Quantization Table segment identifier. */ 86 public static final byte SEGMENT_DQT = (byte)0xDB; 87 /** Define Huffman Table segment identifier. */ 88 public static final byte SEGMENT_DHT = (byte)0xC4; 89 /** Start-of-Frame Zero segment identifier. */ 90 public static final byte SEGMENT_SOF0 = (byte)0xC0; 91 /** Jpeg comment segment identifier. */ 92 public static final byte SEGMENT_COM = (byte)0xFE; 53 private static final byte MARKER_EOI = (byte) 0xD9; 93 54 94 55 /** 95 * Creates a JpegSegmentReader for a specific file. 96 * @param file the Jpeg file to read segments from 56 * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object. 57 * <p> 58 * Will not return SOS (start of scan) or EOI (end of image) segments. 59 * 60 * @param file a {@link File} from which the JPEG data will be read. 61 * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code> 62 * then all found segment types are returned. 97 63 */ 98 @ SuppressWarnings({ "ConstantConditions" })99 public JpegSegment Reader(@NotNull File file) throws JpegProcessingException, IOException64 @NotNull 65 public static JpegSegmentData readSegments(@NotNull File file, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException 100 66 { 101 if (file==null) 102 throw new NullPointerException(); 103 104 InputStream inputStream = null; 67 FileInputStream stream = null; 105 68 try { 106 inputStream = new FileInputStream(file);107 _segmentData =readSegments(newBufferedInputStream(inputStream),false);69 stream = new FileInputStream(file); 70 return readSegments(new StreamReader(stream), segmentTypes); 108 71 } finally { 109 if (inputStream != null) 110 inputStream.close(); 72 if (stream != null) { 73 stream.close(); 74 } 111 75 } 112 76 } 113 77 114 78 /** 115 * Creates a JpegSegmentReader for a byte array. 116 * @param fileContents the byte array containing Jpeg data 79 * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object. 80 * <p> 81 * Will not return SOS (start of scan) or EOI (end of image) segments. 82 * 83 * @param reader a {@link SequentialReader} from which the JPEG data will be read. It must be positioned at the 84 * beginning of the JPEG data stream. 85 * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code> 86 * then all found segment types are returned. 117 87 */ 118 @ SuppressWarnings({ "ConstantConditions" })119 public JpegSegment Reader(@NotNullbyte[] fileContents) throws JpegProcessingException88 @NotNull 89 public static JpegSegmentData readSegments(@NotNull final SequentialReader reader, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException 120 90 { 121 if (fileContents==null)122 throw new NullPointerException();91 // Must be big-endian 92 assert (reader.isMotorolaByteOrder()); 123 93 124 BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents)); 125 _segmentData = readSegments(stream, false); 94 // first two bytes should be JPEG magic number 95 final int magicNumber = reader.getUInt16(); 96 if (magicNumber != 0xFFD8) { 97 throw new JpegProcessingException("JPEG data is expected to begin with 0xFFD8 (ÿØ) not 0x" + Integer.toHexString(magicNumber)); 98 } 99 100 Set<Byte> segmentTypeBytes = null; 101 if (segmentTypes != null) { 102 segmentTypeBytes = new HashSet<Byte>(); 103 for (JpegSegmentType segmentType : segmentTypes) { 104 segmentTypeBytes.add(segmentType.byteValue); 105 } 106 } 107 108 JpegSegmentData segmentData = new JpegSegmentData(); 109 110 do { 111 // Find the segment marker. Markers are zero or more 0xFF bytes, followed 112 // by a 0xFF and then a byte not equal to 0x00 or 0xFF. 113 114 final short segmentIdentifier = reader.getUInt8(); 115 116 // We must have at least one 0xFF byte 117 if (segmentIdentifier != 0xFF) 118 throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase()); 119 120 // Read until we have a non-0xFF byte. This identifies the segment type. 121 byte segmentType = reader.getInt8(); 122 while (segmentType == (byte)0xFF) 123 segmentType = reader.getInt8(); 124 125 if (segmentType == 0) 126 throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier"); 127 128 if (segmentType == SEGMENT_SOS) { 129 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would 130 // have to search for the two bytes: 0xFF 0xD9 (EOI). 131 // It comes last so simply return at this point 132 return segmentData; 133 } 134 135 if (segmentType == MARKER_EOI) { 136 // the 'End-Of-Image' segment -- this should never be found in this fashion 137 return segmentData; 138 } 139 140 // next 2-bytes are <segment-size>: [high-byte] [low-byte] 141 int segmentLength = reader.getUInt16(); 142 143 // segment length includes size bytes, so subtract two 144 segmentLength -= 2; 145 146 if (segmentLength < 0) 147 throw new JpegProcessingException("JPEG segment size would be less than zero"); 148 149 // Check whether we are interested in this segment 150 if (segmentTypeBytes == null || segmentTypeBytes.contains(segmentType)) { 151 byte[] segmentBytes = reader.getBytes(segmentLength); 152 assert (segmentLength == segmentBytes.length); 153 segmentData.addSegment(segmentType, segmentBytes); 154 } else { 155 // Some if the JPEG is truncated, just return what data we've already gathered 156 if (!reader.trySkip(segmentLength)) { 157 return segmentData; 158 } 159 } 160 161 } while (true); 126 162 } 127 163 128 /** 129 * Creates a JpegSegmentReader for an InputStream. 130 * @param inputStream the InputStream containing Jpeg data 131 */ 132 @SuppressWarnings({ "ConstantConditions" }) 133 public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException 164 private JpegSegmentReader() throws Exception 134 165 { 135 if (inputStream==null) 136 throw new NullPointerException(); 137 138 BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream 139 ? (BufferedInputStream)inputStream 140 : new BufferedInputStream(inputStream); 141 142 _segmentData = readSegments(bufferedInputStream, waitForBytes); 143 } 144 145 /** 146 * Reads the first instance of a given Jpeg segment, returning the contents as 147 * a byte array. 148 * @param segmentMarker the byte identifier for the desired segment 149 * @return the byte array if found, else null 150 */ 151 @Nullable 152 public byte[] readSegment(byte segmentMarker) 153 { 154 return readSegment(segmentMarker, 0); 155 } 156 157 /** 158 * Reads the Nth instance of a given Jpeg segment, returning the contents as a byte array. 159 * 160 * @param segmentMarker the byte identifier for the desired segment 161 * @param occurrence the occurrence of the specified segment within the jpeg file 162 * @return the byte array if found, else null 163 */ 164 @Nullable 165 public byte[] readSegment(byte segmentMarker, int occurrence) 166 { 167 return _segmentData.getSegment(segmentMarker, occurrence); 168 } 169 170 /** 171 * Returns all instances of a given Jpeg segment. If no instances exist, an empty sequence is returned. 172 * 173 * @param segmentMarker a number which identifies the type of Jpeg segment being queried 174 * @return zero or more byte arrays, each holding the data of a Jpeg segment 175 */ 176 @NotNull 177 public Iterable<byte[]> readSegments(byte segmentMarker) 178 { 179 return _segmentData.getSegments(segmentMarker); 180 } 181 182 /** 183 * Returns the number of segments having the specified JPEG segment marker. 184 * @param segmentMarker the JPEG segment identifying marker. 185 * @return the count of matching segments. 186 */ 187 public final int getSegmentCount(byte segmentMarker) 188 { 189 return _segmentData.getSegmentCount(segmentMarker); 190 } 191 192 /** 193 * Returns the JpegSegmentData object used by this reader. 194 * @return the JpegSegmentData object. 195 */ 196 @NotNull 197 public final JpegSegmentData getSegmentData() 198 { 199 return _segmentData; 200 } 201 202 @NotNull 203 private JpegSegmentData readSegments(@NotNull final BufferedInputStream jpegInputStream, boolean waitForBytes) throws JpegProcessingException 204 { 205 JpegSegmentData segmentData = new JpegSegmentData(); 206 207 try { 208 int offset = 0; 209 // first two bytes should be jpeg magic number 210 byte[] headerBytes = new byte[2]; 211 if (jpegInputStream.read(headerBytes, 0, 2)!=2) 212 throw new JpegProcessingException("not a jpeg file"); 213 final boolean hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8; 214 if (!hasValidHeader) 215 throw new JpegProcessingException("not a jpeg file"); 216 217 offset += 2; 218 do { 219 // need four bytes from stream for segment header before continuing 220 if (!checkForBytesOnStream(jpegInputStream, 4, waitForBytes)) 221 throw new JpegProcessingException("stream ended before segment header could be read"); 222 223 // next byte is 0xFF 224 byte segmentIdentifier = (byte)(jpegInputStream.read() & 0xFF); 225 if ((segmentIdentifier & 0xFF) != 0xFF) { 226 throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF)); 227 } 228 offset++; 229 // next byte is <segment-marker> 230 byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF); 231 offset++; 232 // next 2-bytes are <segment-size>: [high-byte] [low-byte] 233 byte[] segmentLengthBytes = new byte[2]; 234 if (jpegInputStream.read(segmentLengthBytes, 0, 2) != 2) 235 throw new JpegProcessingException("Jpeg data ended unexpectedly."); 236 offset += 2; 237 int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF); 238 // segment length includes size bytes, so subtract two 239 segmentLength -= 2; 240 if (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes)) 241 throw new JpegProcessingException("segment size would extend beyond file stream length"); 242 if (segmentLength < 0) 243 throw new JpegProcessingException("segment size would be less than zero"); 244 byte[] segmentBytes = new byte[segmentLength]; 245 if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength) 246 throw new JpegProcessingException("Jpeg data ended unexpectedly."); 247 offset += segmentLength; 248 if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) { 249 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would 250 // have to search for the two bytes: 0xFF 0xD9 (EOI). 251 // It comes last so simply return at this point 252 return segmentData; 253 } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) { 254 // the 'End-Of-Image' segment -- this should never be found in this fashion 255 return segmentData; 256 } else { 257 segmentData.addSegment(thisSegmentMarker, segmentBytes); 258 } 259 } while (true); 260 } catch (IOException ioe) { 261 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); 262 } finally { 263 try { 264 if (jpegInputStream != null) { 265 jpegInputStream.close(); 266 } 267 } catch (IOException ioe) { 268 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); 269 } 270 } 271 } 272 273 private boolean checkForBytesOnStream(@NotNull BufferedInputStream stream, int bytesNeeded, boolean waitForBytes) throws IOException 274 { 275 // NOTE waiting is essential for network streams where data can be delayed, but it is not necessary for byte[] or filesystems 276 277 if (!waitForBytes) 278 return bytesNeeded <= stream.available(); 279 280 int count = 40; // * 100ms = approx 4 seconds 281 while (count > 0) { 282 if (bytesNeeded <= stream.available()) 283 return true; 284 try { 285 Thread.sleep(100); 286 } catch (InterruptedException e) { 287 // continue 288 } 289 count--; 290 } 291 return false; 166 throw new Exception("Not intended for instantiation."); 292 167 } 293 168 }
Note:
See TracChangeset
for help on using the changeset viewer.