1<?php
2
3/////////////////////////////////////////////////////////////////
4/// getID3() by James Heinrich <info@getid3.org>               //
5//  available at https://github.com/JamesHeinrich/getID3       //
6//            or https://www.getid3.org                        //
7//            or http://getid3.sourceforge.net                 //
8//  see readme.txt for more details                            //
9/////////////////////////////////////////////////////////////////
10//                                                             //
11// module.tag.xmp.php                                          //
12// module for analyzing XMP metadata (e.g. in JPEG files)      //
13// dependencies: NONE                                          //
14//                                                             //
15/////////////////////////////////////////////////////////////////
16//                                                             //
17// Module originally written [2009-Mar-26] by                  //
18//      Nigel Barnes <ngbarnesØhotmail*com>                    //
19// Bundled into getID3 with permission                         //
20//   called by getID3 in module.graphic.jpg.php                //
21//                                                            ///
22/////////////////////////////////////////////////////////////////
23
24/**************************************************************************************************
25 * SWISScenter Source                                                              Nigel Barnes
26 *
27 * 	Provides functions for reading information from the 'APP1' Extensible Metadata
28 *	Platform (XMP) segment of JPEG format files.
29 *	This XMP segment is XML based and contains the Resource Description Framework (RDF)
30 *	data, which itself can contain the Dublin Core Metadata Initiative (DCMI) information.
31 *
32 * 	This code uses segments from the JPEG Metadata Toolkit project by Evan Hunter.
33 *************************************************************************************************/
34class Image_XMP
35{
36	/**
37	* @var string
38	* The name of the image file that contains the XMP fields to extract and modify.
39	* @see Image_XMP()
40	*/
41	public $_sFilename = null;
42
43	/**
44	* @var array
45	* The XMP fields that were extracted from the image or updated by this class.
46	* @see getAllTags()
47	*/
48	public $_aXMP = array();
49
50	/**
51	* @var boolean
52	* True if an APP1 segment was found to contain XMP metadata.
53	* @see isValid()
54	*/
55	public $_bXMPParse = false;
56
57	/**
58	* Returns the status of XMP parsing during instantiation
59	*
60	* You'll normally want to call this method before trying to get XMP fields.
61	*
62	* @return boolean
63	* Returns true if an APP1 segment was found to contain XMP metadata.
64	*/
65	public function isValid()
66	{
67		return $this->_bXMPParse;
68	}
69
70	/**
71	* Get a copy of all XMP tags extracted from the image
72	*
73	* @return array - An array of XMP fields as it extracted by the XMPparse() function
74	*/
75	public function getAllTags()
76	{
77		return $this->_aXMP;
78	}
79
80	/**
81	* Reads all the JPEG header segments from an JPEG image file into an array
82	*
83	* @param string $filename - the filename of the JPEG file to read
84	* @return array|boolean  $headerdata - Array of JPEG header segments,
85	*                        FALSE - if headers could not be read
86	*/
87	public function _get_jpeg_header_data($filename)
88	{
89		// prevent refresh from aborting file operations and hosing file
90		ignore_user_abort(true);
91
92		// Attempt to open the jpeg file - the at symbol supresses the error message about
93		// not being able to open files. The file_exists would have been used, but it
94		// does not work with files fetched over http or ftp.
95		if (is_readable($filename) && is_file($filename) && ($filehnd = fopen($filename, 'rb'))) {
96			// great
97		} else {
98			return false;
99		}
100
101		// Read the first two characters
102		$data = fread($filehnd, 2);
103
104		// Check that the first two characters are 0xFF 0xD8  (SOI - Start of image)
105		if ($data != "\xFF\xD8")
106		{
107			// No SOI (FF D8) at start of file - This probably isn't a JPEG file - close file and return;
108			echo '<p>This probably is not a JPEG file</p>'."\n";
109			fclose($filehnd);
110			return false;
111		}
112
113		// Read the third character
114		$data = fread($filehnd, 2);
115
116		// Check that the third character is 0xFF (Start of first segment header)
117		if ($data[0] != "\xFF")
118		{
119			// NO FF found - close file and return - JPEG is probably corrupted
120			fclose($filehnd);
121			return false;
122		}
123
124		// Flag that we havent yet hit the compressed image data
125		$hit_compressed_image_data = false;
126
127		$headerdata = array();
128		// Cycle through the file until, one of: 1) an EOI (End of image) marker is hit,
129		//                                       2) we have hit the compressed image data (no more headers are allowed after data)
130		//                                       3) or end of file is hit
131
132		while (($data[1] != "\xD9") && (!$hit_compressed_image_data) && (!feof($filehnd)))
133		{
134			// Found a segment to look at.
135			// Check that the segment marker is not a Restart marker - restart markers don't have size or data after them
136			if ((ord($data[1]) < 0xD0) || (ord($data[1]) > 0xD7))
137			{
138				// Segment isn't a Restart marker
139				// Read the next two bytes (size)
140				$sizestr = fread($filehnd, 2);
141
142				// convert the size bytes to an integer
143				$decodedsize = unpack('nsize', $sizestr);
144
145				// Save the start position of the data
146				$segdatastart = ftell($filehnd);
147
148				// Read the segment data with length indicated by the previously read size
149				$segdata = fread($filehnd, $decodedsize['size'] - 2);
150
151				// Store the segment information in the output array
152				$headerdata[] = array(
153					'SegType'      => ord($data[1]),
154					'SegName'      => $GLOBALS['JPEG_Segment_Names'][ord($data[1])],
155					'SegDataStart' => $segdatastart,
156					'SegData'      => $segdata,
157				);
158			}
159
160			// If this is a SOS (Start Of Scan) segment, then there is no more header data - the compressed image data follows
161			if ($data[1] == "\xDA")
162			{
163				// Flag that we have hit the compressed image data - exit loop as no more headers available.
164				$hit_compressed_image_data = true;
165			}
166			else
167			{
168				// Not an SOS - Read the next two bytes - should be the segment marker for the next segment
169				$data = fread($filehnd, 2);
170
171				// Check that the first byte of the two is 0xFF as it should be for a marker
172				if ($data[0] != "\xFF")
173				{
174					// NO FF found - close file and return - JPEG is probably corrupted
175					fclose($filehnd);
176					return false;
177				}
178			}
179		}
180
181		// Close File
182		fclose($filehnd);
183		// Alow the user to abort from now on
184		ignore_user_abort(false);
185
186		// Return the header data retrieved
187		return $headerdata;
188	}
189
190
191	/**
192	* Retrieves XMP information from an APP1 JPEG segment and returns the raw XML text as a string.
193	*
194	* @param string $filename - the filename of the JPEG file to read
195	* @return string|boolean $xmp_data - the string of raw XML text,
196	*                        FALSE - if an APP 1 XMP segment could not be found, or if an error occured
197	*/
198	public function _get_XMP_text($filename)
199	{
200		//Get JPEG header data
201		$jpeg_header_data = $this->_get_jpeg_header_data($filename);
202
203		//Cycle through the header segments
204		for ($i = 0; $i < count($jpeg_header_data); $i++)
205		{
206			// If we find an APP1 header,
207			if (strcmp($jpeg_header_data[$i]['SegName'], 'APP1') == 0)
208			{
209				// And if it has the Adobe XMP/RDF label (http://ns.adobe.com/xap/1.0/\x00) ,
210				if (strncmp($jpeg_header_data[$i]['SegData'], 'http://ns.adobe.com/xap/1.0/'."\x00", 29) == 0)
211				{
212					// Found a XMP/RDF block
213					// Return the XMP text
214					$xmp_data = substr($jpeg_header_data[$i]['SegData'], 29);
215
216					return trim($xmp_data); // trim() should not be neccesary, but some files found in the wild with null-terminated block (known samples from Apple Aperture) causes problems elsewhere (see https://www.getid3.org/phpBB3/viewtopic.php?f=4&t=1153)
217				}
218			}
219		}
220		return false;
221	}
222
223	/**
224	* Parses a string containing XMP data (XML), and returns an array
225	* which contains all the XMP (XML) information.
226	*
227	* @param string $xmltext - a string containing the XMP data (XML) to be parsed
228	* @return array|boolean $xmp_array - an array containing all xmp details retrieved,
229	*                       FALSE - couldn't parse the XMP data.
230	*/
231	public function read_XMP_array_from_text($xmltext)
232	{
233		// Check if there actually is any text to parse
234		if (trim($xmltext) == '')
235		{
236			return false;
237		}
238
239		// Create an instance of a xml parser to parse the XML text
240		$xml_parser = xml_parser_create('UTF-8');
241
242		// Change: Fixed problem that caused the whitespace (especially newlines) to be destroyed when converting xml text to an xml array, as of revision 1.10
243
244		// We would like to remove unneccessary white space, but this will also
245		// remove things like newlines (&#xA;) in the XML values, so white space
246		// will have to be removed later
247		if (xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, 0) == false)
248		{
249			// Error setting case folding - destroy the parser and return
250			xml_parser_free($xml_parser);
251			return false;
252		}
253
254		// to use XML code correctly we have to turn case folding
255		// (uppercasing) off. XML is case sensitive and upper
256		// casing is in reality XML standards violation
257		if (xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0) == false)
258		{
259			// Error setting case folding - destroy the parser and return
260			xml_parser_free($xml_parser);
261			return false;
262		}
263
264		// Parse the XML text into a array structure
265		if (xml_parse_into_struct($xml_parser, $xmltext, $values, $tags) == 0)
266		{
267			// Error Parsing XML - destroy the parser and return
268			xml_parser_free($xml_parser);
269			return false;
270		}
271
272		// Destroy the xml parser
273		xml_parser_free($xml_parser);
274
275		// Clear the output array
276		$xmp_array = array();
277
278		// The XMP data has now been parsed into an array ...
279
280		// Cycle through each of the array elements
281		$current_property = ''; // current property being processed
282		$container_index = -1; // -1 = no container open, otherwise index of container content
283		foreach ($values as $xml_elem)
284		{
285			// Syntax and Class names
286			switch ($xml_elem['tag'])
287			{
288				case 'x:xmpmeta':
289					// only defined attribute is x:xmptk written by Adobe XMP Toolkit; value is the version of the toolkit
290					break;
291
292				case 'rdf:RDF':
293					// required element immediately within x:xmpmeta; no data here
294					break;
295
296				case 'rdf:Description':
297					switch ($xml_elem['type'])
298					{
299						case 'open':
300						case 'complete':
301							if (array_key_exists('attributes', $xml_elem))
302							{
303								// rdf:Description may contain wanted attributes
304								foreach (array_keys($xml_elem['attributes']) as $key)
305								{
306									// Check whether we want this details from this attribute
307//									if (in_array($key, $GLOBALS['XMP_tag_captions']))
308//									if (true)
309//									{
310										// Attribute wanted
311										$xmp_array[$key] = $xml_elem['attributes'][$key];
312//									}
313								}
314							}
315							break;
316						case 'cdata':
317						case 'close':
318							break;
319					}
320					break;
321
322				case 'rdf:ID':
323				case 'rdf:nodeID':
324					// Attributes are ignored
325					break;
326
327				case 'rdf:li':
328					// Property member
329					if ($xml_elem['type'] == 'complete')
330					{
331						if (array_key_exists('attributes', $xml_elem))
332						{
333							// If Lang Alt (language alternatives) then ensure we take the default language
334							if (isset($xml_elem['attributes']['xml:lang']) && ($xml_elem['attributes']['xml:lang'] != 'x-default'))
335							{
336								break;
337							}
338						}
339						if ($current_property != '')
340						{
341							$xmp_array[$current_property][$container_index] = (isset($xml_elem['value']) ? $xml_elem['value'] : '');
342							$container_index += 1;
343						}
344					//else unidentified attribute!!
345					}
346					break;
347
348				case 'rdf:Seq':
349				case 'rdf:Bag':
350				case 'rdf:Alt':
351					// Container found
352					switch ($xml_elem['type'])
353					{
354						case 'open':
355 							$container_index = 0;
356 							break;
357						case 'close':
358							$container_index = -1;
359							break;
360						case 'cdata':
361							break;
362					}
363					break;
364
365				default:
366					// Check whether we want the details from this attribute
367//					if (in_array($xml_elem['tag'], $GLOBALS['XMP_tag_captions']))
368//					if (true)
369//					{
370						switch ($xml_elem['type'])
371						{
372							case 'open':
373								// open current element
374								$current_property = $xml_elem['tag'];
375								break;
376
377							case 'close':
378								// close current element
379								$current_property = '';
380								break;
381
382							case 'complete':
383								// store attribute value
384								$xmp_array[$xml_elem['tag']] = (isset($xml_elem['attributes']) ? $xml_elem['attributes'] : (isset($xml_elem['value']) ? $xml_elem['value'] : ''));
385								break;
386
387							case 'cdata':
388								// ignore
389								break;
390						}
391//					}
392					break;
393			}
394
395		}
396		return $xmp_array;
397	}
398
399
400	/**
401	* Constructor
402	*
403	* @param string $sFilename - Name of the image file to access and extract XMP information from.
404	*/
405	public function __construct($sFilename)
406	{
407		$this->_sFilename = $sFilename;
408
409		if (is_file($this->_sFilename))
410		{
411			// Get XMP data
412			$xmp_data = $this->_get_XMP_text($sFilename);
413			if ($xmp_data)
414			{
415				$aXMP = $this->read_XMP_array_from_text($xmp_data);
416				if ($aXMP !== false) {
417					$this->_aXMP = (array) $aXMP;
418					$this->_bXMPParse = true;
419				}
420			}
421		}
422	}
423
424}
425
426/**
427* Global Variable: XMP_tag_captions
428*
429* The Property names of all known XMP fields.
430* Note: this is a full list with unrequired properties commented out.
431*/
432/*
433$GLOBALS['XMP_tag_captions'] = array(
434// IPTC Core
435	'Iptc4xmpCore:CiAdrCity',
436	'Iptc4xmpCore:CiAdrCtry',
437	'Iptc4xmpCore:CiAdrExtadr',
438	'Iptc4xmpCore:CiAdrPcode',
439	'Iptc4xmpCore:CiAdrRegion',
440	'Iptc4xmpCore:CiEmailWork',
441	'Iptc4xmpCore:CiTelWork',
442	'Iptc4xmpCore:CiUrlWork',
443	'Iptc4xmpCore:CountryCode',
444	'Iptc4xmpCore:CreatorContactInfo',
445	'Iptc4xmpCore:IntellectualGenre',
446	'Iptc4xmpCore:Location',
447	'Iptc4xmpCore:Scene',
448	'Iptc4xmpCore:SubjectCode',
449// Dublin Core Schema
450	'dc:contributor',
451	'dc:coverage',
452	'dc:creator',
453	'dc:date',
454	'dc:description',
455	'dc:format',
456	'dc:identifier',
457	'dc:language',
458	'dc:publisher',
459	'dc:relation',
460	'dc:rights',
461	'dc:source',
462	'dc:subject',
463	'dc:title',
464	'dc:type',
465// XMP Basic Schema
466	'xmp:Advisory',
467	'xmp:BaseURL',
468	'xmp:CreateDate',
469	'xmp:CreatorTool',
470	'xmp:Identifier',
471	'xmp:Label',
472	'xmp:MetadataDate',
473	'xmp:ModifyDate',
474	'xmp:Nickname',
475	'xmp:Rating',
476	'xmp:Thumbnails',
477	'xmpidq:Scheme',
478// XMP Rights Management Schema
479	'xmpRights:Certificate',
480	'xmpRights:Marked',
481	'xmpRights:Owner',
482	'xmpRights:UsageTerms',
483	'xmpRights:WebStatement',
484// These are not in spec but Photoshop CS seems to use them
485	'xap:Advisory',
486	'xap:BaseURL',
487	'xap:CreateDate',
488	'xap:CreatorTool',
489	'xap:Identifier',
490	'xap:MetadataDate',
491	'xap:ModifyDate',
492	'xap:Nickname',
493	'xap:Rating',
494	'xap:Thumbnails',
495	'xapidq:Scheme',
496	'xapRights:Certificate',
497	'xapRights:Copyright',
498	'xapRights:Marked',
499	'xapRights:Owner',
500	'xapRights:UsageTerms',
501	'xapRights:WebStatement',
502// XMP Media Management Schema
503	'xapMM:DerivedFrom',
504	'xapMM:DocumentID',
505	'xapMM:History',
506	'xapMM:InstanceID',
507	'xapMM:ManagedFrom',
508	'xapMM:Manager',
509	'xapMM:ManageTo',
510	'xapMM:ManageUI',
511	'xapMM:ManagerVariant',
512	'xapMM:RenditionClass',
513	'xapMM:RenditionParams',
514	'xapMM:VersionID',
515	'xapMM:Versions',
516	'xapMM:LastURL',
517	'xapMM:RenditionOf',
518	'xapMM:SaveID',
519// XMP Basic Job Ticket Schema
520	'xapBJ:JobRef',
521// XMP Paged-Text Schema
522	'xmpTPg:MaxPageSize',
523	'xmpTPg:NPages',
524	'xmpTPg:Fonts',
525	'xmpTPg:Colorants',
526	'xmpTPg:PlateNames',
527// Adobe PDF Schema
528	'pdf:Keywords',
529	'pdf:PDFVersion',
530	'pdf:Producer',
531// Photoshop Schema
532	'photoshop:AuthorsPosition',
533	'photoshop:CaptionWriter',
534	'photoshop:Category',
535	'photoshop:City',
536	'photoshop:Country',
537	'photoshop:Credit',
538	'photoshop:DateCreated',
539	'photoshop:Headline',
540	'photoshop:History',
541// Not in XMP spec
542	'photoshop:Instructions',
543	'photoshop:Source',
544	'photoshop:State',
545	'photoshop:SupplementalCategories',
546	'photoshop:TransmissionReference',
547	'photoshop:Urgency',
548// EXIF Schemas
549	'tiff:ImageWidth',
550	'tiff:ImageLength',
551	'tiff:BitsPerSample',
552	'tiff:Compression',
553	'tiff:PhotometricInterpretation',
554	'tiff:Orientation',
555	'tiff:SamplesPerPixel',
556	'tiff:PlanarConfiguration',
557	'tiff:YCbCrSubSampling',
558	'tiff:YCbCrPositioning',
559	'tiff:XResolution',
560	'tiff:YResolution',
561	'tiff:ResolutionUnit',
562	'tiff:TransferFunction',
563	'tiff:WhitePoint',
564	'tiff:PrimaryChromaticities',
565	'tiff:YCbCrCoefficients',
566	'tiff:ReferenceBlackWhite',
567	'tiff:DateTime',
568	'tiff:ImageDescription',
569	'tiff:Make',
570	'tiff:Model',
571	'tiff:Software',
572	'tiff:Artist',
573	'tiff:Copyright',
574	'exif:ExifVersion',
575	'exif:FlashpixVersion',
576	'exif:ColorSpace',
577	'exif:ComponentsConfiguration',
578	'exif:CompressedBitsPerPixel',
579	'exif:PixelXDimension',
580	'exif:PixelYDimension',
581	'exif:MakerNote',
582	'exif:UserComment',
583	'exif:RelatedSoundFile',
584	'exif:DateTimeOriginal',
585	'exif:DateTimeDigitized',
586	'exif:ExposureTime',
587	'exif:FNumber',
588	'exif:ExposureProgram',
589	'exif:SpectralSensitivity',
590	'exif:ISOSpeedRatings',
591	'exif:OECF',
592	'exif:ShutterSpeedValue',
593	'exif:ApertureValue',
594	'exif:BrightnessValue',
595	'exif:ExposureBiasValue',
596	'exif:MaxApertureValue',
597	'exif:SubjectDistance',
598	'exif:MeteringMode',
599	'exif:LightSource',
600	'exif:Flash',
601	'exif:FocalLength',
602	'exif:SubjectArea',
603	'exif:FlashEnergy',
604	'exif:SpatialFrequencyResponse',
605	'exif:FocalPlaneXResolution',
606	'exif:FocalPlaneYResolution',
607	'exif:FocalPlaneResolutionUnit',
608	'exif:SubjectLocation',
609	'exif:SensingMethod',
610	'exif:FileSource',
611	'exif:SceneType',
612	'exif:CFAPattern',
613	'exif:CustomRendered',
614	'exif:ExposureMode',
615	'exif:WhiteBalance',
616	'exif:DigitalZoomRatio',
617	'exif:FocalLengthIn35mmFilm',
618	'exif:SceneCaptureType',
619	'exif:GainControl',
620	'exif:Contrast',
621	'exif:Saturation',
622	'exif:Sharpness',
623	'exif:DeviceSettingDescription',
624	'exif:SubjectDistanceRange',
625	'exif:ImageUniqueID',
626	'exif:GPSVersionID',
627	'exif:GPSLatitude',
628	'exif:GPSLongitude',
629	'exif:GPSAltitudeRef',
630	'exif:GPSAltitude',
631	'exif:GPSTimeStamp',
632	'exif:GPSSatellites',
633	'exif:GPSStatus',
634	'exif:GPSMeasureMode',
635	'exif:GPSDOP',
636	'exif:GPSSpeedRef',
637	'exif:GPSSpeed',
638	'exif:GPSTrackRef',
639	'exif:GPSTrack',
640	'exif:GPSImgDirectionRef',
641	'exif:GPSImgDirection',
642	'exif:GPSMapDatum',
643	'exif:GPSDestLatitude',
644	'exif:GPSDestLongitude',
645	'exif:GPSDestBearingRef',
646	'exif:GPSDestBearing',
647	'exif:GPSDestDistanceRef',
648	'exif:GPSDestDistance',
649	'exif:GPSProcessingMethod',
650	'exif:GPSAreaInformation',
651	'exif:GPSDifferential',
652	'stDim:w',
653	'stDim:h',
654	'stDim:unit',
655	'xapGImg:height',
656	'xapGImg:width',
657	'xapGImg:format',
658	'xapGImg:image',
659	'stEvt:action',
660	'stEvt:instanceID',
661	'stEvt:parameters',
662	'stEvt:softwareAgent',
663	'stEvt:when',
664	'stRef:instanceID',
665	'stRef:documentID',
666	'stRef:versionID',
667	'stRef:renditionClass',
668	'stRef:renditionParams',
669	'stRef:manager',
670	'stRef:managerVariant',
671	'stRef:manageTo',
672	'stRef:manageUI',
673	'stVer:comments',
674	'stVer:event',
675	'stVer:modifyDate',
676	'stVer:modifier',
677	'stVer:version',
678	'stJob:name',
679	'stJob:id',
680	'stJob:url',
681// Exif Flash
682	'exif:Fired',
683	'exif:Return',
684	'exif:Mode',
685	'exif:Function',
686	'exif:RedEyeMode',
687// Exif OECF/SFR
688	'exif:Columns',
689	'exif:Rows',
690	'exif:Names',
691	'exif:Values',
692// Exif CFAPattern
693	'exif:Columns',
694	'exif:Rows',
695	'exif:Values',
696// Exif DeviceSettings
697	'exif:Columns',
698	'exif:Rows',
699	'exif:Settings',
700);
701*/
702
703/**
704* Global Variable: JPEG_Segment_Names
705*
706* The names of the JPEG segment markers, indexed by their marker number
707*/
708$GLOBALS['JPEG_Segment_Names'] = array(
709	0x01 => 'TEM',
710	0x02 => 'RES',
711	0xC0 => 'SOF0',
712	0xC1 => 'SOF1',
713	0xC2 => 'SOF2',
714	0xC3 => 'SOF4',
715	0xC4 => 'DHT',
716	0xC5 => 'SOF5',
717	0xC6 => 'SOF6',
718	0xC7 => 'SOF7',
719	0xC8 => 'JPG',
720	0xC9 => 'SOF9',
721	0xCA => 'SOF10',
722	0xCB => 'SOF11',
723	0xCC => 'DAC',
724	0xCD => 'SOF13',
725	0xCE => 'SOF14',
726	0xCF => 'SOF15',
727	0xD0 => 'RST0',
728	0xD1 => 'RST1',
729	0xD2 => 'RST2',
730	0xD3 => 'RST3',
731	0xD4 => 'RST4',
732	0xD5 => 'RST5',
733	0xD6 => 'RST6',
734	0xD7 => 'RST7',
735	0xD8 => 'SOI',
736	0xD9 => 'EOI',
737	0xDA => 'SOS',
738	0xDB => 'DQT',
739	0xDC => 'DNL',
740	0xDD => 'DRI',
741	0xDE => 'DHP',
742	0xDF => 'EXP',
743	0xE0 => 'APP0',
744	0xE1 => 'APP1',
745	0xE2 => 'APP2',
746	0xE3 => 'APP3',
747	0xE4 => 'APP4',
748	0xE5 => 'APP5',
749	0xE6 => 'APP6',
750	0xE7 => 'APP7',
751	0xE8 => 'APP8',
752	0xE9 => 'APP9',
753	0xEA => 'APP10',
754	0xEB => 'APP11',
755	0xEC => 'APP12',
756	0xED => 'APP13',
757	0xEE => 'APP14',
758	0xEF => 'APP15',
759	0xF0 => 'JPG0',
760	0xF1 => 'JPG1',
761	0xF2 => 'JPG2',
762	0xF3 => 'JPG3',
763	0xF4 => 'JPG4',
764	0xF5 => 'JPG5',
765	0xF6 => 'JPG6',
766	0xF7 => 'JPG7',
767	0xF8 => 'JPG8',
768	0xF9 => 'JPG9',
769	0xFA => 'JPG10',
770	0xFB => 'JPG11',
771	0xFC => 'JPG12',
772	0xFD => 'JPG13',
773	0xFE => 'COM',
774);
775