Source code: com/flexstor/flexdbserver/services/asset/iptc/PSDParser.java
1 /*
2 * PSDParser.java
3 *
4 * Copyright $Date: 2003/08/11 02:22:34 $ FLEXSTOR.net Inc.
5 *
6 * This work is licensed for use and distribution under license terms found at
7 * http://www.flexstor.org/license.html
8 *
9 */
10
11 package com.flexstor.flexdbserver.services.asset.iptc;
12
13 import java.io.IOException;
14 import java.util.Hashtable;
15
16 import com.flexstor.common.io.xfile.FlexXFile;
17 import com.flexstor.common.io.xfile.FlexXRandomAccessFile;
18
19 /** The PSDParser is used to parse header information for an Adobe Photoshop
20 * Document (PSD File).
21 *
22 * @author bparks
23 * @version initial
24 */
25 public class PSDParser extends IPTCParser implements IIPTCParser {
26
27 /** The file we will parse.
28 * It is a RandomAccessFile so we can skip unimportant parts of the file.
29 */
30 private FlexXRandomAccessFile fPSDFile;
31
32 /** The "signature" of a PSD file is always equal to "8BPS"
33 * The format of a Photoshop file is described in a document titled
34 * "Photoshop File Formats.pdf" and is available on the Adobe site.
35 */
36 private String sSignature = null;
37
38 /** The number of channels in the image, including any alpha channels.
39 * Supported range is 1 to 24.
40 */
41 private int nChannels = -1;
42
43 /** The height of the image in pixels. Supported range is 1 to 30,000.
44 */
45 private int nRows = -1;
46
47 /** The width of the image in pixels. Supported range is 1 to 30,000.
48 */
49 private int nColumns = -1;
50
51 /** The number of bits per channel. Supported values are 1, 8, and 16.
52 */
53 private int nBitsPerChannel = -1;
54
55 /** The color mode of the file. Supported values are: Bitmap=0; Grayscale=1;
56 * Indexed=2; RGB=3; CMYK=4; Multichannel=7; Duotone=8; Lab=9.
57 */
58 private int nColorMode = -1;
59
60 /** The length of the color data. Only indexed color and duotone have color
61 * mode data. For all other modes, this will be just four bytes of 0.
62 */
63 private int nColorModeDataLength = -1;
64
65 /* the color data will not be read or stored in a variable */
66
67 /** nImageResourcesLength is the total length of all Image Resource Blocks.
68 */
69 private int nImageResourcesLength = -1;
70
71 /** This is an array of bytes which consists of the entire Image Resource
72 * Section (i.e. several individual Image Resource Blocks)
73 */
74 private byte abImageResourceSection[] = null;
75
76 /** I think the Image Resource Block data would go nicely in a Hashtable because there
77 * may be more than one Image Resource Block in a file and we would really only care
78 * about the 2-byte ID and the N-byte Resource Data (where N is always an even number)
79 */
80 private Hashtable hImageResourceBlockHash = new Hashtable();
81
82
83
84 /** Creates new PSDParser from the File parameter passed in.
85 * @param fInputFile The File to create the PSDParser from.
86 * Once the PSDParser object is instantiated, the PSDParser class
87 * will be done with the fInputFile so it can be nulled immediately
88 * after instantiation.
89 */
90 public PSDParser (FlexXFile fInputFile) {
91 if ( (fInputFile.exists () == false) || (fInputFile.isFile () == false) ) {
92 fPSDFile = null;
93 } else {
94 /* create the RandomAccessFile for the given input file */
95 try {
96 fPSDFile = new FlexXRandomAccessFile ( fInputFile, "r" );
97 } catch ( IOException ioe ) {
98 BGPError ( "IO Exception attempting to create the RAF for file");
99 }
100 /* object variables will not be initialized to anything different */
101 }
102 }
103
104
105 /** Parse a PSD (Photoshop) file. In order to return true, the file must have
106 * a valid PSD file from which we can retrieve IPTC-NAA info. This may or may
107 * not parse the IPTC-NAA data as well. (Right now I'm leaning towards doing it.)
108 *
109 * @return Returns a boolean value indicating success.
110 */
111 public boolean parseFile() {
112
113 // The superclass's no-arg constructor is called by default.
114
115 /** nReadStatus will be used as a temporary variable to make sure
116 * a .read() was successful.
117 */
118 int nReadStatus = -1;
119 byte abSanityBuffer[] = new byte[12];
120
121 // Let's try to read a few bytes of our RandomAccessFile
122 // As a sanity check, we'll check the beginning of the file.
123 // The beginning of a Photoshop file should start with bytes "8BPS 01 000000"
124 try {
125 if( fPSDFile == null )
126 {
127 System.out.println( "What is the sound of a null file opening?" );
128 return false;
129 }
130 nReadStatus = fPSDFile.read ( abSanityBuffer,0,12 );
131 // If it's a -1, we had a problem read'ing and will be unable to parse.
132 if ( nReadStatus == -1 ) {
133 BGPError ( "Unable to read the first 12 bytes of the file" );
134 closeFile();
135 return false;
136 } else {
137 // Verify that the "signature" (the first four bytes) is "8BPS"
138 sSignature = new String ( abSanityBuffer, 0, 4 );
139
140 // Note that the signature should be verified by using the typer only thus allowing
141 // more flexibility with respect to file type versions in this code. Thus we will
142 // bypass the signature check
143 if (true)
144 //if ( sSignature.equals ("8BPS") ) {
145 {
146
147 //BGPDebug ( "Photoshop signature is correct" );
148
149 // Verify the "version" (the next two bytes) are 0 and 1 as specified in
150 // the Photoshop File Formats document.
151
152 // Once again, bypass signature check
153 if (true)
154 //if ( (abSanityBuffer[4] == 0) && (abSanityBuffer[5] == 1) ) {
155 {
156
157 //BGPDebug ( "Photoshop version is correct" );
158
159 // Verify that the "reserved" area (the next six bytes) are each 0
160 // as specified in the Photoshop File Formats document.
161
162 // Bypass signature check
163 //for ( int i=6; i<12; i++ ) {
164 // if ( abSanityBuffer[i] != 0) {
165 // BGPError ( "'Reserved' area is not valid for Photoshop file" );
166 // return false;
167 // }
168 //}
169
170 /* We know we have a valid Photoshop file. Parse the Rest of the Header */
171 byte abRestOfHeader[] = new byte[18];
172 nReadStatus = fPSDFile.read(abRestOfHeader);
173 if ( nReadStatus == -1 ) {
174 BGPError ( "Unable to read the next 18 bytes of the file" );
175 closeFile();
176 return false;
177 } else {
178
179 // Compute the nChannels from bytes 0 and 1
180 nChannels = hexBytesToInt( abRestOfHeader, 0, 2 );
181 // BGPDebug ( "nChannels is " + nChannels );
182
183 // Compute the nRows from bytes 2,3,4,5
184 nRows = hexBytesToInt( abRestOfHeader, 2, 4 );
185 //BGPDebug ( "nRows is " + nRows );
186
187 // Compute the nColumns from bytes 6,7,8,9
188 nColumns = hexBytesToInt( abRestOfHeader, 6, 4 );
189 //BGPDebug ( "nColumns is " + nColumns );
190
191 // Compute the nBitsPerChannel from bytes 10,11
192 nBitsPerChannel = hexBytesToInt( abRestOfHeader, 10, 2 );
193 //BGPDebug ( "nBitsPerChannel is " + nBitsPerChannel );
194
195 // Compute the nColorMode from bytes 12,13
196 nColorMode = hexBytesToInt( abRestOfHeader, 12, 2 );
197 //BGPDebug ( "nColorMode is " + nColorMode );
198
199 // Compute the nColorModeDataLength from bytes 14,15,16,17
200 nColorModeDataLength = hexBytesToInt( abRestOfHeader, 14, 4 );
201 // Only Color Modes 2 (indexed) and 8 (duotone) will have a length.
202 //BGPDebug ( "nColorModeDataLength is " + nColorModeDataLength );
203
204 }
205 // When done with a variable, null'ing it allows for garbase collection
206 abRestOfHeader = null;
207
208 // So far in the file, we have dealt with fixed positions.
209 // Now we will getting into data that could be at various positions.
210 // We left off at byte 30, we need to skip over the Color Mode data
211 fPSDFile.skipBytes( nColorModeDataLength );
212
213 // obtain the length of the Image resources section.
214 // It is important to know how much Image Resource data to read.
215 byte abImageResourceSectionLength[] = new byte[4];
216 nReadStatus = fPSDFile.read( abImageResourceSectionLength );
217 if ( nReadStatus == -1 ) {
218 BGPError ( "Unable to read the Image Resource length" );
219 closeFile();
220 return false;
221 } else {
222 // Compute the nImageResourceLength from bytes 0,1,2,3
223 nImageResourcesLength = hexBytesToInt( abImageResourceSectionLength,0,4 );
224 //BGPDebug ( "nImageResourceLength is " + nImageResourcesLength );
225 abImageResourceSectionLength = null;
226 }
227
228 // read in all of the Image Resources Data
229 abImageResourceSection = new byte[nImageResourcesLength];
230 nReadStatus = fPSDFile.read( abImageResourceSection );
231 if ( nReadStatus == -1 ) {
232 BGPError ( "Unable to read the Image Resources data" );
233 closeFile();
234 return false;
235 } else {
236 // If we have obtained more than zero bytes, parse the Image Resource
237 // Data into blocks using another method.
238 if ( nReadStatus > 0 ) {
239
240 this.parseImageResourceBlocks();
241
242 // finally, attempt to parse the IPTC-NAA record if there is one.
243 byte [] abIPTCNAArecord = getIPTCNAArecord();
244 if ( abIPTCNAArecord != null ) {
245 // Yes, we have IPTC-NAA data
246 parseIPTCdata(abIPTCNAArecord);
247
248 // This next loop is for debugging:
249 /*String sTempID = null;
250 String sTempValue = null;
251 for( Enumeration e = hDataSets.keys(); e.hasMoreElements();) {
252 //sTempID = new String( (String) e.nextElement() );
253 //sTempValue = new String( (String) hDataSets.get( sTempID ) );
254 sTempID = (String) e.nextElement();
255 sTempValue = (String) hDataSets.get( sTempID );
256 BGPDebug( sTempID + " is [" + sTempValue + "]" );
257 }*/
258
259 } else {
260 BGPDebug( "There is no IPTC-NAA record in this file" );
261 }
262 }
263 }
264
265 } else {
266 // If signature doesn't match, then we don't have a valid Photoshop file.
267 BGPError ( "The version is not valid for a Photoshop file" );
268 closeFile();
269 return false;
270 }
271 } else {
272 // If the signature doesn't match, then we do not have a valid Photoshop file.
273 BGPError ( "The signature is not valid for a Photoshop file" );
274 closeFile();
275 return false;
276 }
277 }
278 } catch ( IOException ioe ) {
279 BGPError ( "IO Exception attempting to read the file" );
280 // We had a problem reading the file and will be unable to parse.
281 closeFile();
282 return false;
283 }
284
285 closeFile();
286 return true;
287 } // end of parseFile method
288
289
290 /**
291 *
292 *
293 */
294 protected void closeFile()
295 {
296 if (fPSDFile == null)
297 {
298 return;
299 }
300
301 try
302 {
303 fPSDFile.close();
304 }
305
306 catch ( IOException ioe )
307 {
308 BGPError ( "Error closing file: " + ioe.toString());
309 }
310 } // closeFile
311
312
313 /** This method parses the entire Image Resource Section byte array into separate
314 * Image Resource Blocks and places them in the hIRBhashtable.
315 * @return Either true or false based on whether the parsing was successful.
316 */
317 private boolean parseImageResourceBlocks () {
318 int nArrayIndex = 0;
319 String sOSType = null;
320
321 int nUniqueID = -1;
322 int nResourceBlockSize = -1;
323 byte abResourceBlockData[] = null;
324
325 while (nArrayIndex < abImageResourceSection.length) {
326 // Verify that the "OSType" signature is correct before attempting to
327 // read the Image Resource Block.
328 sOSType = new String( abImageResourceSection, nArrayIndex, 4 );
329 // Increment the array index counter after reading bytes from the array
330 nArrayIndex += 4;
331
332 if ( sOSType.equals ("8BIM") ) {
333 // The next two bytes are the unique identifier
334 nUniqueID = hexBytesToInt( abImageResourceSection, nArrayIndex, 2 );
335 nArrayIndex += 2;
336 //BGPDebug ( "The unique identifier is : " + nUniqueID );
337
338 // skip the next two bytes "PString" which consist of two bytes of 0
339 nArrayIndex += 2;
340
341 // The next four bytes are the size of this Image Resource Block
342 nResourceBlockSize = hexBytesToInt( abImageResourceSection, nArrayIndex, 4 );
343 nArrayIndex += 4;
344
345 //BGPDebug ( "The resource block size is : " + nResourceBlockSize );
346
347 // copy the relevant part of the whole Image Resource array into a
348 // smaller array that is just this one Image Resource Block Data.
349 abResourceBlockData = new byte[nResourceBlockSize];
350 System.arraycopy( abImageResourceSection, nArrayIndex,
351 abResourceBlockData, 0, nResourceBlockSize );
352
353 // Store the nUniqueID and abResourceBlockData in the object's
354 // hImageResourceBlockHash hashtable.
355 hImageResourceBlockHash.put( new Integer(nUniqueID), abResourceBlockData );
356
357 // According to page 8 of the Photoshop File Formats spec document,
358 // the Image Resource Block data is "padded to make size even"
359 // so check if the size is even and add 1 if it is not. We do this
360 // after the array copy because the "padding" is not part of the data.
361 // We do this increase only to line up for the next Image Resource Block.
362 if ((nResourceBlockSize % 2) != 0) {
363 nResourceBlockSize++;
364 }
365
366 // Increment the array index now that we have read the Image Resource Data.
367 nArrayIndex += nResourceBlockSize;
368
369 } else {
370 // Exit this method as soon as we hit bad data. Due to the nature of the
371 // Image Resource Block data, it would be difficult or impossible to get
372 // back on track with a proper offset if we fall off the tracks.
373 BGPError ( "The Image Resource OSType Signature is invalid: [" + sOSType + "]");
374 return false;
375 }
376
377 } // end of while loop
378
379 // If we have not had any problems with parsing the whole Image Resource Section
380 // into the various Image Resource Blocks, everything must be okay.
381 return true;
382
383 } // end of parseImageResourceBlocks method
384
385
386
387 /** Obtain the number of channels in the image.
388 * @return The number of channels in the image, including alpha channels.
389 * Should be in the range 1 to 24.
390 */
391 public int getNumberOfChannels () {
392 return nChannels;
393 } // end of getNumberOfChannels
394
395
396 /** Obtain the height of the image.
397 * @return The height of the image in pixels.
398 * Should be in the range 1 to 30,000.
399 */
400 public int getHeight () {
401 return nRows;
402 } // end of getHeight
403
404
405 /** Obtain the width of the image.
406 * @return The width of the image in pixels.
407 * Should be in the range 1 to 30,000.
408 */
409 public int getWidth () {
410 return nColumns;
411 } // end of getWidth
412
413
414 /** Obtain the number of bits per channel.
415 * Should be either 1, 8, or 16.
416 * @return The number of bits per channel.
417 */
418 public int getBitsPerChannel () {
419 return nBitsPerChannel;
420 } // end of getBitsPerChannel
421
422 /** Obtain the color mode of the file.
423 * <PRE>Value should be one of the following:
424 * 0 is Bitmap
425 * 1 is Grayscale
426 * 2 is Indexed
427 * 3 is RGB
428 * 4 is CMYK
429 * 7 is Multichannel
430 * 8 is Duotone
431 * 9 is Lab</PRE>
432 * @return The color mode of the file.
433 */
434 public int getColorMode () {
435 return nColorMode;
436 } // end of getColorMode
437
438
439 /** Obtain a specific Image Resource Data Block of the file.
440 * @return An Image Resource Block as an array of bytes.
441 * If the specified Resource ID does not exist, null will be returned.
442 * @param nResourceID The Image Resource ID for a specific Image Resource Block.
443 */
444 public byte[] getImageResource (int nResourceID) {
445 return (byte[]) hImageResourceBlockHash.get( (new Integer(nResourceID)) );
446 } // end of getImageResource
447
448
449 /** Obtain the IPTC-NAA Record of the file.
450 * @return The IPTC-NAA Record Image Resource Block as an array of bytes.
451 * If it does not exist, null will be returned.
452 */
453 public byte[] getIPTCNAArecord () {
454 return getImageResource( 0x0404 );
455 } // end of getIPTCNAArecord
456
457
458 /** Obtain the value for the specified Dataset number.
459 * @return the Dataset number value.
460 * @param sDatasetID The IPTC Dataset ID number (A:BB)
461 */
462 public String getDatasetValue( String sDatasetID ) {
463 return (String) hDataSets.get( sDatasetID );
464 } // end of getDataset
465
466
467
468 } // end of PSDParser class