1<?php 2/** 3 * JPEG metadata reader/writer 4 * 5 * @license BSD <http://www.opensource.org/licenses/bsd-license.php> 6 * @link http://github.com/sd/jpeg-php 7 * @author Sebastian Delmont <sdelmont@zonageek.com> 8 * @author Andreas Gohr <andi@splitbrain.org> 9 * @author Hakan Sandell <hakan.sandell@mydata.se> 10 * @todo Add support for Maker Notes, Extend for GIF and PNG metadata 11 */ 12 13// Original copyright notice: 14// 15// Copyright (c) 2003 Sebastian Delmont <sdelmont@zonageek.com> 16// All rights reserved. 17// 18// Redistribution and use in source and binary forms, with or without 19// modification, are permitted provided that the following conditions 20// are met: 21// 1. Redistributions of source code must retain the above copyright 22// notice, this list of conditions and the following disclaimer. 23// 2. Redistributions in binary form must reproduce the above copyright 24// notice, this list of conditions and the following disclaimer in the 25// documentation and/or other materials provided with the distribution. 26// 3. Neither the name of the author nor the names of its contributors 27// may be used to endorse or promote products derived from this software 28// without specific prior written permission. 29// 30// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 31// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 32// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 33// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 34// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 35// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 36// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 37// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 38// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 39// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 40// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE 41 42class JpegMeta { 43 var $_fileName; 44 var $_fp = null; 45 var $_fpout = null; 46 var $_type = 'unknown'; 47 48 var $_markers; 49 var $_info; 50 51 52 /** 53 * Constructor 54 * 55 * @author Sebastian Delmont <sdelmont@zonageek.com> 56 * 57 * @param $fileName 58 */ 59 function __construct($fileName) { 60 61 $this->_fileName = $fileName; 62 63 $this->_fp = null; 64 $this->_type = 'unknown'; 65 66 unset($this->_info); 67 unset($this->_markers); 68 } 69 70 /** 71 * Returns all gathered info as multidim array 72 * 73 * @author Sebastian Delmont <sdelmont@zonageek.com> 74 */ 75 function & getRawInfo() { 76 $this->_parseAll(); 77 78 if ($this->_markers == null) { 79 return false; 80 } 81 82 return $this->_info; 83 } 84 85 /** 86 * Returns basic image info 87 * 88 * @author Sebastian Delmont <sdelmont@zonageek.com> 89 */ 90 function & getBasicInfo() { 91 $this->_parseAll(); 92 93 $info = array(); 94 95 if ($this->_markers == null) { 96 return false; 97 } 98 99 $info['Name'] = $this->_info['file']['Name']; 100 if (isset($this->_info['file']['Url'])) { 101 $info['Url'] = $this->_info['file']['Url']; 102 $info['NiceSize'] = "???KB"; 103 } else { 104 $info['Size'] = $this->_info['file']['Size']; 105 $info['NiceSize'] = $this->_info['file']['NiceSize']; 106 } 107 108 if (@isset($this->_info['sof']['Format'])) { 109 $info['Format'] = $this->_info['sof']['Format'] . " JPEG"; 110 } else { 111 $info['Format'] = $this->_info['sof']['Format'] . " JPEG"; 112 } 113 114 if (@isset($this->_info['sof']['ColorChannels'])) { 115 $info['ColorMode'] = ($this->_info['sof']['ColorChannels'] > 1) ? "Color" : "B&W"; 116 } 117 118 $info['Width'] = $this->getWidth(); 119 $info['Height'] = $this->getHeight(); 120 $info['DimStr'] = $this->getDimStr(); 121 122 $dates = $this->getDates(); 123 124 $info['DateTime'] = $dates['EarliestTime']; 125 $info['DateTimeStr'] = $dates['EarliestTimeStr']; 126 127 $info['HasThumbnail'] = $this->hasThumbnail(); 128 129 return $info; 130 } 131 132 133 /** 134 * Convinience function to access nearly all available Data 135 * through one function 136 * 137 * @author Andreas Gohr <andi@splitbrain.org> 138 * 139 * @param array|string $fields field name or array with field names 140 * @return bool|string 141 */ 142 function getField($fields) { 143 if(!is_array($fields)) $fields = array($fields); 144 $info = false; 145 foreach($fields as $field){ 146 $lower_field = strtolower($field); 147 if(str_starts_with($lower_field, 'iptc.')){ 148 $info = $this->getIPTCField(substr($field,5)); 149 }elseif(str_starts_with($lower_field, 'exif.')){ 150 $info = $this->getExifField(substr($field,5)); 151 }elseif(str_starts_with($lower_field, 'xmp.')){ 152 $info = $this->getXmpField(substr($field,4)); 153 }elseif(str_starts_with($lower_field, 'file.')){ 154 $info = $this->getFileField(substr($field,5)); 155 }elseif(str_starts_with($lower_field, 'date.')){ 156 $info = $this->getDateField(substr($field,5)); 157 }elseif($lower_field == 'simple.camera'){ 158 $info = $this->getCamera(); 159 }elseif($lower_field == 'simple.raw'){ 160 return $this->getRawInfo(); 161 }elseif($lower_field == 'simple.title'){ 162 $info = $this->getTitle(); 163 }elseif($lower_field == 'simple.shutterspeed'){ 164 $info = $this->getShutterSpeed(); 165 }else{ 166 $info = $this->getExifField($field); 167 } 168 if($info != false) break; 169 } 170 171 if($info === false) $info = ''; 172 if(is_array($info)){ 173 if(isset($info['val'])){ 174 $info = $info['val']; 175 }else{ 176 $arr = array(); 177 foreach($info as $part){ 178 if(is_array($part)){ 179 if(isset($part['val'])){ 180 $arr[] = $part['val']; 181 }else{ 182 $arr[] = join(', ',$part); 183 } 184 }else{ 185 $arr[] = $part; 186 } 187 } 188 $info = join(', ',$arr); 189 } 190 } 191 return trim($info); 192 } 193 194 /** 195 * Convinience function to set nearly all available Data 196 * through one function 197 * 198 * @author Andreas Gohr <andi@splitbrain.org> 199 * 200 * @param string $field field name 201 * @param string $value 202 * @return bool success or fail 203 */ 204 function setField($field, $value) { 205 $lower_field = strtolower($field); 206 if(str_starts_with($lower_field, 'iptc.')){ 207 return $this->setIPTCField(substr($field,5),$value); 208 }elseif(str_starts_with($lower_field, 'exif.')){ 209 return $this->setExifField(substr($field,5),$value); 210 }else{ 211 return $this->setExifField($field,$value); 212 } 213 } 214 215 /** 216 * Convinience function to delete nearly all available Data 217 * through one function 218 * 219 * @author Andreas Gohr <andi@splitbrain.org> 220 * 221 * @param string $field field name 222 * @return bool 223 */ 224 function deleteField($field) { 225 $lower_field = strtolower($field); 226 if(str_starts_with($lower_field, 'iptc.')){ 227 return $this->deleteIPTCField(substr($field,5)); 228 }elseif(str_starts_with($lower_field, 'exif.')){ 229 return $this->deleteExifField(substr($field,5)); 230 }else{ 231 return $this->deleteExifField($field); 232 } 233 } 234 235 /** 236 * Return a date field 237 * 238 * @author Andreas Gohr <andi@splitbrain.org> 239 * 240 * @param string $field 241 * @return false|string 242 */ 243 function getDateField($field) { 244 if (!isset($this->_info['dates'])) { 245 $this->_info['dates'] = $this->getDates(); 246 } 247 248 if (isset($this->_info['dates'][$field])) { 249 return $this->_info['dates'][$field]; 250 } 251 252 return false; 253 } 254 255 /** 256 * Return a file info field 257 * 258 * @author Andreas Gohr <andi@splitbrain.org> 259 * 260 * @param string $field field name 261 * @return false|string 262 */ 263 function getFileField($field) { 264 if (!isset($this->_info['file'])) { 265 $this->_parseFileInfo(); 266 } 267 268 if (isset($this->_info['file'][$field])) { 269 return $this->_info['file'][$field]; 270 } 271 272 return false; 273 } 274 275 /** 276 * Return the camera info (Maker and Model) 277 * 278 * @author Andreas Gohr <andi@splitbrain.org> 279 * @todo handle makernotes 280 * 281 * @return false|string 282 */ 283 function getCamera(){ 284 $make = $this->getField(array('Exif.Make','Exif.TIFFMake')); 285 $model = $this->getField(array('Exif.Model','Exif.TIFFModel')); 286 $cam = trim("$make $model"); 287 if(empty($cam)) return false; 288 return $cam; 289 } 290 291 /** 292 * Return shutter speed as a ratio 293 * 294 * @author Joe Lapp <joe.lapp@pobox.com> 295 * 296 * @return string 297 */ 298 function getShutterSpeed() { 299 if (!isset($this->_info['exif'])) { 300 $this->_parseMarkerExif(); 301 } 302 if(!isset($this->_info['exif']['ExposureTime'])){ 303 return ''; 304 } 305 306 $field = $this->_info['exif']['ExposureTime']; 307 if($field['den'] == 1) return $field['num']; 308 return $field['num'].'/'.$field['den']; 309 } 310 311 /** 312 * Return an EXIF field 313 * 314 * @author Sebastian Delmont <sdelmont@zonageek.com> 315 * 316 * @param string $field field name 317 * @return false|string 318 */ 319 function getExifField($field) { 320 if (!isset($this->_info['exif'])) { 321 $this->_parseMarkerExif(); 322 } 323 324 if ($this->_markers == null) { 325 return false; 326 } 327 328 if (isset($this->_info['exif'][$field])) { 329 return $this->_info['exif'][$field]; 330 } 331 332 return false; 333 } 334 335 /** 336 * Return an XMP field 337 * 338 * @author Hakan Sandell <hakan.sandell@mydata.se> 339 * 340 * @param string $field field name 341 * @return false|string 342 */ 343 function getXmpField($field) { 344 if (!isset($this->_info['xmp'])) { 345 $this->_parseMarkerXmp(); 346 } 347 348 if ($this->_markers == null) { 349 return false; 350 } 351 352 if (isset($this->_info['xmp'][$field])) { 353 return $this->_info['xmp'][$field]; 354 } 355 356 return false; 357 } 358 359 /** 360 * Return an Adobe Field 361 * 362 * @author Sebastian Delmont <sdelmont@zonageek.com> 363 * 364 * @param string $field field name 365 * @return false|string 366 */ 367 function getAdobeField($field) { 368 if (!isset($this->_info['adobe'])) { 369 $this->_parseMarkerAdobe(); 370 } 371 372 if ($this->_markers == null) { 373 return false; 374 } 375 376 if (isset($this->_info['adobe'][$field])) { 377 return $this->_info['adobe'][$field]; 378 } 379 380 return false; 381 } 382 383 /** 384 * Return an IPTC field 385 * 386 * @author Sebastian Delmont <sdelmont@zonageek.com> 387 * 388 * @param string $field field name 389 * @return false|string 390 */ 391 function getIPTCField($field) { 392 if (!isset($this->_info['iptc'])) { 393 $this->_parseMarkerAdobe(); 394 } 395 396 if ($this->_markers == null) { 397 return false; 398 } 399 400 if (isset($this->_info['iptc'][$field])) { 401 return $this->_info['iptc'][$field]; 402 } 403 404 return false; 405 } 406 407 /** 408 * Set an EXIF field 409 * 410 * @author Sebastian Delmont <sdelmont@zonageek.com> 411 * @author Joe Lapp <joe.lapp@pobox.com> 412 * 413 * @param string $field field name 414 * @param string $value 415 * @return bool 416 */ 417 function setExifField($field, $value) { 418 if (!isset($this->_info['exif'])) { 419 $this->_parseMarkerExif(); 420 } 421 422 if ($this->_markers == null) { 423 return false; 424 } 425 426 if ($this->_info['exif'] == false) { 427 $this->_info['exif'] = array(); 428 } 429 430 // make sure datetimes are in correct format 431 if(strlen($field) >= 8 && str_starts_with(strtolower($field), 'datetime')) { 432 if(strlen($value) < 8 || $value[4] != ':' || $value[7] != ':') { 433 $value = date('Y:m:d H:i:s', strtotime($value)); 434 } 435 } 436 437 $this->_info['exif'][$field] = $value; 438 439 return true; 440 } 441 442 /** 443 * Set an Adobe Field 444 * 445 * @author Sebastian Delmont <sdelmont@zonageek.com> 446 * 447 * @param string $field field name 448 * @param string $value 449 * @return bool 450 */ 451 function setAdobeField($field, $value) { 452 if (!isset($this->_info['adobe'])) { 453 $this->_parseMarkerAdobe(); 454 } 455 456 if ($this->_markers == null) { 457 return false; 458 } 459 460 if ($this->_info['adobe'] == false) { 461 $this->_info['adobe'] = array(); 462 } 463 464 $this->_info['adobe'][$field] = $value; 465 466 return true; 467 } 468 469 /** 470 * Calculates the multiplier needed to resize the image to the given 471 * dimensions 472 * 473 * @author Andreas Gohr <andi@splitbrain.org> 474 * 475 * @param int $maxwidth 476 * @param int $maxheight 477 * @return float|int 478 */ 479 function getResizeRatio($maxwidth,$maxheight=0){ 480 if(!$maxheight) $maxheight = $maxwidth; 481 482 $w = $this->getField('File.Width'); 483 $h = $this->getField('File.Height'); 484 485 $ratio = 1; 486 if($w >= $h){ 487 if($w >= $maxwidth){ 488 $ratio = $maxwidth/$w; 489 }elseif($h > $maxheight){ 490 $ratio = $maxheight/$h; 491 } 492 }else{ 493 if($h >= $maxheight){ 494 $ratio = $maxheight/$h; 495 }elseif($w > $maxwidth){ 496 $ratio = $maxwidth/$w; 497 } 498 } 499 return $ratio; 500 } 501 502 503 /** 504 * Set an IPTC field 505 * 506 * @author Sebastian Delmont <sdelmont@zonageek.com> 507 * 508 * @param string $field field name 509 * @param string $value 510 * @return bool 511 */ 512 function setIPTCField($field, $value) { 513 if (!isset($this->_info['iptc'])) { 514 $this->_parseMarkerAdobe(); 515 } 516 517 if ($this->_markers == null) { 518 return false; 519 } 520 521 if ($this->_info['iptc'] == false) { 522 $this->_info['iptc'] = array(); 523 } 524 525 $this->_info['iptc'][$field] = $value; 526 527 return true; 528 } 529 530 /** 531 * Delete an EXIF field 532 * 533 * @author Sebastian Delmont <sdelmont@zonageek.com> 534 * 535 * @param string $field field name 536 * @return bool 537 */ 538 function deleteExifField($field) { 539 if (!isset($this->_info['exif'])) { 540 $this->_parseMarkerAdobe(); 541 } 542 543 if ($this->_markers == null) { 544 return false; 545 } 546 547 if ($this->_info['exif'] != false) { 548 unset($this->_info['exif'][$field]); 549 } 550 551 return true; 552 } 553 554 /** 555 * Delete an Adobe field 556 * 557 * @author Sebastian Delmont <sdelmont@zonageek.com> 558 * 559 * @param string $field field name 560 * @return bool 561 */ 562 function deleteAdobeField($field) { 563 if (!isset($this->_info['adobe'])) { 564 $this->_parseMarkerAdobe(); 565 } 566 567 if ($this->_markers == null) { 568 return false; 569 } 570 571 if ($this->_info['adobe'] != false) { 572 unset($this->_info['adobe'][$field]); 573 } 574 575 return true; 576 } 577 578 /** 579 * Delete an IPTC field 580 * 581 * @author Sebastian Delmont <sdelmont@zonageek.com> 582 * 583 * @param string $field field name 584 * @return bool 585 */ 586 function deleteIPTCField($field) { 587 if (!isset($this->_info['iptc'])) { 588 $this->_parseMarkerAdobe(); 589 } 590 591 if ($this->_markers == null) { 592 return false; 593 } 594 595 if ($this->_info['iptc'] != false) { 596 unset($this->_info['iptc'][$field]); 597 } 598 599 return true; 600 } 601 602 /** 603 * Get the image's title, tries various fields 604 * 605 * @param int $max maximum number chars (keeps words) 606 * @return false|string 607 * 608 * @author Andreas Gohr <andi@splitbrain.org> 609 */ 610 function getTitle($max=80){ 611 // try various fields 612 $cap = $this->getField(array('Iptc.Headline', 613 'Iptc.Caption', 614 'Xmp.dc:title', 615 'Exif.UserComment', 616 'Exif.TIFFUserComment', 617 'Exif.TIFFImageDescription', 618 'File.Name')); 619 if (empty($cap)) return false; 620 621 if(!$max) return $cap; 622 // Shorten to 80 chars (keeping words) 623 $new = preg_replace('/\n.+$/','',wordwrap($cap, $max)); 624 if($new != $cap) $new .= '...'; 625 626 return $new; 627 } 628 629 /** 630 * Gather various date fields 631 * 632 * @author Sebastian Delmont <sdelmont@zonageek.com> 633 * 634 * @return array|bool 635 */ 636 function getDates() { 637 $this->_parseAll(); 638 if ($this->_markers == null) { 639 if (@isset($this->_info['file']['UnixTime'])) { 640 $dates = array(); 641 $dates['FileModified'] = $this->_info['file']['UnixTime']; 642 $dates['Time'] = $this->_info['file']['UnixTime']; 643 $dates['TimeSource'] = 'FileModified'; 644 $dates['TimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']); 645 $dates['EarliestTime'] = $this->_info['file']['UnixTime']; 646 $dates['EarliestTimeSource'] = 'FileModified'; 647 $dates['EarliestTimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']); 648 $dates['LatestTime'] = $this->_info['file']['UnixTime']; 649 $dates['LatestTimeSource'] = 'FileModified'; 650 $dates['LatestTimeStr'] = date("Y-m-d H:i:s", $this->_info['file']['UnixTime']); 651 return $dates; 652 } 653 return false; 654 } 655 656 $dates = array(); 657 658 $latestTime = 0; 659 $latestTimeSource = ""; 660 $earliestTime = time(); 661 $earliestTimeSource = ""; 662 663 $exifDateFields = array( 664 'DateTime' => 'ExifDateTime', 665 'DateTimeOriginal' => 'ExifDateTimeOriginal', 666 'DateTimeDigitized' => 'ExifDateTimeDigitized', 667 ); 668 669 foreach ($exifDateFields as $field => $source) { 670 if (!isset($this->_info['exif'][$field])) { 671 continue; 672 } 673 674 $dates[$source] = $this->_info['exif'][$field]; 675 676 foreach ($this->parseExifDateTime($this->_info['exif'][$field]) as $t) { 677 if ($t && $t > $latestTime) { 678 $latestTime = $t; 679 $latestTimeSource = $source; 680 } 681 682 if ($t && $t < $earliestTime) { 683 $earliestTime = $t; 684 $earliestTimeSource = $source; 685 } 686 } 687 } 688 689 if (@isset($this->_info['iptc']['DateCreated'])) { 690 $dates['IPTCDateCreated'] = $this->_info['iptc']['DateCreated']; 691 692 $aux = $this->_info['iptc']['DateCreated']; 693 $aux = substr($aux, 0, 4) . "-" . substr($aux, 4, 2) . "-" . substr($aux, 6, 2); 694 $t = strtotime($aux); 695 696 if ($t && $t > $latestTime) { 697 $latestTime = $t; 698 $latestTimeSource = "IPTCDateCreated"; 699 } 700 701 if ($t && $t < $earliestTime) { 702 $earliestTime = $t; 703 $earliestTimeSource = "IPTCDateCreated"; 704 } 705 } 706 707 if (@isset($this->_info['file']['UnixTime'])) { 708 $dates['FileModified'] = $this->_info['file']['UnixTime']; 709 710 $t = $this->_info['file']['UnixTime']; 711 712 if ($t && $t > $latestTime) { 713 $latestTime = $t; 714 $latestTimeSource = "FileModified"; 715 } 716 717 if ($t && $t < $earliestTime) { 718 $earliestTime = $t; 719 $earliestTimeSource = "FileModified"; 720 } 721 } 722 723 $dates['Time'] = $earliestTime; 724 $dates['TimeSource'] = $earliestTimeSource; 725 $dates['TimeStr'] = date("Y-m-d H:i:s", $earliestTime); 726 $dates['EarliestTime'] = $earliestTime; 727 $dates['EarliestTimeSource'] = $earliestTimeSource; 728 $dates['EarliestTimeStr'] = date("Y-m-d H:i:s", $earliestTime); 729 $dates['LatestTime'] = $latestTime; 730 $dates['LatestTimeSource'] = $latestTimeSource; 731 $dates['LatestTimeStr'] = date("Y-m-d H:i:s", $latestTime); 732 733 return $dates; 734 } 735 736 /** 737 * Parse one or more EXIF date strings to unix timestamps 738 * 739 * EXIF tags can occur multiple times in different IFDs. In that case the 740 * metadata parser stores the field as an array of values. 741 * 742 * @return int[] 743 */ 744 protected function parseExifDateTime(string|array $value): array { 745 $timestamps = []; 746 foreach ((array) $value as $date) { 747 if (is_array($date) && isset($date['val'])) { 748 $date = $date['val']; 749 } 750 751 if (!is_string($date) || strlen($date) < 10) { 752 continue; 753 } 754 755 $date[4] = '-'; 756 $date[7] = '-'; 757 $timestamp = strtotime($date); 758 if ($timestamp) { 759 $timestamps[] = $timestamp; 760 } 761 } 762 763 return $timestamps; 764 } 765 766 /** 767 * Get the image width, tries various fields 768 * 769 * @author Sebastian Delmont <sdelmont@zonageek.com> 770 * 771 * @return false|string 772 */ 773 function getWidth() { 774 if (!isset($this->_info['sof'])) { 775 $this->_parseMarkerSOF(); 776 } 777 778 if ($this->_markers == null) { 779 return false; 780 } 781 782 if (isset($this->_info['sof']['ImageWidth'])) { 783 return $this->_info['sof']['ImageWidth']; 784 } 785 786 if (!isset($this->_info['exif'])) { 787 $this->_parseMarkerExif(); 788 } 789 790 if (isset($this->_info['exif']['PixelXDimension'])) { 791 return $this->_info['exif']['PixelXDimension']; 792 } 793 794 return false; 795 } 796 797 /** 798 * Get the image height, tries various fields 799 * 800 * @author Sebastian Delmont <sdelmont@zonageek.com> 801 * 802 * @return false|string 803 */ 804 function getHeight() { 805 if (!isset($this->_info['sof'])) { 806 $this->_parseMarkerSOF(); 807 } 808 809 if ($this->_markers == null) { 810 return false; 811 } 812 813 if (isset($this->_info['sof']['ImageHeight'])) { 814 return $this->_info['sof']['ImageHeight']; 815 } 816 817 if (!isset($this->_info['exif'])) { 818 $this->_parseMarkerExif(); 819 } 820 821 if (isset($this->_info['exif']['PixelYDimension'])) { 822 return $this->_info['exif']['PixelYDimension']; 823 } 824 825 return false; 826 } 827 828 /** 829 * Get an dimension string for use in img tag 830 * 831 * @author Sebastian Delmont <sdelmont@zonageek.com> 832 * 833 * @return false|string 834 */ 835 function getDimStr() { 836 if ($this->_markers == null) { 837 return false; 838 } 839 840 $w = $this->getWidth(); 841 $h = $this->getHeight(); 842 843 return "width='" . $w . "' height='" . $h . "'"; 844 } 845 846 /** 847 * Checks for an embedded thumbnail 848 * 849 * @author Sebastian Delmont <sdelmont@zonageek.com> 850 * 851 * @param string $which possible values: 'any', 'exif' or 'adobe' 852 * @return false|string 853 */ 854 function hasThumbnail($which = 'any') { 855 if (($which == 'any') || ($which == 'exif')) { 856 if (!isset($this->_info['exif'])) { 857 $this->_parseMarkerExif(); 858 } 859 860 if ($this->_markers == null) { 861 return false; 862 } 863 864 if (isset($this->_info['exif']) && is_array($this->_info['exif'])) { 865 if (isset($this->_info['exif']['JFIFThumbnail'])) { 866 return 'exif'; 867 } 868 } 869 } 870 871 if ($which == 'adobe') { 872 if (!isset($this->_info['adobe'])) { 873 $this->_parseMarkerAdobe(); 874 } 875 876 if ($this->_markers == null) { 877 return false; 878 } 879 880 if (isset($this->_info['adobe']) && is_array($this->_info['adobe'])) { 881 if (isset($this->_info['adobe']['ThumbnailData'])) { 882 return 'exif'; 883 } 884 } 885 } 886 887 return false; 888 } 889 890 /** 891 * Send embedded thumbnail to browser 892 * 893 * @author Sebastian Delmont <sdelmont@zonageek.com> 894 * 895 * @param string $which possible values: 'any', 'exif' or 'adobe' 896 * @return bool 897 */ 898 function sendThumbnail($which = 'any') { 899 $data = null; 900 901 if (($which == 'any') || ($which == 'exif')) { 902 if (!isset($this->_info['exif'])) { 903 $this->_parseMarkerExif(); 904 } 905 906 if ($this->_markers == null) { 907 return false; 908 } 909 910 if (isset($this->_info['exif']) && is_array($this->_info['exif'])) { 911 if (isset($this->_info['exif']['JFIFThumbnail'])) { 912 $data =& $this->_info['exif']['JFIFThumbnail']; 913 } 914 } 915 } 916 917 if (($which == 'adobe') || ($data == null)){ 918 if (!isset($this->_info['adobe'])) { 919 $this->_parseMarkerAdobe(); 920 } 921 922 if ($this->_markers == null) { 923 return false; 924 } 925 926 if (isset($this->_info['adobe']) && is_array($this->_info['adobe'])) { 927 if (isset($this->_info['adobe']['ThumbnailData'])) { 928 $data =& $this->_info['adobe']['ThumbnailData']; 929 } 930 } 931 } 932 933 if ($data != null) { 934 header("Content-type: image/jpeg"); 935 echo $data; 936 return true; 937 } 938 939 return false; 940 } 941 942 /** 943 * Save changed Metadata 944 * 945 * @author Sebastian Delmont <sdelmont@zonageek.com> 946 * @author Andreas Gohr <andi@splitbrain.org> 947 * 948 * @param string $fileName file name or empty string for a random name 949 * @return bool 950 */ 951 function save($fileName = "") { 952 if ($fileName == "") { 953 $tmpName = tempnam(dirname($this->_fileName),'_metatemp_'); 954 $this->_writeJPEG($tmpName); 955 if (file_exists($tmpName)) { 956 return io_rename($tmpName, $this->_fileName); 957 } 958 } else { 959 return $this->_writeJPEG($fileName); 960 } 961 return false; 962 } 963 964 /*************************************************************/ 965 /* PRIVATE FUNCTIONS (Internal Use Only!) */ 966 /*************************************************************/ 967 968 /*************************************************************/ 969 function _dispose($fileName = "") { 970 $this->_fileName = $fileName; 971 972 $this->_fp = null; 973 $this->_type = 'unknown'; 974 975 unset($this->_markers); 976 unset($this->_info); 977 } 978 979 /*************************************************************/ 980 function _readJPEG() { 981 unset($this->_markers); 982 //unset($this->_info); 983 $this->_markers = array(); 984 //$this->_info = array(); 985 986 $this->_fp = @fopen($this->_fileName, 'rb'); 987 if ($this->_fp) { 988 if (file_exists($this->_fileName)) { 989 $this->_type = 'file'; 990 } 991 else { 992 $this->_type = 'url'; 993 } 994 } else { 995 $this->_fp = null; 996 return false; // ERROR: Can't open file 997 } 998 999 // Check for the JPEG signature 1000 $c1 = ord(fgetc($this->_fp)); 1001 $c2 = ord(fgetc($this->_fp)); 1002 1003 if ($c1 != 0xFF || $c2 != 0xD8) { // (0xFF + SOI) 1004 $this->_markers = null; 1005 return false; // ERROR: File is not a JPEG 1006 } 1007 1008 $count = 0; 1009 1010 $done = false; 1011 $ok = true; 1012 1013 while (!$done) { 1014 $capture = false; 1015 1016 // First, skip any non 0xFF bytes 1017 $discarded = 0; 1018 $c = ord(fgetc($this->_fp)); 1019 while (!feof($this->_fp) && ($c != 0xFF)) { 1020 $discarded++; 1021 $c = ord(fgetc($this->_fp)); 1022 } 1023 // Then skip all 0xFF until the marker byte 1024 do { 1025 $marker = ord(fgetc($this->_fp)); 1026 } while (!feof($this->_fp) && ($marker == 0xFF)); 1027 1028 if (feof($this->_fp)) { 1029 return false; // ERROR: Unexpected EOF 1030 } 1031 if ($discarded != 0) { 1032 return false; // ERROR: Extraneous data 1033 } 1034 1035 $length = ord(fgetc($this->_fp)) * 256 + ord(fgetc($this->_fp)); 1036 if (feof($this->_fp)) { 1037 return false; // ERROR: Unexpected EOF 1038 } 1039 if ($length < 2) { 1040 return false; // ERROR: Extraneous data 1041 } 1042 $length = $length - 2; // The length we got counts itself 1043 1044 switch ($marker) { 1045 case 0xC0: // SOF0 1046 case 0xC1: // SOF1 1047 case 0xC2: // SOF2 1048 case 0xC9: // SOF9 1049 case 0xE0: // APP0: JFIF data 1050 case 0xE1: // APP1: EXIF or XMP data 1051 case 0xED: // APP13: IPTC / Photoshop data 1052 $capture = true; 1053 break; 1054 case 0xDA: // SOS: Start of scan... the image itself and the last block on the file 1055 $capture = false; 1056 $length = -1; // This field has no length... it includes all data until EOF 1057 $done = true; 1058 break; 1059 default: 1060 $capture = true;//false; 1061 break; 1062 } 1063 1064 $this->_markers[$count] = array(); 1065 $this->_markers[$count]['marker'] = $marker; 1066 $this->_markers[$count]['length'] = $length; 1067 1068 if ($capture) { 1069 if ($length) 1070 $this->_markers[$count]['data'] = fread($this->_fp, $length); 1071 else 1072 $this->_markers[$count]['data'] = ""; 1073 } 1074 elseif (!$done) { 1075 $result = @fseek($this->_fp, $length, SEEK_CUR); 1076 // fseek doesn't seem to like HTTP 'files', but fgetc has no problem 1077 if (!($result === 0)) { 1078 for ($i = 0; $i < $length; $i++) { 1079 fgetc($this->_fp); 1080 } 1081 } 1082 } 1083 $count++; 1084 } 1085 1086 if ($this->_fp) { 1087 fclose($this->_fp); 1088 $this->_fp = null; 1089 } 1090 1091 return $ok; 1092 } 1093 1094 /*************************************************************/ 1095 function _parseAll() { 1096 if (!isset($this->_info['file'])) { 1097 $this->_parseFileInfo(); 1098 } 1099 if (!isset($this->_markers)) { 1100 $this->_readJPEG(); 1101 } 1102 1103 if ($this->_markers == null) { 1104 return false; 1105 } 1106 1107 if (!isset($this->_info['jfif'])) { 1108 $this->_parseMarkerJFIF(); 1109 } 1110 if (!isset($this->_info['jpeg'])) { 1111 $this->_parseMarkerSOF(); 1112 } 1113 if (!isset($this->_info['exif'])) { 1114 $this->_parseMarkerExif(); 1115 } 1116 if (!isset($this->_info['xmp'])) { 1117 $this->_parseMarkerXmp(); 1118 } 1119 if (!isset($this->_info['adobe'])) { 1120 $this->_parseMarkerAdobe(); 1121 } 1122 } 1123 1124 /*************************************************************/ 1125 1126 /** 1127 * @param string $outputName 1128 * 1129 * @return bool 1130 */ 1131 function _writeJPEG($outputName) { 1132 $this->_parseAll(); 1133 1134 $wroteEXIF = false; 1135 $wroteAdobe = false; 1136 1137 $this->_fp = @fopen($this->_fileName, 'r'); 1138 if ($this->_fp) { 1139 if (file_exists($this->_fileName)) { 1140 $this->_type = 'file'; 1141 } 1142 else { 1143 $this->_type = 'url'; 1144 } 1145 } else { 1146 $this->_fp = null; 1147 return false; // ERROR: Can't open file 1148 } 1149 1150 $this->_fpout = fopen($outputName, 'wb'); 1151 if (!$this->_fpout) { 1152 $this->_fpout = null; 1153 fclose($this->_fp); 1154 $this->_fp = null; 1155 return false; // ERROR: Can't open output file 1156 } 1157 1158 // Check for the JPEG signature 1159 $c1 = ord(fgetc($this->_fp)); 1160 $c2 = ord(fgetc($this->_fp)); 1161 1162 if ($c1 != 0xFF || $c2 != 0xD8) { // (0xFF + SOI) 1163 return false; // ERROR: File is not a JPEG 1164 } 1165 1166 fputs($this->_fpout, chr(0xFF), 1); 1167 fputs($this->_fpout, chr(0xD8), 1); // (0xFF + SOI) 1168 1169 $count = 0; 1170 1171 $done = false; 1172 $ok = true; 1173 1174 while (!$done) { 1175 // First, skip any non 0xFF bytes 1176 $discarded = 0; 1177 $c = ord(fgetc($this->_fp)); 1178 while (!feof($this->_fp) && ($c != 0xFF)) { 1179 $discarded++; 1180 $c = ord(fgetc($this->_fp)); 1181 } 1182 // Then skip all 0xFF until the marker byte 1183 do { 1184 $marker = ord(fgetc($this->_fp)); 1185 } while (!feof($this->_fp) && ($marker == 0xFF)); 1186 1187 if (feof($this->_fp)) { 1188 $ok = false; 1189 break; // ERROR: Unexpected EOF 1190 } 1191 if ($discarded != 0) { 1192 $ok = false; 1193 break; // ERROR: Extraneous data 1194 } 1195 1196 $length = ord(fgetc($this->_fp)) * 256 + ord(fgetc($this->_fp)); 1197 if (feof($this->_fp)) { 1198 $ok = false; 1199 break; // ERROR: Unexpected EOF 1200 } 1201 if ($length < 2) { 1202 $ok = false; 1203 break; // ERROR: Extraneous data 1204 } 1205 $length = $length - 2; // The length we got counts itself 1206 1207 unset($data); 1208 if ($marker == 0xE1) { // APP1: EXIF data 1209 $data =& $this->_createMarkerEXIF(); 1210 $wroteEXIF = true; 1211 } 1212 elseif ($marker == 0xED) { // APP13: IPTC / Photoshop data 1213 $data =& $this->_createMarkerAdobe(); 1214 $wroteAdobe = true; 1215 } 1216 elseif ($marker == 0xDA) { // SOS: Start of scan... the image itself and the last block on the file 1217 $done = true; 1218 } 1219 1220 if (!$wroteEXIF && (($marker < 0xE0) || ($marker > 0xEF))) { 1221 if (isset($this->_info['exif']) && is_array($this->_info['exif'])) { 1222 $exif =& $this->_createMarkerEXIF(); 1223 $this->_writeJPEGMarker(0xE1, strlen($exif), $exif, 0); 1224 unset($exif); 1225 } 1226 $wroteEXIF = true; 1227 } 1228 1229 if (!$wroteAdobe && (($marker < 0xE0) || ($marker > 0xEF))) { 1230 if ((isset($this->_info['adobe']) && is_array($this->_info['adobe'])) 1231 || (isset($this->_info['iptc']) && is_array($this->_info['iptc']))) { 1232 $adobe =& $this->_createMarkerAdobe(); 1233 $this->_writeJPEGMarker(0xED, strlen($adobe), $adobe, 0); 1234 unset($adobe); 1235 } 1236 $wroteAdobe = true; 1237 } 1238 1239 $origLength = $length; 1240 if (isset($data)) { 1241 $length = strlen($data); 1242 } 1243 1244 if ($marker != -1) { 1245 $this->_writeJPEGMarker($marker, $length, $data, $origLength); 1246 } 1247 } 1248 1249 if ($this->_fp) { 1250 fclose($this->_fp); 1251 $this->_fp = null; 1252 } 1253 1254 if ($this->_fpout) { 1255 fclose($this->_fpout); 1256 $this->_fpout = null; 1257 } 1258 1259 return $ok; 1260 } 1261 1262 /*************************************************************/ 1263 1264 /** 1265 * @param integer $marker 1266 * @param integer $length 1267 * @param string $data 1268 * @param integer $origLength 1269 * 1270 * @return bool 1271 */ 1272 function _writeJPEGMarker($marker, $length, &$data, $origLength) { 1273 if ($length <= 0) { 1274 return false; 1275 } 1276 1277 fputs($this->_fpout, chr(0xFF), 1); 1278 fputs($this->_fpout, chr($marker), 1); 1279 fputs($this->_fpout, chr((($length + 2) & 0x0000FF00) >> 8), 1); 1280 fputs($this->_fpout, chr((($length + 2) & 0x000000FF) >> 0), 1); 1281 1282 if (isset($data)) { 1283 // Copy the generated data 1284 fputs($this->_fpout, $data, $length); 1285 1286 if ($origLength > 0) { // Skip the original data 1287 $result = @fseek($this->_fp, $origLength, SEEK_CUR); 1288 // fseek doesn't seem to like HTTP 'files', but fgetc has no problem 1289 if ($result != 0) { 1290 for ($i = 0; $i < $origLength; $i++) { 1291 fgetc($this->_fp); 1292 } 1293 } 1294 } 1295 } else { 1296 if ($marker == 0xDA) { // Copy until EOF 1297 while (!feof($this->_fp)) { 1298 $data = fread($this->_fp, 1024 * 16); 1299 fputs($this->_fpout, $data, strlen($data)); 1300 } 1301 } else { // Copy only $length bytes 1302 $data = @fread($this->_fp, $length); 1303 fputs($this->_fpout, $data, $length); 1304 } 1305 } 1306 1307 return true; 1308 } 1309 1310 /** 1311 * Gets basic info from the file - should work with non-JPEGs 1312 * 1313 * @author Sebastian Delmont <sdelmont@zonageek.com> 1314 * @author Andreas Gohr <andi@splitbrain.org> 1315 */ 1316 function _parseFileInfo() { 1317 if (file_exists($this->_fileName) && is_file($this->_fileName)) { 1318 $this->_info['file'] = array(); 1319 $this->_info['file']['Name'] = utf8_decodeFN(\dokuwiki\Utf8\PhpString::basename($this->_fileName)); 1320 $this->_info['file']['Path'] = fullpath($this->_fileName); 1321 $this->_info['file']['Size'] = filesize($this->_fileName); 1322 if ($this->_info['file']['Size'] < 1024) { 1323 $this->_info['file']['NiceSize'] = $this->_info['file']['Size'] . 'B'; 1324 } elseif ($this->_info['file']['Size'] < (1024 * 1024)) { 1325 $this->_info['file']['NiceSize'] = round($this->_info['file']['Size'] / 1024) . 'KB'; 1326 } elseif ($this->_info['file']['Size'] < (1024 * 1024 * 1024)) { 1327 $this->_info['file']['NiceSize'] = round($this->_info['file']['Size'] / (1024*1024)) . 'MB'; 1328 } else { 1329 $this->_info['file']['NiceSize'] = $this->_info['file']['Size'] . 'B'; 1330 } 1331 $this->_info['file']['UnixTime'] = filemtime($this->_fileName); 1332 1333 // get image size directly from file 1334 if ($size = getimagesize($this->_fileName)) { 1335 $this->_info['file']['Width'] = $size[0]; 1336 $this->_info['file']['Height'] = $size[1]; 1337 1338 // set mime types and formats 1339 // http://php.net/manual/en/function.getimagesize.php 1340 // http://php.net/manual/en/function.image-type-to-mime-type.php 1341 switch ($size[2]) { 1342 case 1: 1343 $this->_info['file']['Mime'] = 'image/gif'; 1344 $this->_info['file']['Format'] = 'GIF'; 1345 break; 1346 case 2: 1347 $this->_info['file']['Mime'] = 'image/jpeg'; 1348 $this->_info['file']['Format'] = 'JPEG'; 1349 break; 1350 case 3: 1351 $this->_info['file']['Mime'] = 'image/png'; 1352 $this->_info['file']['Format'] = 'PNG'; 1353 break; 1354 case 4: 1355 $this->_info['file']['Mime'] = 'application/x-shockwave-flash'; 1356 $this->_info['file']['Format'] = 'SWF'; 1357 break; 1358 case 5: 1359 $this->_info['file']['Mime'] = 'image/psd'; 1360 $this->_info['file']['Format'] = 'PSD'; 1361 break; 1362 case 6: 1363 $this->_info['file']['Mime'] = 'image/bmp'; 1364 $this->_info['file']['Format'] = 'BMP'; 1365 break; 1366 case 7: 1367 $this->_info['file']['Mime'] = 'image/tiff'; 1368 $this->_info['file']['Format'] = 'TIFF (Intel)'; 1369 break; 1370 case 8: 1371 $this->_info['file']['Mime'] = 'image/tiff'; 1372 $this->_info['file']['Format'] = 'TIFF (Motorola)'; 1373 break; 1374 case 9: 1375 $this->_info['file']['Mime'] = 'application/octet-stream'; 1376 $this->_info['file']['Format'] = 'JPC'; 1377 break; 1378 case 10: 1379 $this->_info['file']['Mime'] = 'image/jp2'; 1380 $this->_info['file']['Format'] = 'JP2'; 1381 break; 1382 case 11: 1383 $this->_info['file']['Mime'] = 'application/octet-stream'; 1384 $this->_info['file']['Format'] = 'JPX'; 1385 break; 1386 case 12: 1387 $this->_info['file']['Mime'] = 'application/octet-stream'; 1388 $this->_info['file']['Format'] = 'JB2'; 1389 break; 1390 case 13: 1391 $this->_info['file']['Mime'] = 'application/x-shockwave-flash'; 1392 $this->_info['file']['Format'] = 'SWC'; 1393 break; 1394 case 14: 1395 $this->_info['file']['Mime'] = 'image/iff'; 1396 $this->_info['file']['Format'] = 'IFF'; 1397 break; 1398 case 15: 1399 $this->_info['file']['Mime'] = 'image/vnd.wap.wbmp'; 1400 $this->_info['file']['Format'] = 'WBMP'; 1401 break; 1402 case 16: 1403 $this->_info['file']['Mime'] = 'image/xbm'; 1404 $this->_info['file']['Format'] = 'XBM'; 1405 break; 1406 default: 1407 $this->_info['file']['Mime'] = 'image/unknown'; 1408 } 1409 } 1410 } else { 1411 $this->_info['file'] = array(); 1412 $this->_info['file']['Name'] = \dokuwiki\Utf8\PhpString::basename($this->_fileName); 1413 $this->_info['file']['Url'] = $this->_fileName; 1414 } 1415 1416 return true; 1417 } 1418 1419 /*************************************************************/ 1420 function _parseMarkerJFIF() { 1421 if (!isset($this->_markers)) { 1422 $this->_readJPEG(); 1423 } 1424 1425 if ($this->_markers == null || $this->_isMarkerDisabled(('jfif'))) { 1426 return false; 1427 } 1428 1429 try { 1430 $data = null; 1431 $count = count($this->_markers); 1432 for ($i = 0; $i < $count; $i++) { 1433 if ($this->_markers[$i]['marker'] == 0xE0 1434 && strlen($this->_markers[$i]['data']) >= 4) { 1435 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 4); 1436 if ($signature == 'JFIF') { 1437 $data =& $this->_markers[$i]['data']; 1438 break; 1439 } 1440 } 1441 } 1442 1443 if ($data == null) { 1444 $this->_info['jfif'] = false; 1445 return false; 1446 } 1447 1448 $this->_info['jfif'] = array(); 1449 1450 $vmaj = $this->_getByte($data, 5); 1451 $vmin = $this->_getByte($data, 6); 1452 1453 $this->_info['jfif']['Version'] = sprintf('%d.%02d', $vmaj, $vmin); 1454 1455 $units = $this->_getByte($data, 7); 1456 switch ($units) { 1457 case 0: 1458 $this->_info['jfif']['Units'] = 'pixels'; 1459 break; 1460 case 1: 1461 $this->_info['jfif']['Units'] = 'dpi'; 1462 break; 1463 case 2: 1464 $this->_info['jfif']['Units'] = 'dpcm'; 1465 break; 1466 default: 1467 $this->_info['jfif']['Units'] = 'unknown'; 1468 break; 1469 } 1470 1471 $xdens = $this->_getShort($data, 8); 1472 $ydens = $this->_getShort($data, 10); 1473 1474 $this->_info['jfif']['XDensity'] = $xdens; 1475 $this->_info['jfif']['YDensity'] = $ydens; 1476 1477 $thumbx = $this->_getByte($data, 12); 1478 $thumby = $this->_getByte($data, 13); 1479 1480 $this->_info['jfif']['ThumbnailWidth'] = $thumbx; 1481 $this->_info['jfif']['ThumbnailHeight'] = $thumby; 1482 } catch(Exception $e) { 1483 $this->_handleMarkerParsingException($e); 1484 $this->_info['jfif'] = false; 1485 return false; 1486 } 1487 1488 return true; 1489 } 1490 1491 /*************************************************************/ 1492 function _parseMarkerSOF() { 1493 if (!isset($this->_markers)) { 1494 $this->_readJPEG(); 1495 } 1496 1497 if ($this->_markers == null || $this->_isMarkerDisabled(('sof'))) { 1498 return false; 1499 } 1500 1501 try { 1502 $data = null; 1503 $count = count($this->_markers); 1504 for ($i = 0; $i < $count; $i++) { 1505 switch ($this->_markers[$i]['marker']) { 1506 case 0xC0: // SOF0 1507 case 0xC1: // SOF1 1508 case 0xC2: // SOF2 1509 case 0xC9: // SOF9 1510 $data =& $this->_markers[$i]['data']; 1511 $marker = $this->_markers[$i]['marker']; 1512 break; 1513 } 1514 } 1515 1516 if ($data == null) { 1517 $this->_info['sof'] = false; 1518 return false; 1519 } 1520 1521 $pos = 0; 1522 $this->_info['sof'] = array(); 1523 1524 switch ($marker) { 1525 case 0xC0: // SOF0 1526 $format = 'Baseline'; 1527 break; 1528 case 0xC1: // SOF1 1529 $format = 'Progessive'; 1530 break; 1531 case 0xC2: // SOF2 1532 $format = 'Non-baseline'; 1533 break; 1534 case 0xC9: // SOF9 1535 $format = 'Arithmetic'; 1536 break; 1537 default: 1538 return false; 1539 } 1540 1541 $this->_info['sof']['Format'] = $format; 1542 $this->_info['sof']['SamplePrecision'] = $this->_getByte($data, $pos + 0); 1543 $this->_info['sof']['ImageHeight'] = $this->_getShort($data, $pos + 1); 1544 $this->_info['sof']['ImageWidth'] = $this->_getShort($data, $pos + 3); 1545 $this->_info['sof']['ColorChannels'] = $this->_getByte($data, $pos + 5); 1546 } catch(Exception $e) { 1547 $this->_handleMarkerParsingException($e); 1548 $this->_info['sof'] = false; 1549 return false; 1550 } 1551 1552 return true; 1553 } 1554 1555 /** 1556 * Parses the XMP data 1557 * 1558 * @author Hakan Sandell <hakan.sandell@mydata.se> 1559 */ 1560 function _parseMarkerXmp() { 1561 if (!isset($this->_markers)) { 1562 $this->_readJPEG(); 1563 } 1564 1565 if ($this->_markers == null || $this->_isMarkerDisabled(('xmp'))) { 1566 return false; 1567 } 1568 1569 try { 1570 $data = null; 1571 $count = count($this->_markers); 1572 for ($i = 0; $i < $count; $i++) { 1573 if ($this->_markers[$i]['marker'] == 0xE1 1574 && strlen($this->_markers[$i]['data']) >= 29) { 1575 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 29); 1576 if ($signature == "http://ns.adobe.com/xap/1.0/\0") { 1577 $data = substr($this->_markers[$i]['data'], 29); 1578 break; 1579 } 1580 } 1581 } 1582 1583 if ($data == null) { 1584 $this->_info['xmp'] = false; 1585 return false; 1586 } 1587 1588 $parser = xml_parser_create(); 1589 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); 1590 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); 1591 $result = xml_parse_into_struct($parser, $data, $values, $tags); 1592 xml_parser_free($parser); 1593 1594 if ($result == 0) { 1595 $this->_info['xmp'] = false; 1596 return false; 1597 } 1598 1599 $this->_info['xmp'] = array(); 1600 $count = count($values); 1601 for ($i = 0; $i < $count; $i++) { 1602 if ($values[$i]['tag'] == 'rdf:Description' && $values[$i]['type'] == 'open') { 1603 1604 while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Description')) { 1605 $this->_parseXmpNode($values, $i, $this->_info['xmp'][$values[$i]['tag']], $count); 1606 } 1607 } 1608 } 1609 } catch (Exception $e) { 1610 $this->_handleMarkerParsingException($e); 1611 $this->_info['xmp'] = false; 1612 return false; 1613 } 1614 1615 return true; 1616 } 1617 1618 /** 1619 * Parses XMP nodes by recursion 1620 * 1621 * @author Hakan Sandell <hakan.sandell@mydata.se> 1622 * 1623 * @param array $values 1624 * @param int $i 1625 * @param mixed $meta 1626 * @param integer $count 1627 */ 1628 function _parseXmpNode($values, &$i, &$meta, $count) { 1629 if ($values[$i]['type'] == 'close') return; 1630 1631 if ($values[$i]['type'] == 'complete') { 1632 // Simple Type property 1633 $meta = $values[$i]['value'] ?? ''; 1634 return; 1635 } 1636 1637 $i++; 1638 if ($i >= $count) return; 1639 1640 if ($values[$i]['tag'] == 'rdf:Bag' || $values[$i]['tag'] == 'rdf:Seq') { 1641 // Array property 1642 $meta = array(); 1643 while ($values[++$i]['tag'] == 'rdf:li') { 1644 $this->_parseXmpNode($values, $i, $meta[], $count); 1645 } 1646 $i++; // skip closing Bag/Seq tag 1647 1648 } elseif ($values[$i]['tag'] == 'rdf:Alt') { 1649 // Language Alternative property, only the first (default) value is used 1650 if ($values[$i]['type'] == 'open') { 1651 $i++; 1652 $this->_parseXmpNode($values, $i, $meta, $count); 1653 while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Alt')); 1654 $i++; // skip closing Alt tag 1655 } 1656 1657 } else { 1658 // Structure property 1659 $meta = array(); 1660 $startTag = $values[$i-1]['tag']; 1661 do { 1662 $this->_parseXmpNode($values, $i, $meta[$values[$i]['tag']], $count); 1663 } while ((++$i < $count) && ($values[$i]['tag'] != $startTag)); 1664 } 1665 } 1666 1667 /*************************************************************/ 1668 function _parseMarkerExif() { 1669 if (!isset($this->_markers)) { 1670 $this->_readJPEG(); 1671 } 1672 1673 if ($this->_markers == null || $this->_isMarkerDisabled(('exif'))) { 1674 return false; 1675 } 1676 1677 try { 1678 $data = null; 1679 $count = count($this->_markers); 1680 for ($i = 0; $i < $count; $i++) { 1681 if ($this->_markers[$i]['marker'] == 0xE1 1682 && strlen($this->_markers[$i]['data']) >= 6) { 1683 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6); 1684 if ($signature == "Exif\0\0") { 1685 $data =& $this->_markers[$i]['data']; 1686 break; 1687 } 1688 } 1689 } 1690 1691 if ($data == null) { 1692 $this->_info['exif'] = false; 1693 return false; 1694 } 1695 $pos = 6; 1696 $this->_info['exif'] = array(); 1697 1698 // We don't increment $pos after this because Exif uses offsets relative to this point 1699 1700 $byteAlign = $this->_getShort($data, $pos + 0); 1701 1702 if ($byteAlign == 0x4949) { // "II" 1703 $isBigEndian = false; 1704 } elseif ($byteAlign == 0x4D4D) { // "MM" 1705 $isBigEndian = true; 1706 } else { 1707 return false; // Unexpected data 1708 } 1709 1710 $alignCheck = $this->_getShort($data, $pos + 2, $isBigEndian); 1711 if ($alignCheck != 0x002A) // That's the expected value 1712 return false; // Unexpected data 1713 1714 if ($isBigEndian) { 1715 $this->_info['exif']['ByteAlign'] = "Big Endian"; 1716 } else { 1717 $this->_info['exif']['ByteAlign'] = "Little Endian"; 1718 } 1719 1720 $offsetIFD0 = $this->_getLong($data, $pos + 4, $isBigEndian); 1721 if ($offsetIFD0 < 8) 1722 return false; // Unexpected data 1723 1724 $offsetIFD1 = $this->_readIFD($data, $pos, $offsetIFD0, $isBigEndian, 'ifd0'); 1725 if ($offsetIFD1 != 0) 1726 $this->_readIFD($data, $pos, $offsetIFD1, $isBigEndian, 'ifd1'); 1727 } catch(Exception $e) { 1728 $this->_handleMarkerParsingException($e); 1729 $this->_info['exif'] = false; 1730 return false; 1731 } 1732 1733 return true; 1734 } 1735 1736 /*************************************************************/ 1737 1738 /** 1739 * @param mixed $data 1740 * @param integer $base 1741 * @param integer $offset 1742 * @param boolean $isBigEndian 1743 * @param string $mode 1744 * 1745 * @return int 1746 */ 1747 function _readIFD($data, $base, $offset, $isBigEndian, $mode) { 1748 $EXIFTags = $this->_exifTagNames($mode); 1749 1750 $numEntries = $this->_getShort($data, $base + $offset, $isBigEndian); 1751 $offset += 2; 1752 1753 $exifTIFFOffset = 0; 1754 $exifTIFFLength = 0; 1755 $exifThumbnailOffset = 0; 1756 $exifThumbnailLength = 0; 1757 1758 for ($i = 0; $i < $numEntries; $i++) { 1759 $tag = $this->_getShort($data, $base + $offset, $isBigEndian); 1760 $offset += 2; 1761 $type = $this->_getShort($data, $base + $offset, $isBigEndian); 1762 $offset += 2; 1763 $count = $this->_getLong($data, $base + $offset, $isBigEndian); 1764 $offset += 4; 1765 1766 if (($type < 1) || ($type > 12)) 1767 return false; // Unexpected Type 1768 1769 $typeLengths = array( -1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 ); 1770 1771 $dataLength = $typeLengths[$type] * $count; 1772 if ($dataLength > 4) { 1773 $dataOffset = $this->_getLong($data, $base + $offset, $isBigEndian); 1774 $rawValue = $this->_getFixedString($data, $base + $dataOffset, $dataLength); 1775 } else { 1776 $rawValue = $this->_getFixedString($data, $base + $offset, $dataLength); 1777 } 1778 $offset += 4; 1779 1780 switch ($type) { 1781 case 1: // UBYTE 1782 if ($count == 1) { 1783 $value = $this->_getByte($rawValue, 0); 1784 } else { 1785 $value = array(); 1786 for ($j = 0; $j < $count; $j++) 1787 $value[$j] = $this->_getByte($rawValue, $j); 1788 } 1789 break; 1790 case 2: // ASCII 1791 $value = $rawValue; 1792 break; 1793 case 3: // USHORT 1794 if ($count == 1) { 1795 $value = $this->_getShort($rawValue, 0, $isBigEndian); 1796 } else { 1797 $value = array(); 1798 for ($j = 0; $j < $count; $j++) 1799 $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian); 1800 } 1801 break; 1802 case 4: // ULONG 1803 if ($count == 1) { 1804 $value = $this->_getLong($rawValue, 0, $isBigEndian); 1805 } else { 1806 $value = array(); 1807 for ($j = 0; $j < $count; $j++) 1808 $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian); 1809 } 1810 break; 1811 case 5: // URATIONAL 1812 if ($count == 1) { 1813 $a = $this->_getLong($rawValue, 0, $isBigEndian); 1814 $b = $this->_getLong($rawValue, 4, $isBigEndian); 1815 $value = array(); 1816 $value['val'] = 0; 1817 $value['num'] = $a; 1818 $value['den'] = $b; 1819 if (($a != 0) && ($b != 0)) { 1820 $value['val'] = $a / $b; 1821 } 1822 } else { 1823 $value = array(); 1824 for ($j = 0; $j < $count; $j++) { 1825 $a = $this->_getLong($rawValue, $j * 8, $isBigEndian); 1826 $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian); 1827 $value = array(); 1828 $value[$j]['val'] = 0; 1829 $value[$j]['num'] = $a; 1830 $value[$j]['den'] = $b; 1831 if (($a != 0) && ($b != 0)) 1832 $value[$j]['val'] = $a / $b; 1833 } 1834 } 1835 break; 1836 case 6: // SBYTE 1837 if ($count == 1) { 1838 $value = $this->_getByte($rawValue, 0); 1839 } else { 1840 $value = array(); 1841 for ($j = 0; $j < $count; $j++) 1842 $value[$j] = $this->_getByte($rawValue, $j); 1843 } 1844 break; 1845 case 7: // UNDEFINED 1846 $value = $rawValue; 1847 break; 1848 case 8: // SSHORT 1849 if ($count == 1) { 1850 $value = $this->_getShort($rawValue, 0, $isBigEndian); 1851 } else { 1852 $value = array(); 1853 for ($j = 0; $j < $count; $j++) 1854 $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian); 1855 } 1856 break; 1857 case 9: // SLONG 1858 if ($count == 1) { 1859 $value = $this->_getLong($rawValue, 0, $isBigEndian); 1860 } else { 1861 $value = array(); 1862 for ($j = 0; $j < $count; $j++) 1863 $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian); 1864 } 1865 break; 1866 case 10: // SRATIONAL 1867 if ($count == 1) { 1868 $a = $this->_getLong($rawValue, 0, $isBigEndian); 1869 $b = $this->_getLong($rawValue, 4, $isBigEndian); 1870 $value = array(); 1871 $value['val'] = 0; 1872 $value['num'] = $a; 1873 $value['den'] = $b; 1874 if (($a != 0) && ($b != 0)) 1875 $value['val'] = $a / $b; 1876 } else { 1877 $value = array(); 1878 for ($j = 0; $j < $count; $j++) { 1879 $a = $this->_getLong($rawValue, $j * 8, $isBigEndian); 1880 $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian); 1881 $value = array(); 1882 $value[$j]['val'] = 0; 1883 $value[$j]['num'] = $a; 1884 $value[$j]['den'] = $b; 1885 if (($a != 0) && ($b != 0)) 1886 $value[$j]['val'] = $a / $b; 1887 } 1888 } 1889 break; 1890 case 11: // FLOAT 1891 $value = $rawValue; 1892 break; 1893 1894 case 12: // DFLOAT 1895 $value = $rawValue; 1896 break; 1897 default: 1898 return false; // Unexpected Type 1899 } 1900 1901 $tagName = ''; 1902 if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset 1903 $this->_readIFD($data, $base, $value, $isBigEndian, 'exif'); 1904 } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset 1905 $this->_readIFD($data, $base, $value, $isBigEndian, 'gps'); 1906 } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets 1907 $exifTIFFOffset = $value; 1908 } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts 1909 $exifTIFFLength = $value; 1910 } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset 1911 $exifThumbnailOffset = $value; 1912 } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength 1913 $exifThumbnailLength = $value; 1914 } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset 1915 $this->_readIFD($data, $base, $value, $isBigEndian, 'interop'); 1916 } 1917 // elseif (($mode == 'exif') && ($tag == 0x927C)) { // MakerNote 1918 // } 1919 else { 1920 if (isset($EXIFTags[$tag])) { 1921 $tagName = $EXIFTags[$tag]; 1922 if (isset($this->_info['exif'][$tagName])) { 1923 if (!is_array($this->_info['exif'][$tagName])) { 1924 $aux = array(); 1925 $aux[0] = $this->_info['exif'][$tagName]; 1926 $this->_info['exif'][$tagName] = $aux; 1927 } 1928 1929 $this->_info['exif'][$tagName][count($this->_info['exif'][$tagName])] = $value; 1930 } else { 1931 $this->_info['exif'][$tagName] = $value; 1932 } 1933 } 1934 /* 1935 else { 1936 echo sprintf("<h1>Unknown tag %02x (t: %d l: %d) %s in %s</h1>", $tag, $type, $count, $mode, $this->_fileName); 1937 // Unknown Tags will be ignored!!! 1938 // That's because the tag might be a pointer (like the Exif tag) 1939 // and saving it without saving the data it points to might 1940 // create an invalid file. 1941 } 1942 */ 1943 } 1944 } 1945 1946 if (($exifThumbnailOffset > 0) && ($exifThumbnailLength > 0)) { 1947 $this->_info['exif']['JFIFThumbnail'] = $this->_getFixedString($data, $base + $exifThumbnailOffset, $exifThumbnailLength); 1948 } 1949 1950 if (($exifTIFFOffset > 0) && ($exifTIFFLength > 0)) { 1951 $this->_info['exif']['TIFFStrips'] = $this->_getFixedString($data, $base + $exifTIFFOffset, $exifTIFFLength); 1952 } 1953 1954 $nextOffset = $this->_getLong($data, $base + $offset, $isBigEndian); 1955 return $nextOffset; 1956 } 1957 1958 /*************************************************************/ 1959 function & _createMarkerExif() { 1960 $data = null; 1961 $count = count($this->_markers); 1962 for ($i = 0; $i < $count; $i++) { 1963 if ($this->_markers[$i]['marker'] == 0xE1 1964 && strlen($this->_markers[$i]['data']) >= 6) { 1965 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6); 1966 if ($signature == "Exif\0\0") { 1967 $data =& $this->_markers[$i]['data']; 1968 break; 1969 } 1970 } 1971 } 1972 1973 if (!isset($this->_info['exif'])) { 1974 return false; 1975 } 1976 1977 $data = "Exif\0\0"; 1978 $pos = 6; 1979 $offsetBase = 6; 1980 1981 if (isset($this->_info['exif']['ByteAlign']) && ($this->_info['exif']['ByteAlign'] == "Big Endian")) { 1982 $isBigEndian = true; 1983 $aux = "MM"; 1984 $pos = $this->_putString($data, $pos, $aux); 1985 } else { 1986 $isBigEndian = false; 1987 $aux = "II"; 1988 $pos = $this->_putString($data, $pos, $aux); 1989 } 1990 $pos = $this->_putShort($data, $pos, 0x002A, $isBigEndian); 1991 $pos = $this->_putLong($data, $pos, 0x00000008, $isBigEndian); // IFD0 Offset is always 8 1992 1993 $ifd0 =& $this->_getIFDEntries($isBigEndian, 'ifd0'); 1994 $ifd1 =& $this->_getIFDEntries($isBigEndian, 'ifd1'); 1995 1996 $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd0, $isBigEndian, true); 1997 $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd1, $isBigEndian, false); 1998 1999 return $data; 2000 } 2001 2002 /*************************************************************/ 2003 2004 /** 2005 * @param mixed $data 2006 * @param integer $pos 2007 * @param integer $offsetBase 2008 * @param array $entries 2009 * @param boolean $isBigEndian 2010 * @param boolean $hasNext 2011 * 2012 * @return mixed 2013 */ 2014 function _writeIFD(&$data, $pos, $offsetBase, &$entries, $isBigEndian, $hasNext) { 2015 $tiffData = null; 2016 $tiffDataOffsetPos = -1; 2017 2018 $entryCount = count($entries); 2019 2020 $dataPos = $pos + 2 + ($entryCount * 12) + 4; 2021 $pos = $this->_putShort($data, $pos, $entryCount, $isBigEndian); 2022 2023 for ($i = 0; $i < $entryCount; $i++) { 2024 $tag = $entries[$i]['tag']; 2025 $type = $entries[$i]['type']; 2026 2027 if ($type == -99) { // SubIFD 2028 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2029 $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG 2030 $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1 2031 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2032 2033 $dataPos = $this->_writeIFD($data, $dataPos, $offsetBase, $entries[$i]['value'], $isBigEndian, false); 2034 } elseif ($type == -98) { // TIFF Data 2035 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2036 $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG 2037 $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1 2038 $tiffDataOffsetPos = $pos; 2039 $pos = $this->_putLong($data, $pos, 0x00, $isBigEndian); // For Now 2040 $tiffData =& $entries[$i]['value'] ; 2041 } else { // Regular Entry 2042 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2043 $pos = $this->_putShort($data, $pos, $type, $isBigEndian); 2044 $pos = $this->_putLong($data, $pos, $entries[$i]['count'], $isBigEndian); 2045 if (strlen($entries[$i]['value']) > 4) { 2046 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2047 $dataPos = $this->_putString($data, $dataPos, $entries[$i]['value']); 2048 } else { 2049 $val = str_pad($entries[$i]['value'], 4, "\0"); 2050 $pos = $this->_putString($data, $pos, $val); 2051 } 2052 } 2053 } 2054 2055 if ($tiffData != null) { 2056 $this->_putLong($data, $tiffDataOffsetPos, $dataPos - $offsetBase, $isBigEndian); 2057 $dataPos = $this->_putString($data, $dataPos, $tiffData); 2058 } 2059 2060 if ($hasNext) { 2061 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2062 } else { 2063 $pos = $this->_putLong($data, $pos, 0, $isBigEndian); 2064 } 2065 2066 return $dataPos; 2067 } 2068 2069 /*************************************************************/ 2070 2071 /** 2072 * @param boolean $isBigEndian 2073 * @param string $mode 2074 * 2075 * @return array 2076 */ 2077 function & _getIFDEntries($isBigEndian, $mode) { 2078 $EXIFNames = $this->_exifTagNames($mode); 2079 $EXIFTags = $this->_exifNameTags($mode); 2080 $EXIFTypeInfo = $this->_exifTagTypes($mode); 2081 2082 $ifdEntries = array(); 2083 $entryCount = 0; 2084 2085 foreach($EXIFNames as $tag => $name) { 2086 $type = $EXIFTypeInfo[$tag][0]; 2087 $count = $EXIFTypeInfo[$tag][1]; 2088 $value = null; 2089 2090 if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset 2091 if (isset($this->_info['exif']['EXIFVersion'])) { 2092 $value =& $this->_getIFDEntries($isBigEndian, "exif"); 2093 $type = -99; 2094 } 2095 else { 2096 $value = null; 2097 } 2098 } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset 2099 if (isset($this->_info['exif']['GPSVersionID'])) { 2100 $value =& $this->_getIFDEntries($isBigEndian, "gps"); 2101 $type = -99; 2102 } else { 2103 $value = null; 2104 } 2105 } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets 2106 if (isset($this->_info['exif']['TIFFStrips'])) { 2107 $value =& $this->_info['exif']['TIFFStrips']; 2108 $type = -98; 2109 } else { 2110 $value = null; 2111 } 2112 } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts 2113 if (isset($this->_info['exif']['TIFFStrips'])) { 2114 $value = strlen($this->_info['exif']['TIFFStrips']); 2115 } else { 2116 $value = null; 2117 } 2118 } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset 2119 if (isset($this->_info['exif']['JFIFThumbnail'])) { 2120 $value =& $this->_info['exif']['JFIFThumbnail']; 2121 $type = -98; 2122 } else { 2123 $value = null; 2124 } 2125 } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength 2126 if (isset($this->_info['exif']['JFIFThumbnail'])) { 2127 $value = strlen($this->_info['exif']['JFIFThumbnail']); 2128 } else { 2129 $value = null; 2130 } 2131 } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset 2132 if (isset($this->_info['exif']['InteroperabilityIndex'])) { 2133 $value =& $this->_getIFDEntries($isBigEndian, "interop"); 2134 $type = -99; 2135 } else { 2136 $value = null; 2137 } 2138 } elseif (isset($this->_info['exif'][$name])) { 2139 $origValue =& $this->_info['exif'][$name]; 2140 2141 // This makes it easier to process variable size elements 2142 if (!is_array($origValue) || isset($origValue['val'])) { 2143 unset($origValue); // Break the reference 2144 $origValue = array($this->_info['exif'][$name]); 2145 } 2146 $origCount = count($origValue); 2147 2148 if ($origCount == 0 ) { 2149 $type = -1; // To ignore this field 2150 } 2151 2152 $value = " "; 2153 2154 switch ($type) { 2155 case 1: // UBYTE 2156 if ($count == 0) { 2157 $count = $origCount; 2158 } 2159 2160 $j = 0; 2161 while (($j < $count) && ($j < $origCount)) { 2162 2163 $this->_putByte($value, $j, $origValue[$j]); 2164 $j++; 2165 } 2166 2167 while ($j < $count) { 2168 $this->_putByte($value, $j, 0); 2169 $j++; 2170 } 2171 break; 2172 case 2: // ASCII 2173 $v = strval($origValue[0]); 2174 if (($count != 0) && (strlen($v) > $count)) { 2175 $v = substr($v, 0, $count); 2176 } 2177 elseif (($count > 0) && (strlen($v) < $count)) { 2178 $v = str_pad($v, $count, "\0"); 2179 } 2180 2181 $count = strlen($v); 2182 2183 $this->_putString($value, 0, $v); 2184 break; 2185 case 3: // USHORT 2186 if ($count == 0) { 2187 $count = $origCount; 2188 } 2189 2190 $j = 0; 2191 while (($j < $count) && ($j < $origCount)) { 2192 $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian); 2193 $j++; 2194 } 2195 2196 while ($j < $count) { 2197 $this->_putShort($value, $j * 2, 0, $isBigEndian); 2198 $j++; 2199 } 2200 break; 2201 case 4: // ULONG 2202 if ($count == 0) { 2203 $count = $origCount; 2204 } 2205 2206 $j = 0; 2207 while (($j < $count) && ($j < $origCount)) { 2208 $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian); 2209 $j++; 2210 } 2211 2212 while ($j < $count) { 2213 $this->_putLong($value, $j * 4, 0, $isBigEndian); 2214 $j++; 2215 } 2216 break; 2217 case 5: // URATIONAL 2218 if ($count == 0) { 2219 $count = $origCount; 2220 } 2221 2222 $j = 0; 2223 while (($j < $count) && ($j < $origCount)) { 2224 $v = $origValue[$j]; 2225 if (is_array($v)) { 2226 $a = $v['num']; 2227 $b = $v['den']; 2228 } 2229 else { 2230 $a = 0; 2231 $b = 0; 2232 // TODO: Allow other types and convert them 2233 } 2234 $this->_putLong($value, $j * 8, $a, $isBigEndian); 2235 $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian); 2236 $j++; 2237 } 2238 2239 while ($j < $count) { 2240 $this->_putLong($value, $j * 8, 0, $isBigEndian); 2241 $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian); 2242 $j++; 2243 } 2244 break; 2245 case 6: // SBYTE 2246 if ($count == 0) { 2247 $count = $origCount; 2248 } 2249 2250 $j = 0; 2251 while (($j < $count) && ($j < $origCount)) { 2252 $this->_putByte($value, $j, $origValue[$j]); 2253 $j++; 2254 } 2255 2256 while ($j < $count) { 2257 $this->_putByte($value, $j, 0); 2258 $j++; 2259 } 2260 break; 2261 case 7: // UNDEFINED 2262 $v = strval($origValue[0]); 2263 if (($count != 0) && (strlen($v) > $count)) { 2264 $v = substr($v, 0, $count); 2265 } 2266 elseif (($count > 0) && (strlen($v) < $count)) { 2267 $v = str_pad($v, $count, "\0"); 2268 } 2269 2270 $count = strlen($v); 2271 2272 $this->_putString($value, 0, $v); 2273 break; 2274 case 8: // SSHORT 2275 if ($count == 0) { 2276 $count = $origCount; 2277 } 2278 2279 $j = 0; 2280 while (($j < $count) && ($j < $origCount)) { 2281 $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian); 2282 $j++; 2283 } 2284 2285 while ($j < $count) { 2286 $this->_putShort($value, $j * 2, 0, $isBigEndian); 2287 $j++; 2288 } 2289 break; 2290 case 9: // SLONG 2291 if ($count == 0) { 2292 $count = $origCount; 2293 } 2294 2295 $j = 0; 2296 while (($j < $count) && ($j < $origCount)) { 2297 $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian); 2298 $j++; 2299 } 2300 2301 while ($j < $count) { 2302 $this->_putLong($value, $j * 4, 0, $isBigEndian); 2303 $j++; 2304 } 2305 break; 2306 case 10: // SRATIONAL 2307 if ($count == 0) { 2308 $count = $origCount; 2309 } 2310 2311 $j = 0; 2312 while (($j < $count) && ($j < $origCount)) { 2313 $v = $origValue[$j]; 2314 if (is_array($v)) { 2315 $a = $v['num']; 2316 $b = $v['den']; 2317 } 2318 else { 2319 $a = 0; 2320 $b = 0; 2321 // TODO: Allow other types and convert them 2322 } 2323 2324 $this->_putLong($value, $j * 8, $a, $isBigEndian); 2325 $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian); 2326 $j++; 2327 } 2328 2329 while ($j < $count) { 2330 $this->_putLong($value, $j * 8, 0, $isBigEndian); 2331 $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian); 2332 $j++; 2333 } 2334 break; 2335 case 11: // FLOAT 2336 if ($count == 0) { 2337 $count = $origCount; 2338 } 2339 2340 $j = 0; 2341 while (($j < $count) && ($j < $origCount)) { 2342 $v = strval($origValue[$j]); 2343 if (strlen($v) > 4) { 2344 $v = substr($v, 0, 4); 2345 } 2346 elseif (strlen($v) < 4) { 2347 $v = str_pad($v, 4, "\0"); 2348 } 2349 $this->_putString($value, $j * 4, $v); 2350 $j++; 2351 } 2352 2353 while ($j < $count) { 2354 $v = "\0\0\0\0"; 2355 $this->_putString($value, $j * 4, $v); 2356 $j++; 2357 } 2358 break; 2359 case 12: // DFLOAT 2360 if ($count == 0) { 2361 $count = $origCount; 2362 } 2363 2364 $j = 0; 2365 while (($j < $count) && ($j < $origCount)) { 2366 $v = strval($origValue[$j]); 2367 if (strlen($v) > 8) { 2368 $v = substr($v, 0, 8); 2369 } 2370 elseif (strlen($v) < 8) { 2371 $v = str_pad($v, 8, "\0"); 2372 } 2373 $this->_putString($value, $j * 8, $v); 2374 $j++; 2375 } 2376 2377 while ($j < $count) { 2378 $v = "\0\0\0\0\0\0\0\0"; 2379 $this->_putString($value, $j * 8, $v); 2380 $j++; 2381 } 2382 break; 2383 default: 2384 $value = null; 2385 break; 2386 } 2387 } 2388 2389 if ($value != null) { 2390 $ifdEntries[$entryCount] = array(); 2391 $ifdEntries[$entryCount]['tag'] = $tag; 2392 $ifdEntries[$entryCount]['type'] = $type; 2393 $ifdEntries[$entryCount]['count'] = $count; 2394 $ifdEntries[$entryCount]['value'] = $value; 2395 2396 $entryCount++; 2397 } 2398 } 2399 2400 return $ifdEntries; 2401 } 2402 /*************************************************************/ 2403 function _handleMarkerParsingException($e) { 2404 \dokuwiki\ErrorHandler::logException($e, $this->_fileName); 2405 } 2406 2407 /*************************************************************/ 2408 function _isMarkerDisabled($name) { 2409 if (!isset($this->_info)) return false; 2410 return isset($this->_info[$name]) && $this->_info[$name] === false; 2411 } 2412 2413 /*************************************************************/ 2414 function _parseMarkerAdobe() { 2415 if (!isset($this->_markers)) { 2416 $this->_readJPEG(); 2417 } 2418 2419 if ($this->_markers == null || $this->_isMarkerDisabled('adobe')) { 2420 return false; 2421 } 2422 try { 2423 $data = null; 2424 $count = count($this->_markers); 2425 for ($i = 0; $i < $count; $i++) { 2426 if ($this->_markers[$i]['marker'] == 0xED 2427 && strlen($this->_markers[$i]['data']) >= 14) { 2428 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 14); 2429 if ($signature == "Photoshop 3.0\0") { 2430 $data =& $this->_markers[$i]['data']; 2431 break; 2432 } 2433 } 2434 } 2435 2436 if ($data == null) { 2437 $this->_info['adobe'] = false; 2438 $this->_info['iptc'] = false; 2439 return false; 2440 } 2441 $pos = 14; 2442 $this->_info['adobe'] = array(); 2443 $this->_info['adobe']['raw'] = array(); 2444 $this->_info['iptc'] = array(); 2445 2446 $datasize = strlen($data); 2447 2448 while ($pos < $datasize) { 2449 $signature = $this->_getFixedString($data, $pos, 4); 2450 if ($signature != '8BIM') 2451 return false; 2452 $pos += 4; 2453 2454 $type = $this->_getShort($data, $pos); 2455 $pos += 2; 2456 2457 $strlen = $this->_getByte($data, $pos); 2458 $pos += 1; 2459 $header = ''; 2460 for ($i = 0; $i < $strlen; $i++) { 2461 $header .= $data[$pos + $i]; 2462 } 2463 $pos += $strlen + 1 - ($strlen % 2); // The string is padded to even length, counting the length byte itself 2464 2465 $length = $this->_getLong($data, $pos); 2466 $pos += 4; 2467 2468 $basePos = $pos; 2469 2470 switch ($type) { 2471 case 0x0404: // Caption (IPTC Data) 2472 $pos = $this->_readIPTC($data, $pos); 2473 if ($pos == false) 2474 return false; 2475 break; 2476 case 0x040A: // CopyrightFlag 2477 $this->_info['adobe']['CopyrightFlag'] = $this->_getByte($data, $pos); 2478 $pos += $length; 2479 break; 2480 case 0x040B: // ImageURL 2481 $this->_info['adobe']['ImageURL'] = $this->_getFixedString($data, $pos, $length); 2482 $pos += $length; 2483 break; 2484 case 0x040C: // Thumbnail 2485 $aux = $this->_getLong($data, $pos); 2486 $pos += 4; 2487 if ($aux == 1) { 2488 $this->_info['adobe']['ThumbnailWidth'] = $this->_getLong($data, $pos); 2489 $pos += 4; 2490 $this->_info['adobe']['ThumbnailHeight'] = $this->_getLong($data, $pos); 2491 $pos += 4; 2492 2493 $pos += 16; // Skip some data 2494 2495 $this->_info['adobe']['ThumbnailData'] = $this->_getFixedString($data, $pos, $length - 28); 2496 $pos += $length - 28; 2497 } 2498 break; 2499 default: 2500 break; 2501 } 2502 2503 // We save all blocks, even those we recognized 2504 $label = sprintf('8BIM_0x%04x', $type); 2505 $this->_info['adobe']['raw'][$label] = array(); 2506 $this->_info['adobe']['raw'][$label]['type'] = $type; 2507 $this->_info['adobe']['raw'][$label]['header'] = $header; 2508 $this->_info['adobe']['raw'][$label]['data'] =& $this->_getFixedString($data, $basePos, $length); 2509 2510 $pos = $basePos + $length + ($length % 2); // Even padding 2511 } 2512 } catch(Exception $e) { 2513 $this->_handleMarkerParsingException($e); 2514 $this->_info['adobe'] = false; 2515 $this->_info['iptc'] = false; 2516 return false; 2517 } 2518 } 2519 2520 /*************************************************************/ 2521 function _readIPTC(&$data, $pos = 0) { 2522 $totalLength = strlen($data); 2523 2524 $IPTCTags = $this->_iptcTagNames(); 2525 2526 while ($pos < ($totalLength - 5)) { 2527 $signature = $this->_getShort($data, $pos); 2528 if ($signature != 0x1C02) 2529 return $pos; 2530 $pos += 2; 2531 2532 $type = $this->_getByte($data, $pos); 2533 $pos += 1; 2534 $length = $this->_getShort($data, $pos); 2535 $pos += 2; 2536 2537 $basePos = $pos; 2538 $label = ''; 2539 2540 if (isset($IPTCTags[$type])) { 2541 $label = $IPTCTags[$type]; 2542 } else { 2543 $label = sprintf('IPTC_0x%02x', $type); 2544 } 2545 2546 if ($label != '') { 2547 if (isset($this->_info['iptc'][$label])) { 2548 if (!is_array($this->_info['iptc'][$label])) { 2549 $aux = array(); 2550 $aux[0] = $this->_info['iptc'][$label]; 2551 $this->_info['iptc'][$label] = $aux; 2552 } 2553 $this->_info['iptc'][$label][ count($this->_info['iptc'][$label]) ] = $this->_getFixedString($data, $pos, $length); 2554 } else { 2555 $this->_info['iptc'][$label] = $this->_getFixedString($data, $pos, $length); 2556 } 2557 } 2558 2559 $pos = $basePos + $length; // No padding 2560 } 2561 return $pos; 2562 } 2563 2564 /*************************************************************/ 2565 function & _createMarkerAdobe() { 2566 if (isset($this->_info['iptc'])) { 2567 if (!isset($this->_info['adobe'])) { 2568 $this->_info['adobe'] = array(); 2569 } 2570 if (!isset($this->_info['adobe']['raw'])) { 2571 $this->_info['adobe']['raw'] = array(); 2572 } 2573 if (!isset($this->_info['adobe']['raw']['8BIM_0x0404'])) { 2574 $this->_info['adobe']['raw']['8BIM_0x0404'] = array(); 2575 } 2576 $this->_info['adobe']['raw']['8BIM_0x0404']['type'] = 0x0404; 2577 $this->_info['adobe']['raw']['8BIM_0x0404']['header'] = "Caption"; 2578 $this->_info['adobe']['raw']['8BIM_0x0404']['data'] =& $this->_writeIPTC(); 2579 } 2580 2581 if (isset($this->_info['adobe']['raw']) && (count($this->_info['adobe']['raw']) > 0)) { 2582 $data = "Photoshop 3.0\0"; 2583 $pos = 14; 2584 2585 reset($this->_info['adobe']['raw']); 2586 foreach ($this->_info['adobe']['raw'] as $value){ 2587 $pos = $this->_write8BIM( 2588 $data, 2589 $pos, 2590 $value['type'], 2591 $value['header'], 2592 $value['data'] ); 2593 } 2594 } 2595 2596 return $data; 2597 } 2598 2599 /*************************************************************/ 2600 2601 /** 2602 * @param mixed $data 2603 * @param integer $pos 2604 * 2605 * @param string $type 2606 * @param string $header 2607 * @param mixed $value 2608 * 2609 * @return int|mixed 2610 */ 2611 function _write8BIM(&$data, $pos, $type, $header, &$value) { 2612 $signature = "8BIM"; 2613 2614 $pos = $this->_putString($data, $pos, $signature); 2615 $pos = $this->_putShort($data, $pos, $type); 2616 2617 $len = strlen($header); 2618 2619 $pos = $this->_putByte($data, $pos, $len); 2620 $pos = $this->_putString($data, $pos, $header); 2621 if (($len % 2) == 0) { // Even padding, including the length byte 2622 $pos = $this->_putByte($data, $pos, 0); 2623 } 2624 2625 $len = strlen($value); 2626 $pos = $this->_putLong($data, $pos, $len); 2627 $pos = $this->_putString($data, $pos, $value); 2628 if (($len % 2) != 0) { // Even padding 2629 $pos = $this->_putByte($data, $pos, 0); 2630 } 2631 return $pos; 2632 } 2633 2634 /*************************************************************/ 2635 function & _writeIPTC() { 2636 $data = " "; 2637 $pos = 0; 2638 2639 $IPTCNames =& $this->_iptcNameTags(); 2640 2641 foreach($this->_info['iptc'] as $label => $value) { 2642 $value =& $this->_info['iptc'][$label]; 2643 $type = -1; 2644 2645 if (isset($IPTCNames[$label])) { 2646 $type = $IPTCNames[$label]; 2647 } 2648 elseif (str_starts_with($label, 'IPTC_0x')) { 2649 $type = hexdec(substr($label, 7, 2)); 2650 } 2651 2652 if ($type != -1) { 2653 if (is_array($value)) { 2654 $vcnt = count($value); 2655 for ($i = 0; $i < $vcnt; $i++) { 2656 $pos = $this->_writeIPTCEntry($data, $pos, $type, $value[$i]); 2657 } 2658 } 2659 else { 2660 $pos = $this->_writeIPTCEntry($data, $pos, $type, $value); 2661 } 2662 } 2663 } 2664 2665 return $data; 2666 } 2667 2668 /*************************************************************/ 2669 2670 /** 2671 * @param mixed $data 2672 * @param integer $pos 2673 * 2674 * @param string $type 2675 * @param mixed $value 2676 * 2677 * @return int|mixed 2678 */ 2679 function _writeIPTCEntry(&$data, $pos, $type, &$value) { 2680 $pos = $this->_putShort($data, $pos, 0x1C02); 2681 $pos = $this->_putByte($data, $pos, $type); 2682 $pos = $this->_putShort($data, $pos, strlen($value)); 2683 $pos = $this->_putString($data, $pos, $value); 2684 2685 return $pos; 2686 } 2687 2688 /*************************************************************/ 2689 function _exifTagNames($mode) { 2690 $tags = array(); 2691 2692 if ($mode == 'ifd0') { 2693 $tags[0x010E] = 'ImageDescription'; 2694 $tags[0x010F] = 'Make'; 2695 $tags[0x0110] = 'Model'; 2696 $tags[0x0112] = 'Orientation'; 2697 $tags[0x011A] = 'XResolution'; 2698 $tags[0x011B] = 'YResolution'; 2699 $tags[0x0128] = 'ResolutionUnit'; 2700 $tags[0x0131] = 'Software'; 2701 $tags[0x0132] = 'DateTime'; 2702 $tags[0x013B] = 'Artist'; 2703 $tags[0x013E] = 'WhitePoint'; 2704 $tags[0x013F] = 'PrimaryChromaticities'; 2705 $tags[0x0211] = 'YCbCrCoefficients'; 2706 $tags[0x0212] = 'YCbCrSubSampling'; 2707 $tags[0x0213] = 'YCbCrPositioning'; 2708 $tags[0x0214] = 'ReferenceBlackWhite'; 2709 $tags[0x8298] = 'Copyright'; 2710 $tags[0x8769] = 'ExifIFDOffset'; 2711 $tags[0x8825] = 'GPSIFDOffset'; 2712 } 2713 if ($mode == 'ifd1') { 2714 $tags[0x00FE] = 'TIFFNewSubfileType'; 2715 $tags[0x00FF] = 'TIFFSubfileType'; 2716 $tags[0x0100] = 'TIFFImageWidth'; 2717 $tags[0x0101] = 'TIFFImageHeight'; 2718 $tags[0x0102] = 'TIFFBitsPerSample'; 2719 $tags[0x0103] = 'TIFFCompression'; 2720 $tags[0x0106] = 'TIFFPhotometricInterpretation'; 2721 $tags[0x0107] = 'TIFFThreshholding'; 2722 $tags[0x0108] = 'TIFFCellWidth'; 2723 $tags[0x0109] = 'TIFFCellLength'; 2724 $tags[0x010A] = 'TIFFFillOrder'; 2725 $tags[0x010E] = 'TIFFImageDescription'; 2726 $tags[0x010F] = 'TIFFMake'; 2727 $tags[0x0110] = 'TIFFModel'; 2728 $tags[0x0111] = 'TIFFStripOffsets'; 2729 $tags[0x0112] = 'TIFFOrientation'; 2730 $tags[0x0115] = 'TIFFSamplesPerPixel'; 2731 $tags[0x0116] = 'TIFFRowsPerStrip'; 2732 $tags[0x0117] = 'TIFFStripByteCounts'; 2733 $tags[0x0118] = 'TIFFMinSampleValue'; 2734 $tags[0x0119] = 'TIFFMaxSampleValue'; 2735 $tags[0x011A] = 'TIFFXResolution'; 2736 $tags[0x011B] = 'TIFFYResolution'; 2737 $tags[0x011C] = 'TIFFPlanarConfiguration'; 2738 $tags[0x0122] = 'TIFFGrayResponseUnit'; 2739 $tags[0x0123] = 'TIFFGrayResponseCurve'; 2740 $tags[0x0128] = 'TIFFResolutionUnit'; 2741 $tags[0x0131] = 'TIFFSoftware'; 2742 $tags[0x0132] = 'TIFFDateTime'; 2743 $tags[0x013B] = 'TIFFArtist'; 2744 $tags[0x013C] = 'TIFFHostComputer'; 2745 $tags[0x0140] = 'TIFFColorMap'; 2746 $tags[0x0152] = 'TIFFExtraSamples'; 2747 $tags[0x0201] = 'TIFFJFIFOffset'; 2748 $tags[0x0202] = 'TIFFJFIFLength'; 2749 $tags[0x0211] = 'TIFFYCbCrCoefficients'; 2750 $tags[0x0212] = 'TIFFYCbCrSubSampling'; 2751 $tags[0x0213] = 'TIFFYCbCrPositioning'; 2752 $tags[0x0214] = 'TIFFReferenceBlackWhite'; 2753 $tags[0x8298] = 'TIFFCopyright'; 2754 $tags[0x9286] = 'TIFFUserComment'; 2755 } elseif ($mode == 'exif') { 2756 $tags[0x829A] = 'ExposureTime'; 2757 $tags[0x829D] = 'FNumber'; 2758 $tags[0x8822] = 'ExposureProgram'; 2759 $tags[0x8824] = 'SpectralSensitivity'; 2760 $tags[0x8827] = 'ISOSpeedRatings'; 2761 $tags[0x8828] = 'OECF'; 2762 $tags[0x9000] = 'EXIFVersion'; 2763 $tags[0x9003] = 'DateTimeOriginal'; 2764 $tags[0x9004] = 'DateTimeDigitized'; 2765 $tags[0x9101] = 'ComponentsConfiguration'; 2766 $tags[0x9102] = 'CompressedBitsPerPixel'; 2767 $tags[0x9201] = 'ShutterSpeedValue'; 2768 $tags[0x9202] = 'ApertureValue'; 2769 $tags[0x9203] = 'BrightnessValue'; 2770 $tags[0x9204] = 'ExposureBiasValue'; 2771 $tags[0x9205] = 'MaxApertureValue'; 2772 $tags[0x9206] = 'SubjectDistance'; 2773 $tags[0x9207] = 'MeteringMode'; 2774 $tags[0x9208] = 'LightSource'; 2775 $tags[0x9209] = 'Flash'; 2776 $tags[0x920A] = 'FocalLength'; 2777 $tags[0x927C] = 'MakerNote'; 2778 $tags[0x9286] = 'UserComment'; 2779 $tags[0x9290] = 'SubSecTime'; 2780 $tags[0x9291] = 'SubSecTimeOriginal'; 2781 $tags[0x9292] = 'SubSecTimeDigitized'; 2782 $tags[0xA000] = 'FlashPixVersion'; 2783 $tags[0xA001] = 'ColorSpace'; 2784 $tags[0xA002] = 'PixelXDimension'; 2785 $tags[0xA003] = 'PixelYDimension'; 2786 $tags[0xA004] = 'RelatedSoundFile'; 2787 $tags[0xA005] = 'InteropIFDOffset'; 2788 $tags[0xA20B] = 'FlashEnergy'; 2789 $tags[0xA20C] = 'SpatialFrequencyResponse'; 2790 $tags[0xA20E] = 'FocalPlaneXResolution'; 2791 $tags[0xA20F] = 'FocalPlaneYResolution'; 2792 $tags[0xA210] = 'FocalPlaneResolutionUnit'; 2793 $tags[0xA214] = 'SubjectLocation'; 2794 $tags[0xA215] = 'ExposureIndex'; 2795 $tags[0xA217] = 'SensingMethod'; 2796 $tags[0xA300] = 'FileSource'; 2797 $tags[0xA301] = 'SceneType'; 2798 $tags[0xA302] = 'CFAPattern'; 2799 } elseif ($mode == 'interop') { 2800 $tags[0x0001] = 'InteroperabilityIndex'; 2801 $tags[0x0002] = 'InteroperabilityVersion'; 2802 $tags[0x1000] = 'RelatedImageFileFormat'; 2803 $tags[0x1001] = 'RelatedImageWidth'; 2804 $tags[0x1002] = 'RelatedImageLength'; 2805 } elseif ($mode == 'gps') { 2806 $tags[0x0000] = 'GPSVersionID'; 2807 $tags[0x0001] = 'GPSLatitudeRef'; 2808 $tags[0x0002] = 'GPSLatitude'; 2809 $tags[0x0003] = 'GPSLongitudeRef'; 2810 $tags[0x0004] = 'GPSLongitude'; 2811 $tags[0x0005] = 'GPSAltitudeRef'; 2812 $tags[0x0006] = 'GPSAltitude'; 2813 $tags[0x0007] = 'GPSTimeStamp'; 2814 $tags[0x0008] = 'GPSSatellites'; 2815 $tags[0x0009] = 'GPSStatus'; 2816 $tags[0x000A] = 'GPSMeasureMode'; 2817 $tags[0x000B] = 'GPSDOP'; 2818 $tags[0x000C] = 'GPSSpeedRef'; 2819 $tags[0x000D] = 'GPSSpeed'; 2820 $tags[0x000E] = 'GPSTrackRef'; 2821 $tags[0x000F] = 'GPSTrack'; 2822 $tags[0x0010] = 'GPSImgDirectionRef'; 2823 $tags[0x0011] = 'GPSImgDirection'; 2824 $tags[0x0012] = 'GPSMapDatum'; 2825 $tags[0x0013] = 'GPSDestLatitudeRef'; 2826 $tags[0x0014] = 'GPSDestLatitude'; 2827 $tags[0x0015] = 'GPSDestLongitudeRef'; 2828 $tags[0x0016] = 'GPSDestLongitude'; 2829 $tags[0x0017] = 'GPSDestBearingRef'; 2830 $tags[0x0018] = 'GPSDestBearing'; 2831 $tags[0x0019] = 'GPSDestDistanceRef'; 2832 $tags[0x001A] = 'GPSDestDistance'; 2833 } 2834 2835 return $tags; 2836 } 2837 2838 /*************************************************************/ 2839 function _exifTagTypes($mode) { 2840 $tags = array(); 2841 2842 if ($mode == 'ifd0') { 2843 $tags[0x010E] = array(2, 0); // ImageDescription -> ASCII, Any 2844 $tags[0x010F] = array(2, 0); // Make -> ASCII, Any 2845 $tags[0x0110] = array(2, 0); // Model -> ASCII, Any 2846 $tags[0x0112] = array(3, 1); // Orientation -> SHORT, 1 2847 $tags[0x011A] = array(5, 1); // XResolution -> RATIONAL, 1 2848 $tags[0x011B] = array(5, 1); // YResolution -> RATIONAL, 1 2849 $tags[0x0128] = array(3, 1); // ResolutionUnit -> SHORT 2850 $tags[0x0131] = array(2, 0); // Software -> ASCII, Any 2851 $tags[0x0132] = array(2, 20); // DateTime -> ASCII, 20 2852 $tags[0x013B] = array(2, 0); // Artist -> ASCII, Any 2853 $tags[0x013E] = array(5, 2); // WhitePoint -> RATIONAL, 2 2854 $tags[0x013F] = array(5, 6); // PrimaryChromaticities -> RATIONAL, 6 2855 $tags[0x0211] = array(5, 3); // YCbCrCoefficients -> RATIONAL, 3 2856 $tags[0x0212] = array(3, 2); // YCbCrSubSampling -> SHORT, 2 2857 $tags[0x0213] = array(3, 1); // YCbCrPositioning -> SHORT, 1 2858 $tags[0x0214] = array(5, 6); // ReferenceBlackWhite -> RATIONAL, 6 2859 $tags[0x8298] = array(2, 0); // Copyright -> ASCII, Any 2860 $tags[0x8769] = array(4, 1); // ExifIFDOffset -> LONG, 1 2861 $tags[0x8825] = array(4, 1); // GPSIFDOffset -> LONG, 1 2862 } 2863 if ($mode == 'ifd1') { 2864 $tags[0x00FE] = array(4, 1); // TIFFNewSubfileType -> LONG, 1 2865 $tags[0x00FF] = array(3, 1); // TIFFSubfileType -> SHORT, 1 2866 $tags[0x0100] = array(4, 1); // TIFFImageWidth -> LONG (or SHORT), 1 2867 $tags[0x0101] = array(4, 1); // TIFFImageHeight -> LONG (or SHORT), 1 2868 $tags[0x0102] = array(3, 3); // TIFFBitsPerSample -> SHORT, 3 2869 $tags[0x0103] = array(3, 1); // TIFFCompression -> SHORT, 1 2870 $tags[0x0106] = array(3, 1); // TIFFPhotometricInterpretation -> SHORT, 1 2871 $tags[0x0107] = array(3, 1); // TIFFThreshholding -> SHORT, 1 2872 $tags[0x0108] = array(3, 1); // TIFFCellWidth -> SHORT, 1 2873 $tags[0x0109] = array(3, 1); // TIFFCellLength -> SHORT, 1 2874 $tags[0x010A] = array(3, 1); // TIFFFillOrder -> SHORT, 1 2875 $tags[0x010E] = array(2, 0); // TIFFImageDescription -> ASCII, Any 2876 $tags[0x010F] = array(2, 0); // TIFFMake -> ASCII, Any 2877 $tags[0x0110] = array(2, 0); // TIFFModel -> ASCII, Any 2878 $tags[0x0111] = array(4, 0); // TIFFStripOffsets -> LONG (or SHORT), Any (one per strip) 2879 $tags[0x0112] = array(3, 1); // TIFFOrientation -> SHORT, 1 2880 $tags[0x0115] = array(3, 1); // TIFFSamplesPerPixel -> SHORT, 1 2881 $tags[0x0116] = array(4, 1); // TIFFRowsPerStrip -> LONG (or SHORT), 1 2882 $tags[0x0117] = array(4, 0); // TIFFStripByteCounts -> LONG (or SHORT), Any (one per strip) 2883 $tags[0x0118] = array(3, 0); // TIFFMinSampleValue -> SHORT, Any (SamplesPerPixel) 2884 $tags[0x0119] = array(3, 0); // TIFFMaxSampleValue -> SHORT, Any (SamplesPerPixel) 2885 $tags[0x011A] = array(5, 1); // TIFFXResolution -> RATIONAL, 1 2886 $tags[0x011B] = array(5, 1); // TIFFYResolution -> RATIONAL, 1 2887 $tags[0x011C] = array(3, 1); // TIFFPlanarConfiguration -> SHORT, 1 2888 $tags[0x0122] = array(3, 1); // TIFFGrayResponseUnit -> SHORT, 1 2889 $tags[0x0123] = array(3, 0); // TIFFGrayResponseCurve -> SHORT, Any (2^BitsPerSample) 2890 $tags[0x0128] = array(3, 1); // TIFFResolutionUnit -> SHORT, 1 2891 $tags[0x0131] = array(2, 0); // TIFFSoftware -> ASCII, Any 2892 $tags[0x0132] = array(2, 20); // TIFFDateTime -> ASCII, 20 2893 $tags[0x013B] = array(2, 0); // TIFFArtist -> ASCII, Any 2894 $tags[0x013C] = array(2, 0); // TIFFHostComputer -> ASCII, Any 2895 $tags[0x0140] = array(3, 0); // TIFFColorMap -> SHORT, Any (3 * 2^BitsPerSample) 2896 $tags[0x0152] = array(3, 0); // TIFFExtraSamples -> SHORT, Any (SamplesPerPixel - 3) 2897 $tags[0x0201] = array(4, 1); // TIFFJFIFOffset -> LONG, 1 2898 $tags[0x0202] = array(4, 1); // TIFFJFIFLength -> LONG, 1 2899 $tags[0x0211] = array(5, 3); // TIFFYCbCrCoefficients -> RATIONAL, 3 2900 $tags[0x0212] = array(3, 2); // TIFFYCbCrSubSampling -> SHORT, 2 2901 $tags[0x0213] = array(3, 1); // TIFFYCbCrPositioning -> SHORT, 1 2902 $tags[0x0214] = array(5, 6); // TIFFReferenceBlackWhite -> RATIONAL, 6 2903 $tags[0x8298] = array(2, 0); // TIFFCopyright -> ASCII, Any 2904 $tags[0x9286] = array(2, 0); // TIFFUserComment -> ASCII, Any 2905 } elseif ($mode == 'exif') { 2906 $tags[0x829A] = array(5, 1); // ExposureTime -> RATIONAL, 1 2907 $tags[0x829D] = array(5, 1); // FNumber -> RATIONAL, 1 2908 $tags[0x8822] = array(3, 1); // ExposureProgram -> SHORT, 1 2909 $tags[0x8824] = array(2, 0); // SpectralSensitivity -> ASCII, Any 2910 $tags[0x8827] = array(3, 0); // ISOSpeedRatings -> SHORT, Any 2911 $tags[0x8828] = array(7, 0); // OECF -> UNDEFINED, Any 2912 $tags[0x9000] = array(7, 4); // EXIFVersion -> UNDEFINED, 4 2913 $tags[0x9003] = array(2, 20); // DateTimeOriginal -> ASCII, 20 2914 $tags[0x9004] = array(2, 20); // DateTimeDigitized -> ASCII, 20 2915 $tags[0x9101] = array(7, 4); // ComponentsConfiguration -> UNDEFINED, 4 2916 $tags[0x9102] = array(5, 1); // CompressedBitsPerPixel -> RATIONAL, 1 2917 $tags[0x9201] = array(10, 1); // ShutterSpeedValue -> SRATIONAL, 1 2918 $tags[0x9202] = array(5, 1); // ApertureValue -> RATIONAL, 1 2919 $tags[0x9203] = array(10, 1); // BrightnessValue -> SRATIONAL, 1 2920 $tags[0x9204] = array(10, 1); // ExposureBiasValue -> SRATIONAL, 1 2921 $tags[0x9205] = array(5, 1); // MaxApertureValue -> RATIONAL, 1 2922 $tags[0x9206] = array(5, 1); // SubjectDistance -> RATIONAL, 1 2923 $tags[0x9207] = array(3, 1); // MeteringMode -> SHORT, 1 2924 $tags[0x9208] = array(3, 1); // LightSource -> SHORT, 1 2925 $tags[0x9209] = array(3, 1); // Flash -> SHORT, 1 2926 $tags[0x920A] = array(5, 1); // FocalLength -> RATIONAL, 1 2927 $tags[0x927C] = array(7, 0); // MakerNote -> UNDEFINED, Any 2928 $tags[0x9286] = array(7, 0); // UserComment -> UNDEFINED, Any 2929 $tags[0x9290] = array(2, 0); // SubSecTime -> ASCII, Any 2930 $tags[0x9291] = array(2, 0); // SubSecTimeOriginal -> ASCII, Any 2931 $tags[0x9292] = array(2, 0); // SubSecTimeDigitized -> ASCII, Any 2932 $tags[0xA000] = array(7, 4); // FlashPixVersion -> UNDEFINED, 4 2933 $tags[0xA001] = array(3, 1); // ColorSpace -> SHORT, 1 2934 $tags[0xA002] = array(4, 1); // PixelXDimension -> LONG (or SHORT), 1 2935 $tags[0xA003] = array(4, 1); // PixelYDimension -> LONG (or SHORT), 1 2936 $tags[0xA004] = array(2, 13); // RelatedSoundFile -> ASCII, 13 2937 $tags[0xA005] = array(4, 1); // InteropIFDOffset -> LONG, 1 2938 $tags[0xA20B] = array(5, 1); // FlashEnergy -> RATIONAL, 1 2939 $tags[0xA20C] = array(7, 0); // SpatialFrequencyResponse -> UNDEFINED, Any 2940 $tags[0xA20E] = array(5, 1); // FocalPlaneXResolution -> RATIONAL, 1 2941 $tags[0xA20F] = array(5, 1); // FocalPlaneYResolution -> RATIONAL, 1 2942 $tags[0xA210] = array(3, 1); // FocalPlaneResolutionUnit -> SHORT, 1 2943 $tags[0xA214] = array(3, 2); // SubjectLocation -> SHORT, 2 2944 $tags[0xA215] = array(5, 1); // ExposureIndex -> RATIONAL, 1 2945 $tags[0xA217] = array(3, 1); // SensingMethod -> SHORT, 1 2946 $tags[0xA300] = array(7, 1); // FileSource -> UNDEFINED, 1 2947 $tags[0xA301] = array(7, 1); // SceneType -> UNDEFINED, 1 2948 $tags[0xA302] = array(7, 0); // CFAPattern -> UNDEFINED, Any 2949 } elseif ($mode == 'interop') { 2950 $tags[0x0001] = array(2, 0); // InteroperabilityIndex -> ASCII, Any 2951 $tags[0x0002] = array(7, 4); // InteroperabilityVersion -> UNKNOWN, 4 2952 $tags[0x1000] = array(2, 0); // RelatedImageFileFormat -> ASCII, Any 2953 $tags[0x1001] = array(4, 1); // RelatedImageWidth -> LONG (or SHORT), 1 2954 $tags[0x1002] = array(4, 1); // RelatedImageLength -> LONG (or SHORT), 1 2955 } elseif ($mode == 'gps') { 2956 $tags[0x0000] = array(1, 4); // GPSVersionID -> BYTE, 4 2957 $tags[0x0001] = array(2, 2); // GPSLatitudeRef -> ASCII, 2 2958 $tags[0x0002] = array(5, 3); // GPSLatitude -> RATIONAL, 3 2959 $tags[0x0003] = array(2, 2); // GPSLongitudeRef -> ASCII, 2 2960 $tags[0x0004] = array(5, 3); // GPSLongitude -> RATIONAL, 3 2961 $tags[0x0005] = array(2, 2); // GPSAltitudeRef -> ASCII, 2 2962 $tags[0x0006] = array(5, 1); // GPSAltitude -> RATIONAL, 1 2963 $tags[0x0007] = array(5, 3); // GPSTimeStamp -> RATIONAL, 3 2964 $tags[0x0008] = array(2, 0); // GPSSatellites -> ASCII, Any 2965 $tags[0x0009] = array(2, 2); // GPSStatus -> ASCII, 2 2966 $tags[0x000A] = array(2, 2); // GPSMeasureMode -> ASCII, 2 2967 $tags[0x000B] = array(5, 1); // GPSDOP -> RATIONAL, 1 2968 $tags[0x000C] = array(2, 2); // GPSSpeedRef -> ASCII, 2 2969 $tags[0x000D] = array(5, 1); // GPSSpeed -> RATIONAL, 1 2970 $tags[0x000E] = array(2, 2); // GPSTrackRef -> ASCII, 2 2971 $tags[0x000F] = array(5, 1); // GPSTrack -> RATIONAL, 1 2972 $tags[0x0010] = array(2, 2); // GPSImgDirectionRef -> ASCII, 2 2973 $tags[0x0011] = array(5, 1); // GPSImgDirection -> RATIONAL, 1 2974 $tags[0x0012] = array(2, 0); // GPSMapDatum -> ASCII, Any 2975 $tags[0x0013] = array(2, 2); // GPSDestLatitudeRef -> ASCII, 2 2976 $tags[0x0014] = array(5, 3); // GPSDestLatitude -> RATIONAL, 3 2977 $tags[0x0015] = array(2, 2); // GPSDestLongitudeRef -> ASCII, 2 2978 $tags[0x0016] = array(5, 3); // GPSDestLongitude -> RATIONAL, 3 2979 $tags[0x0017] = array(2, 2); // GPSDestBearingRef -> ASCII, 2 2980 $tags[0x0018] = array(5, 1); // GPSDestBearing -> RATIONAL, 1 2981 $tags[0x0019] = array(2, 2); // GPSDestDistanceRef -> ASCII, 2 2982 $tags[0x001A] = array(5, 1); // GPSDestDistance -> RATIONAL, 1 2983 } 2984 2985 return $tags; 2986 } 2987 2988 /*************************************************************/ 2989 function _exifNameTags($mode) { 2990 $tags = $this->_exifTagNames($mode); 2991 return $this->_names2Tags($tags); 2992 } 2993 2994 /*************************************************************/ 2995 function _iptcTagNames() { 2996 $tags = array(); 2997 $tags[0x14] = 'SuplementalCategories'; 2998 $tags[0x19] = 'Keywords'; 2999 $tags[0x78] = 'Caption'; 3000 $tags[0x7A] = 'CaptionWriter'; 3001 $tags[0x69] = 'Headline'; 3002 $tags[0x28] = 'SpecialInstructions'; 3003 $tags[0x0F] = 'Category'; 3004 $tags[0x50] = 'Byline'; 3005 $tags[0x55] = 'BylineTitle'; 3006 $tags[0x6E] = 'Credit'; 3007 $tags[0x73] = 'Source'; 3008 $tags[0x74] = 'CopyrightNotice'; 3009 $tags[0x05] = 'ObjectName'; 3010 $tags[0x5A] = 'City'; 3011 $tags[0x5C] = 'Sublocation'; 3012 $tags[0x5F] = 'ProvinceState'; 3013 $tags[0x65] = 'CountryName'; 3014 $tags[0x67] = 'OriginalTransmissionReference'; 3015 $tags[0x37] = 'DateCreated'; 3016 $tags[0x0A] = 'CopyrightFlag'; 3017 3018 return $tags; 3019 } 3020 3021 /*************************************************************/ 3022 function & _iptcNameTags() { 3023 $tags = $this->_iptcTagNames(); 3024 return $this->_names2Tags($tags); 3025 } 3026 3027 /*************************************************************/ 3028 function _names2Tags($tags2Names) { 3029 $names2Tags = array(); 3030 3031 foreach($tags2Names as $tag => $name) { 3032 $names2Tags[$name] = $tag; 3033 } 3034 3035 return $names2Tags; 3036 } 3037 3038 /*************************************************************/ 3039 3040 /** 3041 * @param $data 3042 * @param integer $pos 3043 * 3044 * @return int 3045 */ 3046 function _getByte(&$data, $pos) { 3047 if (!isset($data[$pos])) { 3048 throw new Exception("Requested byte at ".$pos.". Reading outside of file's boundaries."); 3049 } 3050 3051 return ord($data[$pos]); 3052 } 3053 3054 /*************************************************************/ 3055 3056 /** 3057 * @param mixed $data 3058 * @param integer $pos 3059 * 3060 * @param mixed $val 3061 * 3062 * @return int 3063 */ 3064 function _putByte(&$data, $pos, $val) { 3065 $val = intval($val); 3066 3067 $data[$pos] = chr($val); 3068 3069 return $pos + 1; 3070 } 3071 3072 /*************************************************************/ 3073 function _getShort(&$data, $pos, $bigEndian = true) { 3074 if (!isset($data[$pos]) || !isset($data[$pos + 1])) { 3075 throw new Exception("Requested short at ".$pos.". Reading outside of file's boundaries."); 3076 } 3077 3078 if ($bigEndian) { 3079 return (ord($data[$pos]) << 8) 3080 + ord($data[$pos + 1]); 3081 } else { 3082 return ord($data[$pos]) 3083 + (ord($data[$pos + 1]) << 8); 3084 } 3085 } 3086 3087 /*************************************************************/ 3088 function _putShort(&$data, $pos = 0, $val = 0, $bigEndian = true) { 3089 $val = intval($val); 3090 3091 if ($bigEndian) { 3092 $data[$pos + 0] = chr(($val & 0x0000FF00) >> 8); 3093 $data[$pos + 1] = chr(($val & 0x000000FF) >> 0); 3094 } else { 3095 $data[$pos + 0] = chr(($val & 0x00FF) >> 0); 3096 $data[$pos + 1] = chr(($val & 0xFF00) >> 8); 3097 } 3098 3099 return $pos + 2; 3100 } 3101 3102 /*************************************************************/ 3103 3104 /** 3105 * @param mixed $data 3106 * @param integer $pos 3107 * 3108 * @param bool $bigEndian 3109 * 3110 * @return int 3111 */ 3112 function _getLong(&$data, $pos, $bigEndian = true) { 3113 // Assume that if the start and end bytes are defined, the bytes inbetween are defined as well. 3114 if (!isset($data[$pos]) || !isset($data[$pos + 3])){ 3115 throw new Exception("Requested long at ".$pos.". Reading outside of file's boundaries."); 3116 } 3117 if ($bigEndian) { 3118 return (ord($data[$pos]) << 24) 3119 + (ord($data[$pos + 1]) << 16) 3120 + (ord($data[$pos + 2]) << 8) 3121 + ord($data[$pos + 3]); 3122 } else { 3123 return ord($data[$pos]) 3124 + (ord($data[$pos + 1]) << 8) 3125 + (ord($data[$pos + 2]) << 16) 3126 + (ord($data[$pos + 3]) << 24); 3127 } 3128 } 3129 3130 /*************************************************************/ 3131 3132 /** 3133 * @param mixed $data 3134 * @param integer $pos 3135 * 3136 * @param mixed $val 3137 * @param bool $bigEndian 3138 * 3139 * @return int 3140 */ 3141 function _putLong(&$data, $pos, $val, $bigEndian = true) { 3142 $val = intval($val); 3143 3144 if ($bigEndian) { 3145 $data[$pos + 0] = chr(($val & 0xFF000000) >> 24); 3146 $data[$pos + 1] = chr(($val & 0x00FF0000) >> 16); 3147 $data[$pos + 2] = chr(($val & 0x0000FF00) >> 8); 3148 $data[$pos + 3] = chr(($val & 0x000000FF) >> 0); 3149 } else { 3150 $data[$pos + 0] = chr(($val & 0x000000FF) >> 0); 3151 $data[$pos + 1] = chr(($val & 0x0000FF00) >> 8); 3152 $data[$pos + 2] = chr(($val & 0x00FF0000) >> 16); 3153 $data[$pos + 3] = chr(($val & 0xFF000000) >> 24); 3154 } 3155 3156 return $pos + 4; 3157 } 3158 3159 /*************************************************************/ 3160 function & _getNullString(&$data, $pos) { 3161 $str = ''; 3162 $max = strlen($data); 3163 3164 while ($pos < $max) { 3165 if (!isset($data[$pos])) { 3166 throw new Exception("Requested null-terminated string at offset ".$pos.". File terminated before the null-byte."); 3167 } 3168 if (ord($data[$pos]) == 0) { 3169 return $str; 3170 } else { 3171 $str .= $data[$pos]; 3172 } 3173 $pos++; 3174 } 3175 3176 return $str; 3177 } 3178 3179 /*************************************************************/ 3180 function & _getFixedString(&$data, $pos, $length = -1) { 3181 if ($length == -1) { 3182 $length = strlen($data) - $pos; 3183 } 3184 3185 $rv = substr($data, $pos, $length); 3186 if (strlen($rv) != $length) { 3187 throw new ErrorException(sprintf( 3188 "JPEGMeta failed parsing image metadata of %s. Got %d instead of %d bytes at offset %d.", 3189 $this->_fileName, strlen($rv), $length, $pos 3190 ), 0, E_WARNING); 3191 } 3192 return $rv; 3193 } 3194 3195 /*************************************************************/ 3196 function _putString(&$data, $pos, &$str) { 3197 $len = strlen($str); 3198 for ($i = 0; $i < $len; $i++) { 3199 $data[$pos + $i] = $str[$i]; 3200 } 3201 3202 return $pos + $len; 3203 } 3204 3205 /*************************************************************/ 3206 function _hexDump(&$data, $start = 0, $length = -1) { 3207 if (($length == -1) || (($length + $start) > strlen($data))) { 3208 $end = strlen($data); 3209 } else { 3210 $end = $start + $length; 3211 } 3212 3213 $ascii = ''; 3214 $count = 0; 3215 3216 echo "<tt>\n"; 3217 3218 while ($start < $end) { 3219 if (($count % 16) == 0) { 3220 echo sprintf('%04d', $count) . ': '; 3221 } 3222 3223 $c = ord($data[$start]); 3224 $count++; 3225 $start++; 3226 3227 $aux = dechex($c); 3228 if (strlen($aux) == 1) 3229 echo '0'; 3230 echo $aux . ' '; 3231 3232 if ($c == 60) 3233 $ascii .= '<'; 3234 elseif ($c == 62) 3235 $ascii .= '>'; 3236 elseif ($c == 32) 3237 $ascii .= ' '; 3238 elseif ($c > 32) 3239 $ascii .= chr($c); 3240 else 3241 $ascii .= '.'; 3242 3243 if (($count % 4) == 0) { 3244 echo ' - '; 3245 } 3246 3247 if (($count % 16) == 0) { 3248 echo ': ' . $ascii . "<br>\n"; 3249 $ascii = ''; 3250 } 3251 } 3252 3253 if ($ascii != '') { 3254 while (($count % 16) != 0) { 3255 echo '-- '; 3256 $count++; 3257 if (($count % 4) == 0) { 3258 echo ' - '; 3259 } 3260 } 3261 echo ': ' . $ascii . "<br>\n"; 3262 } 3263 3264 echo "</tt>\n"; 3265 } 3266 3267 /*****************************************************************/ 3268} 3269 3270/* vim: set expandtab tabstop=4 shiftwidth=4: */ 3271