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 (
) 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