Source code: com/drew/imaging/jpeg/JpegSegmentReader.java
1 /*
2 * JpegSegmentReader.java
3 *
4 * This class written by Drew Noakes, in accordance with the Jpeg specification.
5 *
6 * This is public domain software - that is, you can do whatever you want
7 * with it, and include it software that is licensed under the GNU or the
8 * BSD license, or whatever other licence you choose, including proprietary
9 * closed source licenses. I do ask that you leave this header in tact.
10 *
11 * If you make modifications to this code that you think would benefit the
12 * wider community, please send me a copy and I'll post it on my site.
13 *
14 * If you make use of this code, I'd appreciate hearing about it.
15 * drew@drewnoakes.com
16 * Latest version of this software kept at
17 * http://drewnoakes.com/
18 *
19 * Created by dnoakes on 04-Nov-2002 00:54:00 using IntelliJ IDEA
20 */
21 package com.drew.imaging.jpeg;
22
23 import java.io.*;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.ArrayList;
27
28 /**
29 * Performs read functions of Jpeg files, returning specific file segments.
30 * TODO add a findAvailableSegments() method
31 * TODO add more segment identifiers
32 * TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'
33 * @author Drew Noakes http://drewnoakes.com
34 */
35 public class JpegSegmentReader
36 {
37 /**
38 * Jpeg file.
39 */
40 private File _file;
41
42 /**
43 * Jpeg data as byte array.
44 */
45 private byte[] _data;
46
47 /**
48 * Jpeg data as an InputStream.
49 */
50 private InputStream _stream;
51
52 private HashMap _segmentDataMap;
53
54 /**
55 * Private, because this segment crashes my algorithm, and searching for
56 * it doesn't work (yet).
57 */
58 private static byte SEGMENT_SOS = (byte)0xDA;
59
60 /**
61 * Private, because one wouldn't search for it.
62 */
63 private static byte MARKER_EOI = (byte)0xD9;
64
65 /** APP0 Jpeg segment identifier -- Jfif data. */
66 public static final byte SEGMENT_APP0 = (byte)0xE0;
67 /** APP1 Jpeg segment identifier -- where Exif data is kept. */
68 public static final byte SEGMENT_APP1 = (byte)0xE1;
69 /** APP2 Jpeg segment identifier. */
70 public static final byte SEGMENT_APP2 = (byte)0xE2;
71 /** APP3 Jpeg segment identifier. */
72 public static final byte SEGMENT_APP3 = (byte)0xE3;
73 /** APP4 Jpeg segment identifier. */
74 public static final byte SEGMENT_APP4 = (byte)0xE4;
75 /** APP5 Jpeg segment identifier. */
76 public static final byte SEGMENT_APP5 = (byte)0xE5;
77 /** APP6 Jpeg segment identifier. */
78 public static final byte SEGMENT_APP6 = (byte)0xE6;
79 /** APP7 Jpeg segment identifier. */
80 public static final byte SEGMENT_APP7 = (byte)0xE7;
81 /** APP8 Jpeg segment identifier. */
82 public static final byte SEGMENT_APP8 = (byte)0xE8;
83 /** APP9 Jpeg segment identifier. */
84 public static final byte SEGMENT_APP9 = (byte)0xE9;
85 /** APPA Jpeg segment identifier. */
86 public static final byte SEGMENT_APPA = (byte)0xEA;
87 /** APPB Jpeg segment identifier. */
88 public static final byte SEGMENT_APPB = (byte)0xEB;
89 /** APPC Jpeg segment identifier. */
90 public static final byte SEGMENT_APPC = (byte)0xEC;
91 /** APPD Jpeg segment identifier -- IPTC data in here. */
92 public static final byte SEGMENT_APPD = (byte)0xED;
93 /** APPE Jpeg segment identifier. */
94 public static final byte SEGMENT_APPE = (byte)0xEE;
95 /** APPF Jpeg segment identifier. */
96 public static final byte SEGMENT_APPF = (byte)0xEF;
97
98 /** Start Of Image segment identifier. */
99 public static final byte SEGMENT_SOI = (byte)0xD8;
100 /** Define Quantization Table segment identifier. */
101 public static final byte SEGMENT_DQT = (byte)0xDB;
102 /** Define Huffman Table segment identifier. */
103 public static final byte SEGMENT_DHT = (byte)0xC4;
104 /** Start-of-Frame Zero segment identifier. */
105 public static final byte SEGMENT_SOF0 = (byte)0xC0;
106
107 /**
108 * Creates a JpegSegmentReader for a specific file.
109 * @param file the Jpeg file to read segments from
110 */
111 public JpegSegmentReader(File file) throws FileNotFoundException, JpegProcessingException
112 {
113 _file = file;
114 _data = null;
115 readSegments();
116 }
117
118 /**
119 * Creates a JpegSegmentReader for a byte array.
120 * @param fileContents the byte array containing Jpeg data
121 */
122 public JpegSegmentReader(byte[] fileContents) throws JpegProcessingException {
123 _file = null;
124 _data = fileContents;
125 readSegments();
126 }
127
128 public JpegSegmentReader(InputStream in) throws JpegProcessingException {
129 _stream = in;
130 _file = null;
131 _data = null;
132 readSegments();
133 }
134
135 /**
136 * Reads the first instance of a given Jpeg segment, returning the contents as
137 * a byte array.
138 * @param segmentMarker the byte identifier for the desired segment
139 * @return the byte array if found, else null
140 * @throws JpegProcessingException for any problems processing the Jpeg data,
141 * including inner IOExceptions
142 */
143 public byte[] readSegment(byte segmentMarker) throws JpegProcessingException
144 {
145 return readSegment(segmentMarker, 0);
146 }
147
148 /**
149 * Reads the first instance of a given Jpeg segment, returning the contents as
150 * a byte array.
151 * @param segmentMarker the byte identifier for the desired segment
152 * @param occurrence the occurrence of the specified segment within the jpeg file
153 * @return the byte array if found, else null
154 * @throws JpegProcessingException for any problems processing the Jpeg data,
155 * including inner IOExceptions
156 */
157 public byte[] readSegment(byte segmentMarker, int occurrence) throws JpegProcessingException
158 {
159 Byte key = new Byte(segmentMarker);
160 if (_segmentDataMap.containsKey(key)) {
161 List segmentList = (List)_segmentDataMap.get(key);
162 if (segmentList.size()<=occurrence) {
163 return null;
164 }
165 return (byte[]) segmentList.get(occurrence);
166 } else {
167 return null;
168 }
169 }
170
171 public int getSegmentCount(byte segmentMarker)
172 {
173 List segmentList = (List)_segmentDataMap.get(new Byte(segmentMarker));
174 if (segmentList==null) {
175 return 0;
176 }
177 return segmentList.size();
178 }
179
180 private void readSegments() throws JpegProcessingException
181 {
182 _segmentDataMap = new HashMap();
183 BufferedInputStream inStream = getJpegInputStream();
184 try {
185 int offset = 0;
186 // first two bytes should be jpeg magic number
187 if (!isValidJpegHeaderBytes(inStream)) {
188 throw new JpegProcessingException("not a jpeg file");
189 }
190 offset += 2;
191 do {
192 // next byte is 0xFF
193 byte segmentIdentifier = (byte)(inStream.read() & 0xFF);
194 if ((segmentIdentifier & 0xFF) != 0xFF) {
195 throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF));
196 }
197 offset++;
198 // next byte is <segment-marker>
199 byte thisSegmentMarker = (byte)(inStream.read() & 0xFF);
200 offset++;
201 // next 2-bytes are <segment-size>: [high-byte] [low-byte]
202 byte[] segmentLengthBytes = new byte[2];
203 inStream.read(segmentLengthBytes, 0, 2);
204 offset += 2;
205 int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF);
206 // segment length includes size bytes, so subtract two
207 segmentLength -= 2;
208 if (segmentLength > inStream.available()) {
209 throw new JpegProcessingException("segment size would extend beyond file stream length");
210 }
211 byte[] segmentBytes = new byte[segmentLength];
212 inStream.read(segmentBytes, 0, segmentLength);
213 offset += segmentLength;
214 if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) {
215 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
216 // have to search for the two bytes: 0xFF 0xD9 (EOI).
217 // It comes last so simply return at this point
218 return;
219 } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) {
220 // the 'End-Of-Image' segment -- this should never be found in this fashion
221 return;
222 } else {
223 List segmentList;
224 Byte key = new Byte(thisSegmentMarker);
225 if (_segmentDataMap.containsKey(key)) {
226 segmentList = (List)_segmentDataMap.get(key);
227 } else{
228 segmentList = new ArrayList();
229 _segmentDataMap.put(key, segmentList);
230 }
231 segmentList.add(segmentBytes);
232 }
233 // didn't find the one we're looking for, loop through to the next segment
234 } while (true);
235 } catch (IOException ioe) {
236 //throw new JpegProcessingException("IOException processing Jpeg file", ioe);
237 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
238 } finally {
239 try {
240 if (inStream != null) {
241 inStream.close();
242 }
243 } catch (IOException ioe) {
244 //throw new JpegProcessingException("IOException processing Jpeg file", ioe);
245 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
246 }
247 }
248 }
249
250 /**
251 * Private helper method to create a BufferedInputStream of Jpeg data from whichever
252 * data source was specified upon construction of this instance.
253 * @return a a BufferedInputStream of Jpeg data
254 * @throws JpegProcessingException for any problems obtaining the stream
255 */
256 private BufferedInputStream getJpegInputStream() throws JpegProcessingException
257 {
258 if (_stream!=null) {
259 if (_stream instanceof BufferedInputStream) {
260 return (BufferedInputStream) _stream;
261 } else {
262 return new BufferedInputStream(_stream);
263 }
264 }
265 InputStream inputStream;
266 if (_data == null) {
267 try {
268 inputStream = new FileInputStream(_file);
269 } catch (FileNotFoundException e) {
270 throw new JpegProcessingException("Jpeg file does not exist", e);
271 }
272 } else {
273 inputStream = new ByteArrayInputStream(_data);
274 }
275 return new BufferedInputStream(inputStream);
276 }
277
278 /**
279 * Helper method that validates the Jpeg file's magic number.
280 * @param fileStream the InputStream to read bytes from, which must be positioned
281 * at its start (i.e. no bytes read yet)
282 * @return true if the magic number is Jpeg (0xFFD8)
283 * @throws IOException for any problem in reading the file
284 */
285 private boolean isValidJpegHeaderBytes(InputStream fileStream) throws IOException
286 {
287 byte[] header = new byte[2];
288 fileStream.read(header, 0, 2);
289 return ((header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8);
290 }
291 }