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 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 4); 1435 if ($signature == 'JFIF') { 1436 $data =& $this->_markers[$i]['data']; 1437 break; 1438 } 1439 } 1440 } 1441 1442 if ($data == null) { 1443 $this->_info['jfif'] = false; 1444 return false; 1445 } 1446 1447 $this->_info['jfif'] = array(); 1448 1449 $vmaj = $this->_getByte($data, 5); 1450 $vmin = $this->_getByte($data, 6); 1451 1452 $this->_info['jfif']['Version'] = sprintf('%d.%02d', $vmaj, $vmin); 1453 1454 $units = $this->_getByte($data, 7); 1455 switch ($units) { 1456 case 0: 1457 $this->_info['jfif']['Units'] = 'pixels'; 1458 break; 1459 case 1: 1460 $this->_info['jfif']['Units'] = 'dpi'; 1461 break; 1462 case 2: 1463 $this->_info['jfif']['Units'] = 'dpcm'; 1464 break; 1465 default: 1466 $this->_info['jfif']['Units'] = 'unknown'; 1467 break; 1468 } 1469 1470 $xdens = $this->_getShort($data, 8); 1471 $ydens = $this->_getShort($data, 10); 1472 1473 $this->_info['jfif']['XDensity'] = $xdens; 1474 $this->_info['jfif']['YDensity'] = $ydens; 1475 1476 $thumbx = $this->_getByte($data, 12); 1477 $thumby = $this->_getByte($data, 13); 1478 1479 $this->_info['jfif']['ThumbnailWidth'] = $thumbx; 1480 $this->_info['jfif']['ThumbnailHeight'] = $thumby; 1481 } catch(Exception $e) { 1482 $this->_handleMarkerParsingException($e); 1483 $this->_info['jfif'] = false; 1484 return false; 1485 } 1486 1487 return true; 1488 } 1489 1490 /*************************************************************/ 1491 function _parseMarkerSOF() { 1492 if (!isset($this->_markers)) { 1493 $this->_readJPEG(); 1494 } 1495 1496 if ($this->_markers == null || $this->_isMarkerDisabled(('sof'))) { 1497 return false; 1498 } 1499 1500 try { 1501 $data = null; 1502 $count = count($this->_markers); 1503 for ($i = 0; $i < $count; $i++) { 1504 switch ($this->_markers[$i]['marker']) { 1505 case 0xC0: // SOF0 1506 case 0xC1: // SOF1 1507 case 0xC2: // SOF2 1508 case 0xC9: // SOF9 1509 $data =& $this->_markers[$i]['data']; 1510 $marker = $this->_markers[$i]['marker']; 1511 break; 1512 } 1513 } 1514 1515 if ($data == null) { 1516 $this->_info['sof'] = false; 1517 return false; 1518 } 1519 1520 $pos = 0; 1521 $this->_info['sof'] = array(); 1522 1523 switch ($marker) { 1524 case 0xC0: // SOF0 1525 $format = 'Baseline'; 1526 break; 1527 case 0xC1: // SOF1 1528 $format = 'Progessive'; 1529 break; 1530 case 0xC2: // SOF2 1531 $format = 'Non-baseline'; 1532 break; 1533 case 0xC9: // SOF9 1534 $format = 'Arithmetic'; 1535 break; 1536 default: 1537 return false; 1538 } 1539 1540 $this->_info['sof']['Format'] = $format; 1541 $this->_info['sof']['SamplePrecision'] = $this->_getByte($data, $pos + 0); 1542 $this->_info['sof']['ImageHeight'] = $this->_getShort($data, $pos + 1); 1543 $this->_info['sof']['ImageWidth'] = $this->_getShort($data, $pos + 3); 1544 $this->_info['sof']['ColorChannels'] = $this->_getByte($data, $pos + 5); 1545 } catch(Exception $e) { 1546 $this->_handleMarkerParsingException($e); 1547 $this->_info['sof'] = false; 1548 return false; 1549 } 1550 1551 return true; 1552 } 1553 1554 /** 1555 * Parses the XMP data 1556 * 1557 * @author Hakan Sandell <hakan.sandell@mydata.se> 1558 */ 1559 function _parseMarkerXmp() { 1560 if (!isset($this->_markers)) { 1561 $this->_readJPEG(); 1562 } 1563 1564 if ($this->_markers == null || $this->_isMarkerDisabled(('xmp'))) { 1565 return false; 1566 } 1567 1568 try { 1569 $data = null; 1570 $count = count($this->_markers); 1571 for ($i = 0; $i < $count; $i++) { 1572 if ($this->_markers[$i]['marker'] == 0xE1) { 1573 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 29); 1574 if ($signature == "http://ns.adobe.com/xap/1.0/\0") { 1575 $data = substr($this->_markers[$i]['data'], 29); 1576 break; 1577 } 1578 } 1579 } 1580 1581 if ($data == null) { 1582 $this->_info['xmp'] = false; 1583 return false; 1584 } 1585 1586 $parser = xml_parser_create(); 1587 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); 1588 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); 1589 $result = xml_parse_into_struct($parser, $data, $values, $tags); 1590 xml_parser_free($parser); 1591 1592 if ($result == 0) { 1593 $this->_info['xmp'] = false; 1594 return false; 1595 } 1596 1597 $this->_info['xmp'] = array(); 1598 $count = count($values); 1599 for ($i = 0; $i < $count; $i++) { 1600 if ($values[$i]['tag'] == 'rdf:Description' && $values[$i]['type'] == 'open') { 1601 1602 while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Description')) { 1603 $this->_parseXmpNode($values, $i, $this->_info['xmp'][$values[$i]['tag']], $count); 1604 } 1605 } 1606 } 1607 } catch (Exception $e) { 1608 $this->_handleMarkerParsingException($e); 1609 $this->_info['xmp'] = false; 1610 return false; 1611 } 1612 1613 return true; 1614 } 1615 1616 /** 1617 * Parses XMP nodes by recursion 1618 * 1619 * @author Hakan Sandell <hakan.sandell@mydata.se> 1620 * 1621 * @param array $values 1622 * @param int $i 1623 * @param mixed $meta 1624 * @param integer $count 1625 */ 1626 function _parseXmpNode($values, &$i, &$meta, $count) { 1627 if ($values[$i]['type'] == 'close') return; 1628 1629 if ($values[$i]['type'] == 'complete') { 1630 // Simple Type property 1631 $meta = $values[$i]['value'] ?? ''; 1632 return; 1633 } 1634 1635 $i++; 1636 if ($i >= $count) return; 1637 1638 if ($values[$i]['tag'] == 'rdf:Bag' || $values[$i]['tag'] == 'rdf:Seq') { 1639 // Array property 1640 $meta = array(); 1641 while ($values[++$i]['tag'] == 'rdf:li') { 1642 $this->_parseXmpNode($values, $i, $meta[], $count); 1643 } 1644 $i++; // skip closing Bag/Seq tag 1645 1646 } elseif ($values[$i]['tag'] == 'rdf:Alt') { 1647 // Language Alternative property, only the first (default) value is used 1648 if ($values[$i]['type'] == 'open') { 1649 $i++; 1650 $this->_parseXmpNode($values, $i, $meta, $count); 1651 while ((++$i < $count) && ($values[$i]['tag'] != 'rdf:Alt')); 1652 $i++; // skip closing Alt tag 1653 } 1654 1655 } else { 1656 // Structure property 1657 $meta = array(); 1658 $startTag = $values[$i-1]['tag']; 1659 do { 1660 $this->_parseXmpNode($values, $i, $meta[$values[$i]['tag']], $count); 1661 } while ((++$i < $count) && ($values[$i]['tag'] != $startTag)); 1662 } 1663 } 1664 1665 /*************************************************************/ 1666 function _parseMarkerExif() { 1667 if (!isset($this->_markers)) { 1668 $this->_readJPEG(); 1669 } 1670 1671 if ($this->_markers == null || $this->_isMarkerDisabled(('exif'))) { 1672 return false; 1673 } 1674 1675 try { 1676 $data = null; 1677 $count = count($this->_markers); 1678 for ($i = 0; $i < $count; $i++) { 1679 if ($this->_markers[$i]['marker'] == 0xE1) { 1680 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6); 1681 if ($signature == "Exif\0\0") { 1682 $data =& $this->_markers[$i]['data']; 1683 break; 1684 } 1685 } 1686 } 1687 1688 if ($data == null) { 1689 $this->_info['exif'] = false; 1690 return false; 1691 } 1692 $pos = 6; 1693 $this->_info['exif'] = array(); 1694 1695 // We don't increment $pos after this because Exif uses offsets relative to this point 1696 1697 $byteAlign = $this->_getShort($data, $pos + 0); 1698 1699 if ($byteAlign == 0x4949) { // "II" 1700 $isBigEndian = false; 1701 } elseif ($byteAlign == 0x4D4D) { // "MM" 1702 $isBigEndian = true; 1703 } else { 1704 return false; // Unexpected data 1705 } 1706 1707 $alignCheck = $this->_getShort($data, $pos + 2, $isBigEndian); 1708 if ($alignCheck != 0x002A) // That's the expected value 1709 return false; // Unexpected data 1710 1711 if ($isBigEndian) { 1712 $this->_info['exif']['ByteAlign'] = "Big Endian"; 1713 } else { 1714 $this->_info['exif']['ByteAlign'] = "Little Endian"; 1715 } 1716 1717 $offsetIFD0 = $this->_getLong($data, $pos + 4, $isBigEndian); 1718 if ($offsetIFD0 < 8) 1719 return false; // Unexpected data 1720 1721 $offsetIFD1 = $this->_readIFD($data, $pos, $offsetIFD0, $isBigEndian, 'ifd0'); 1722 if ($offsetIFD1 != 0) 1723 $this->_readIFD($data, $pos, $offsetIFD1, $isBigEndian, 'ifd1'); 1724 } catch(Exception $e) { 1725 $this->_handleMarkerParsingException($e); 1726 $this->_info['exif'] = false; 1727 return false; 1728 } 1729 1730 return true; 1731 } 1732 1733 /*************************************************************/ 1734 1735 /** 1736 * @param mixed $data 1737 * @param integer $base 1738 * @param integer $offset 1739 * @param boolean $isBigEndian 1740 * @param string $mode 1741 * 1742 * @return int 1743 */ 1744 function _readIFD($data, $base, $offset, $isBigEndian, $mode) { 1745 $EXIFTags = $this->_exifTagNames($mode); 1746 1747 $numEntries = $this->_getShort($data, $base + $offset, $isBigEndian); 1748 $offset += 2; 1749 1750 $exifTIFFOffset = 0; 1751 $exifTIFFLength = 0; 1752 $exifThumbnailOffset = 0; 1753 $exifThumbnailLength = 0; 1754 1755 for ($i = 0; $i < $numEntries; $i++) { 1756 $tag = $this->_getShort($data, $base + $offset, $isBigEndian); 1757 $offset += 2; 1758 $type = $this->_getShort($data, $base + $offset, $isBigEndian); 1759 $offset += 2; 1760 $count = $this->_getLong($data, $base + $offset, $isBigEndian); 1761 $offset += 4; 1762 1763 if (($type < 1) || ($type > 12)) 1764 return false; // Unexpected Type 1765 1766 $typeLengths = array( -1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 ); 1767 1768 $dataLength = $typeLengths[$type] * $count; 1769 if ($dataLength > 4) { 1770 $dataOffset = $this->_getLong($data, $base + $offset, $isBigEndian); 1771 $rawValue = $this->_getFixedString($data, $base + $dataOffset, $dataLength); 1772 } else { 1773 $rawValue = $this->_getFixedString($data, $base + $offset, $dataLength); 1774 } 1775 $offset += 4; 1776 1777 switch ($type) { 1778 case 1: // UBYTE 1779 if ($count == 1) { 1780 $value = $this->_getByte($rawValue, 0); 1781 } else { 1782 $value = array(); 1783 for ($j = 0; $j < $count; $j++) 1784 $value[$j] = $this->_getByte($rawValue, $j); 1785 } 1786 break; 1787 case 2: // ASCII 1788 $value = $rawValue; 1789 break; 1790 case 3: // USHORT 1791 if ($count == 1) { 1792 $value = $this->_getShort($rawValue, 0, $isBigEndian); 1793 } else { 1794 $value = array(); 1795 for ($j = 0; $j < $count; $j++) 1796 $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian); 1797 } 1798 break; 1799 case 4: // ULONG 1800 if ($count == 1) { 1801 $value = $this->_getLong($rawValue, 0, $isBigEndian); 1802 } else { 1803 $value = array(); 1804 for ($j = 0; $j < $count; $j++) 1805 $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian); 1806 } 1807 break; 1808 case 5: // URATIONAL 1809 if ($count == 1) { 1810 $a = $this->_getLong($rawValue, 0, $isBigEndian); 1811 $b = $this->_getLong($rawValue, 4, $isBigEndian); 1812 $value = array(); 1813 $value['val'] = 0; 1814 $value['num'] = $a; 1815 $value['den'] = $b; 1816 if (($a != 0) && ($b != 0)) { 1817 $value['val'] = $a / $b; 1818 } 1819 } else { 1820 $value = array(); 1821 for ($j = 0; $j < $count; $j++) { 1822 $a = $this->_getLong($rawValue, $j * 8, $isBigEndian); 1823 $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian); 1824 $value = array(); 1825 $value[$j]['val'] = 0; 1826 $value[$j]['num'] = $a; 1827 $value[$j]['den'] = $b; 1828 if (($a != 0) && ($b != 0)) 1829 $value[$j]['val'] = $a / $b; 1830 } 1831 } 1832 break; 1833 case 6: // SBYTE 1834 if ($count == 1) { 1835 $value = $this->_getByte($rawValue, 0); 1836 } else { 1837 $value = array(); 1838 for ($j = 0; $j < $count; $j++) 1839 $value[$j] = $this->_getByte($rawValue, $j); 1840 } 1841 break; 1842 case 7: // UNDEFINED 1843 $value = $rawValue; 1844 break; 1845 case 8: // SSHORT 1846 if ($count == 1) { 1847 $value = $this->_getShort($rawValue, 0, $isBigEndian); 1848 } else { 1849 $value = array(); 1850 for ($j = 0; $j < $count; $j++) 1851 $value[$j] = $this->_getShort($rawValue, $j * 2, $isBigEndian); 1852 } 1853 break; 1854 case 9: // SLONG 1855 if ($count == 1) { 1856 $value = $this->_getLong($rawValue, 0, $isBigEndian); 1857 } else { 1858 $value = array(); 1859 for ($j = 0; $j < $count; $j++) 1860 $value[$j] = $this->_getLong($rawValue, $j * 4, $isBigEndian); 1861 } 1862 break; 1863 case 10: // SRATIONAL 1864 if ($count == 1) { 1865 $a = $this->_getLong($rawValue, 0, $isBigEndian); 1866 $b = $this->_getLong($rawValue, 4, $isBigEndian); 1867 $value = array(); 1868 $value['val'] = 0; 1869 $value['num'] = $a; 1870 $value['den'] = $b; 1871 if (($a != 0) && ($b != 0)) 1872 $value['val'] = $a / $b; 1873 } else { 1874 $value = array(); 1875 for ($j = 0; $j < $count; $j++) { 1876 $a = $this->_getLong($rawValue, $j * 8, $isBigEndian); 1877 $b = $this->_getLong($rawValue, ($j * 8) + 4, $isBigEndian); 1878 $value = array(); 1879 $value[$j]['val'] = 0; 1880 $value[$j]['num'] = $a; 1881 $value[$j]['den'] = $b; 1882 if (($a != 0) && ($b != 0)) 1883 $value[$j]['val'] = $a / $b; 1884 } 1885 } 1886 break; 1887 case 11: // FLOAT 1888 $value = $rawValue; 1889 break; 1890 1891 case 12: // DFLOAT 1892 $value = $rawValue; 1893 break; 1894 default: 1895 return false; // Unexpected Type 1896 } 1897 1898 $tagName = ''; 1899 if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset 1900 $this->_readIFD($data, $base, $value, $isBigEndian, 'exif'); 1901 } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset 1902 $this->_readIFD($data, $base, $value, $isBigEndian, 'gps'); 1903 } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets 1904 $exifTIFFOffset = $value; 1905 } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts 1906 $exifTIFFLength = $value; 1907 } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset 1908 $exifThumbnailOffset = $value; 1909 } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength 1910 $exifThumbnailLength = $value; 1911 } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset 1912 $this->_readIFD($data, $base, $value, $isBigEndian, 'interop'); 1913 } 1914 // elseif (($mode == 'exif') && ($tag == 0x927C)) { // MakerNote 1915 // } 1916 else { 1917 if (isset($EXIFTags[$tag])) { 1918 $tagName = $EXIFTags[$tag]; 1919 if (isset($this->_info['exif'][$tagName])) { 1920 if (!is_array($this->_info['exif'][$tagName])) { 1921 $aux = array(); 1922 $aux[0] = $this->_info['exif'][$tagName]; 1923 $this->_info['exif'][$tagName] = $aux; 1924 } 1925 1926 $this->_info['exif'][$tagName][count($this->_info['exif'][$tagName])] = $value; 1927 } else { 1928 $this->_info['exif'][$tagName] = $value; 1929 } 1930 } 1931 /* 1932 else { 1933 echo sprintf("<h1>Unknown tag %02x (t: %d l: %d) %s in %s</h1>", $tag, $type, $count, $mode, $this->_fileName); 1934 // Unknown Tags will be ignored!!! 1935 // That's because the tag might be a pointer (like the Exif tag) 1936 // and saving it without saving the data it points to might 1937 // create an invalid file. 1938 } 1939 */ 1940 } 1941 } 1942 1943 if (($exifThumbnailOffset > 0) && ($exifThumbnailLength > 0)) { 1944 $this->_info['exif']['JFIFThumbnail'] = $this->_getFixedString($data, $base + $exifThumbnailOffset, $exifThumbnailLength); 1945 } 1946 1947 if (($exifTIFFOffset > 0) && ($exifTIFFLength > 0)) { 1948 $this->_info['exif']['TIFFStrips'] = $this->_getFixedString($data, $base + $exifTIFFOffset, $exifTIFFLength); 1949 } 1950 1951 $nextOffset = $this->_getLong($data, $base + $offset, $isBigEndian); 1952 return $nextOffset; 1953 } 1954 1955 /*************************************************************/ 1956 function & _createMarkerExif() { 1957 $data = null; 1958 $count = count($this->_markers); 1959 for ($i = 0; $i < $count; $i++) { 1960 if ($this->_markers[$i]['marker'] == 0xE1) { 1961 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 6); 1962 if ($signature == "Exif\0\0") { 1963 $data =& $this->_markers[$i]['data']; 1964 break; 1965 } 1966 } 1967 } 1968 1969 if (!isset($this->_info['exif'])) { 1970 return false; 1971 } 1972 1973 $data = "Exif\0\0"; 1974 $pos = 6; 1975 $offsetBase = 6; 1976 1977 if (isset($this->_info['exif']['ByteAlign']) && ($this->_info['exif']['ByteAlign'] == "Big Endian")) { 1978 $isBigEndian = true; 1979 $aux = "MM"; 1980 $pos = $this->_putString($data, $pos, $aux); 1981 } else { 1982 $isBigEndian = false; 1983 $aux = "II"; 1984 $pos = $this->_putString($data, $pos, $aux); 1985 } 1986 $pos = $this->_putShort($data, $pos, 0x002A, $isBigEndian); 1987 $pos = $this->_putLong($data, $pos, 0x00000008, $isBigEndian); // IFD0 Offset is always 8 1988 1989 $ifd0 =& $this->_getIFDEntries($isBigEndian, 'ifd0'); 1990 $ifd1 =& $this->_getIFDEntries($isBigEndian, 'ifd1'); 1991 1992 $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd0, $isBigEndian, true); 1993 $pos = $this->_writeIFD($data, $pos, $offsetBase, $ifd1, $isBigEndian, false); 1994 1995 return $data; 1996 } 1997 1998 /*************************************************************/ 1999 2000 /** 2001 * @param mixed $data 2002 * @param integer $pos 2003 * @param integer $offsetBase 2004 * @param array $entries 2005 * @param boolean $isBigEndian 2006 * @param boolean $hasNext 2007 * 2008 * @return mixed 2009 */ 2010 function _writeIFD(&$data, $pos, $offsetBase, &$entries, $isBigEndian, $hasNext) { 2011 $tiffData = null; 2012 $tiffDataOffsetPos = -1; 2013 2014 $entryCount = count($entries); 2015 2016 $dataPos = $pos + 2 + ($entryCount * 12) + 4; 2017 $pos = $this->_putShort($data, $pos, $entryCount, $isBigEndian); 2018 2019 for ($i = 0; $i < $entryCount; $i++) { 2020 $tag = $entries[$i]['tag']; 2021 $type = $entries[$i]['type']; 2022 2023 if ($type == -99) { // SubIFD 2024 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2025 $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG 2026 $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1 2027 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2028 2029 $dataPos = $this->_writeIFD($data, $dataPos, $offsetBase, $entries[$i]['value'], $isBigEndian, false); 2030 } elseif ($type == -98) { // TIFF Data 2031 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2032 $pos = $this->_putShort($data, $pos, 0x04, $isBigEndian); // LONG 2033 $pos = $this->_putLong($data, $pos, 0x01, $isBigEndian); // Count = 1 2034 $tiffDataOffsetPos = $pos; 2035 $pos = $this->_putLong($data, $pos, 0x00, $isBigEndian); // For Now 2036 $tiffData =& $entries[$i]['value'] ; 2037 } else { // Regular Entry 2038 $pos = $this->_putShort($data, $pos, $tag, $isBigEndian); 2039 $pos = $this->_putShort($data, $pos, $type, $isBigEndian); 2040 $pos = $this->_putLong($data, $pos, $entries[$i]['count'], $isBigEndian); 2041 if (strlen($entries[$i]['value']) > 4) { 2042 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2043 $dataPos = $this->_putString($data, $dataPos, $entries[$i]['value']); 2044 } else { 2045 $val = str_pad($entries[$i]['value'], 4, "\0"); 2046 $pos = $this->_putString($data, $pos, $val); 2047 } 2048 } 2049 } 2050 2051 if ($tiffData != null) { 2052 $this->_putLong($data, $tiffDataOffsetPos, $dataPos - $offsetBase, $isBigEndian); 2053 $dataPos = $this->_putString($data, $dataPos, $tiffData); 2054 } 2055 2056 if ($hasNext) { 2057 $pos = $this->_putLong($data, $pos, $dataPos - $offsetBase, $isBigEndian); 2058 } else { 2059 $pos = $this->_putLong($data, $pos, 0, $isBigEndian); 2060 } 2061 2062 return $dataPos; 2063 } 2064 2065 /*************************************************************/ 2066 2067 /** 2068 * @param boolean $isBigEndian 2069 * @param string $mode 2070 * 2071 * @return array 2072 */ 2073 function & _getIFDEntries($isBigEndian, $mode) { 2074 $EXIFNames = $this->_exifTagNames($mode); 2075 $EXIFTags = $this->_exifNameTags($mode); 2076 $EXIFTypeInfo = $this->_exifTagTypes($mode); 2077 2078 $ifdEntries = array(); 2079 $entryCount = 0; 2080 2081 foreach($EXIFNames as $tag => $name) { 2082 $type = $EXIFTypeInfo[$tag][0]; 2083 $count = $EXIFTypeInfo[$tag][1]; 2084 $value = null; 2085 2086 if (($mode == 'ifd0') && ($tag == 0x8769)) { // ExifIFDOffset 2087 if (isset($this->_info['exif']['EXIFVersion'])) { 2088 $value =& $this->_getIFDEntries($isBigEndian, "exif"); 2089 $type = -99; 2090 } 2091 else { 2092 $value = null; 2093 } 2094 } elseif (($mode == 'ifd0') && ($tag == 0x8825)) { // GPSIFDOffset 2095 if (isset($this->_info['exif']['GPSVersionID'])) { 2096 $value =& $this->_getIFDEntries($isBigEndian, "gps"); 2097 $type = -99; 2098 } else { 2099 $value = null; 2100 } 2101 } elseif (($mode == 'ifd1') && ($tag == 0x0111)) { // TIFFStripOffsets 2102 if (isset($this->_info['exif']['TIFFStrips'])) { 2103 $value =& $this->_info['exif']['TIFFStrips']; 2104 $type = -98; 2105 } else { 2106 $value = null; 2107 } 2108 } elseif (($mode == 'ifd1') && ($tag == 0x0117)) { // TIFFStripByteCounts 2109 if (isset($this->_info['exif']['TIFFStrips'])) { 2110 $value = strlen($this->_info['exif']['TIFFStrips']); 2111 } else { 2112 $value = null; 2113 } 2114 } elseif (($mode == 'ifd1') && ($tag == 0x0201)) { // TIFFJFIFOffset 2115 if (isset($this->_info['exif']['JFIFThumbnail'])) { 2116 $value =& $this->_info['exif']['JFIFThumbnail']; 2117 $type = -98; 2118 } else { 2119 $value = null; 2120 } 2121 } elseif (($mode == 'ifd1') && ($tag == 0x0202)) { // TIFFJFIFLength 2122 if (isset($this->_info['exif']['JFIFThumbnail'])) { 2123 $value = strlen($this->_info['exif']['JFIFThumbnail']); 2124 } else { 2125 $value = null; 2126 } 2127 } elseif (($mode == 'exif') && ($tag == 0xA005)) { // InteropIFDOffset 2128 if (isset($this->_info['exif']['InteroperabilityIndex'])) { 2129 $value =& $this->_getIFDEntries($isBigEndian, "interop"); 2130 $type = -99; 2131 } else { 2132 $value = null; 2133 } 2134 } elseif (isset($this->_info['exif'][$name])) { 2135 $origValue =& $this->_info['exif'][$name]; 2136 2137 // This makes it easier to process variable size elements 2138 if (!is_array($origValue) || isset($origValue['val'])) { 2139 unset($origValue); // Break the reference 2140 $origValue = array($this->_info['exif'][$name]); 2141 } 2142 $origCount = count($origValue); 2143 2144 if ($origCount == 0 ) { 2145 $type = -1; // To ignore this field 2146 } 2147 2148 $value = " "; 2149 2150 switch ($type) { 2151 case 1: // UBYTE 2152 if ($count == 0) { 2153 $count = $origCount; 2154 } 2155 2156 $j = 0; 2157 while (($j < $count) && ($j < $origCount)) { 2158 2159 $this->_putByte($value, $j, $origValue[$j]); 2160 $j++; 2161 } 2162 2163 while ($j < $count) { 2164 $this->_putByte($value, $j, 0); 2165 $j++; 2166 } 2167 break; 2168 case 2: // ASCII 2169 $v = strval($origValue[0]); 2170 if (($count != 0) && (strlen($v) > $count)) { 2171 $v = substr($v, 0, $count); 2172 } 2173 elseif (($count > 0) && (strlen($v) < $count)) { 2174 $v = str_pad($v, $count, "\0"); 2175 } 2176 2177 $count = strlen($v); 2178 2179 $this->_putString($value, 0, $v); 2180 break; 2181 case 3: // USHORT 2182 if ($count == 0) { 2183 $count = $origCount; 2184 } 2185 2186 $j = 0; 2187 while (($j < $count) && ($j < $origCount)) { 2188 $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian); 2189 $j++; 2190 } 2191 2192 while ($j < $count) { 2193 $this->_putShort($value, $j * 2, 0, $isBigEndian); 2194 $j++; 2195 } 2196 break; 2197 case 4: // ULONG 2198 if ($count == 0) { 2199 $count = $origCount; 2200 } 2201 2202 $j = 0; 2203 while (($j < $count) && ($j < $origCount)) { 2204 $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian); 2205 $j++; 2206 } 2207 2208 while ($j < $count) { 2209 $this->_putLong($value, $j * 4, 0, $isBigEndian); 2210 $j++; 2211 } 2212 break; 2213 case 5: // URATIONAL 2214 if ($count == 0) { 2215 $count = $origCount; 2216 } 2217 2218 $j = 0; 2219 while (($j < $count) && ($j < $origCount)) { 2220 $v = $origValue[$j]; 2221 if (is_array($v)) { 2222 $a = $v['num']; 2223 $b = $v['den']; 2224 } 2225 else { 2226 $a = 0; 2227 $b = 0; 2228 // TODO: Allow other types and convert them 2229 } 2230 $this->_putLong($value, $j * 8, $a, $isBigEndian); 2231 $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian); 2232 $j++; 2233 } 2234 2235 while ($j < $count) { 2236 $this->_putLong($value, $j * 8, 0, $isBigEndian); 2237 $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian); 2238 $j++; 2239 } 2240 break; 2241 case 6: // SBYTE 2242 if ($count == 0) { 2243 $count = $origCount; 2244 } 2245 2246 $j = 0; 2247 while (($j < $count) && ($j < $origCount)) { 2248 $this->_putByte($value, $j, $origValue[$j]); 2249 $j++; 2250 } 2251 2252 while ($j < $count) { 2253 $this->_putByte($value, $j, 0); 2254 $j++; 2255 } 2256 break; 2257 case 7: // UNDEFINED 2258 $v = strval($origValue[0]); 2259 if (($count != 0) && (strlen($v) > $count)) { 2260 $v = substr($v, 0, $count); 2261 } 2262 elseif (($count > 0) && (strlen($v) < $count)) { 2263 $v = str_pad($v, $count, "\0"); 2264 } 2265 2266 $count = strlen($v); 2267 2268 $this->_putString($value, 0, $v); 2269 break; 2270 case 8: // SSHORT 2271 if ($count == 0) { 2272 $count = $origCount; 2273 } 2274 2275 $j = 0; 2276 while (($j < $count) && ($j < $origCount)) { 2277 $this->_putShort($value, $j * 2, $origValue[$j], $isBigEndian); 2278 $j++; 2279 } 2280 2281 while ($j < $count) { 2282 $this->_putShort($value, $j * 2, 0, $isBigEndian); 2283 $j++; 2284 } 2285 break; 2286 case 9: // SLONG 2287 if ($count == 0) { 2288 $count = $origCount; 2289 } 2290 2291 $j = 0; 2292 while (($j < $count) && ($j < $origCount)) { 2293 $this->_putLong($value, $j * 4, $origValue[$j], $isBigEndian); 2294 $j++; 2295 } 2296 2297 while ($j < $count) { 2298 $this->_putLong($value, $j * 4, 0, $isBigEndian); 2299 $j++; 2300 } 2301 break; 2302 case 10: // SRATIONAL 2303 if ($count == 0) { 2304 $count = $origCount; 2305 } 2306 2307 $j = 0; 2308 while (($j < $count) && ($j < $origCount)) { 2309 $v = $origValue[$j]; 2310 if (is_array($v)) { 2311 $a = $v['num']; 2312 $b = $v['den']; 2313 } 2314 else { 2315 $a = 0; 2316 $b = 0; 2317 // TODO: Allow other types and convert them 2318 } 2319 2320 $this->_putLong($value, $j * 8, $a, $isBigEndian); 2321 $this->_putLong($value, ($j * 8) + 4, $b, $isBigEndian); 2322 $j++; 2323 } 2324 2325 while ($j < $count) { 2326 $this->_putLong($value, $j * 8, 0, $isBigEndian); 2327 $this->_putLong($value, ($j * 8) + 4, 0, $isBigEndian); 2328 $j++; 2329 } 2330 break; 2331 case 11: // FLOAT 2332 if ($count == 0) { 2333 $count = $origCount; 2334 } 2335 2336 $j = 0; 2337 while (($j < $count) && ($j < $origCount)) { 2338 $v = strval($origValue[$j]); 2339 if (strlen($v) > 4) { 2340 $v = substr($v, 0, 4); 2341 } 2342 elseif (strlen($v) < 4) { 2343 $v = str_pad($v, 4, "\0"); 2344 } 2345 $this->_putString($value, $j * 4, $v); 2346 $j++; 2347 } 2348 2349 while ($j < $count) { 2350 $v = "\0\0\0\0"; 2351 $this->_putString($value, $j * 4, $v); 2352 $j++; 2353 } 2354 break; 2355 case 12: // DFLOAT 2356 if ($count == 0) { 2357 $count = $origCount; 2358 } 2359 2360 $j = 0; 2361 while (($j < $count) && ($j < $origCount)) { 2362 $v = strval($origValue[$j]); 2363 if (strlen($v) > 8) { 2364 $v = substr($v, 0, 8); 2365 } 2366 elseif (strlen($v) < 8) { 2367 $v = str_pad($v, 8, "\0"); 2368 } 2369 $this->_putString($value, $j * 8, $v); 2370 $j++; 2371 } 2372 2373 while ($j < $count) { 2374 $v = "\0\0\0\0\0\0\0\0"; 2375 $this->_putString($value, $j * 8, $v); 2376 $j++; 2377 } 2378 break; 2379 default: 2380 $value = null; 2381 break; 2382 } 2383 } 2384 2385 if ($value != null) { 2386 $ifdEntries[$entryCount] = array(); 2387 $ifdEntries[$entryCount]['tag'] = $tag; 2388 $ifdEntries[$entryCount]['type'] = $type; 2389 $ifdEntries[$entryCount]['count'] = $count; 2390 $ifdEntries[$entryCount]['value'] = $value; 2391 2392 $entryCount++; 2393 } 2394 } 2395 2396 return $ifdEntries; 2397 } 2398 /*************************************************************/ 2399 function _handleMarkerParsingException($e) { 2400 \dokuwiki\ErrorHandler::logException($e, $this->_fileName); 2401 } 2402 2403 /*************************************************************/ 2404 function _isMarkerDisabled($name) { 2405 if (!isset($this->_info)) return false; 2406 return isset($this->_info[$name]) && $this->_info[$name] === false; 2407 } 2408 2409 /*************************************************************/ 2410 function _parseMarkerAdobe() { 2411 if (!isset($this->_markers)) { 2412 $this->_readJPEG(); 2413 } 2414 2415 if ($this->_markers == null || $this->_isMarkerDisabled('adobe')) { 2416 return false; 2417 } 2418 try { 2419 $data = null; 2420 $count = count($this->_markers); 2421 for ($i = 0; $i < $count; $i++) { 2422 if ($this->_markers[$i]['marker'] == 0xED) { 2423 $signature = $this->_getFixedString($this->_markers[$i]['data'], 0, 14); 2424 if ($signature == "Photoshop 3.0\0") { 2425 $data =& $this->_markers[$i]['data']; 2426 break; 2427 } 2428 } 2429 } 2430 2431 if ($data == null) { 2432 $this->_info['adobe'] = false; 2433 $this->_info['iptc'] = false; 2434 return false; 2435 } 2436 $pos = 14; 2437 $this->_info['adobe'] = array(); 2438 $this->_info['adobe']['raw'] = array(); 2439 $this->_info['iptc'] = array(); 2440 2441 $datasize = strlen($data); 2442 2443 while ($pos < $datasize) { 2444 $signature = $this->_getFixedString($data, $pos, 4); 2445 if ($signature != '8BIM') 2446 return false; 2447 $pos += 4; 2448 2449 $type = $this->_getShort($data, $pos); 2450 $pos += 2; 2451 2452 $strlen = $this->_getByte($data, $pos); 2453 $pos += 1; 2454 $header = ''; 2455 for ($i = 0; $i < $strlen; $i++) { 2456 $header .= $data[$pos + $i]; 2457 } 2458 $pos += $strlen + 1 - ($strlen % 2); // The string is padded to even length, counting the length byte itself 2459 2460 $length = $this->_getLong($data, $pos); 2461 $pos += 4; 2462 2463 $basePos = $pos; 2464 2465 switch ($type) { 2466 case 0x0404: // Caption (IPTC Data) 2467 $pos = $this->_readIPTC($data, $pos); 2468 if ($pos == false) 2469 return false; 2470 break; 2471 case 0x040A: // CopyrightFlag 2472 $this->_info['adobe']['CopyrightFlag'] = $this->_getByte($data, $pos); 2473 $pos += $length; 2474 break; 2475 case 0x040B: // ImageURL 2476 $this->_info['adobe']['ImageURL'] = $this->_getFixedString($data, $pos, $length); 2477 $pos += $length; 2478 break; 2479 case 0x040C: // Thumbnail 2480 $aux = $this->_getLong($data, $pos); 2481 $pos += 4; 2482 if ($aux == 1) { 2483 $this->_info['adobe']['ThumbnailWidth'] = $this->_getLong($data, $pos); 2484 $pos += 4; 2485 $this->_info['adobe']['ThumbnailHeight'] = $this->_getLong($data, $pos); 2486 $pos += 4; 2487 2488 $pos += 16; // Skip some data 2489 2490 $this->_info['adobe']['ThumbnailData'] = $this->_getFixedString($data, $pos, $length - 28); 2491 $pos += $length - 28; 2492 } 2493 break; 2494 default: 2495 break; 2496 } 2497 2498 // We save all blocks, even those we recognized 2499 $label = sprintf('8BIM_0x%04x', $type); 2500 $this->_info['adobe']['raw'][$label] = array(); 2501 $this->_info['adobe']['raw'][$label]['type'] = $type; 2502 $this->_info['adobe']['raw'][$label]['header'] = $header; 2503 $this->_info['adobe']['raw'][$label]['data'] =& $this->_getFixedString($data, $basePos, $length); 2504 2505 $pos = $basePos + $length + ($length % 2); // Even padding 2506 } 2507 } catch(Exception $e) { 2508 $this->_handleMarkerParsingException($e); 2509 $this->_info['adobe'] = false; 2510 $this->_info['iptc'] = false; 2511 return false; 2512 } 2513 } 2514 2515 /*************************************************************/ 2516 function _readIPTC(&$data, $pos = 0) { 2517 $totalLength = strlen($data); 2518 2519 $IPTCTags = $this->_iptcTagNames(); 2520 2521 while ($pos < ($totalLength - 5)) { 2522 $signature = $this->_getShort($data, $pos); 2523 if ($signature != 0x1C02) 2524 return $pos; 2525 $pos += 2; 2526 2527 $type = $this->_getByte($data, $pos); 2528 $pos += 1; 2529 $length = $this->_getShort($data, $pos); 2530 $pos += 2; 2531 2532 $basePos = $pos; 2533 $label = ''; 2534 2535 if (isset($IPTCTags[$type])) { 2536 $label = $IPTCTags[$type]; 2537 } else { 2538 $label = sprintf('IPTC_0x%02x', $type); 2539 } 2540 2541 if ($label != '') { 2542 if (isset($this->_info['iptc'][$label])) { 2543 if (!is_array($this->_info['iptc'][$label])) { 2544 $aux = array(); 2545 $aux[0] = $this->_info['iptc'][$label]; 2546 $this->_info['iptc'][$label] = $aux; 2547 } 2548 $this->_info['iptc'][$label][ count($this->_info['iptc'][$label]) ] = $this->_getFixedString($data, $pos, $length); 2549 } else { 2550 $this->_info['iptc'][$label] = $this->_getFixedString($data, $pos, $length); 2551 } 2552 } 2553 2554 $pos = $basePos + $length; // No padding 2555 } 2556 return $pos; 2557 } 2558 2559 /*************************************************************/ 2560 function & _createMarkerAdobe() { 2561 if (isset($this->_info['iptc'])) { 2562 if (!isset($this->_info['adobe'])) { 2563 $this->_info['adobe'] = array(); 2564 } 2565 if (!isset($this->_info['adobe']['raw'])) { 2566 $this->_info['adobe']['raw'] = array(); 2567 } 2568 if (!isset($this->_info['adobe']['raw']['8BIM_0x0404'])) { 2569 $this->_info['adobe']['raw']['8BIM_0x0404'] = array(); 2570 } 2571 $this->_info['adobe']['raw']['8BIM_0x0404']['type'] = 0x0404; 2572 $this->_info['adobe']['raw']['8BIM_0x0404']['header'] = "Caption"; 2573 $this->_info['adobe']['raw']['8BIM_0x0404']['data'] =& $this->_writeIPTC(); 2574 } 2575 2576 if (isset($this->_info['adobe']['raw']) && (count($this->_info['adobe']['raw']) > 0)) { 2577 $data = "Photoshop 3.0\0"; 2578 $pos = 14; 2579 2580 reset($this->_info['adobe']['raw']); 2581 foreach ($this->_info['adobe']['raw'] as $value){ 2582 $pos = $this->_write8BIM( 2583 $data, 2584 $pos, 2585 $value['type'], 2586 $value['header'], 2587 $value['data'] ); 2588 } 2589 } 2590 2591 return $data; 2592 } 2593 2594 /*************************************************************/ 2595 2596 /** 2597 * @param mixed $data 2598 * @param integer $pos 2599 * 2600 * @param string $type 2601 * @param string $header 2602 * @param mixed $value 2603 * 2604 * @return int|mixed 2605 */ 2606 function _write8BIM(&$data, $pos, $type, $header, &$value) { 2607 $signature = "8BIM"; 2608 2609 $pos = $this->_putString($data, $pos, $signature); 2610 $pos = $this->_putShort($data, $pos, $type); 2611 2612 $len = strlen($header); 2613 2614 $pos = $this->_putByte($data, $pos, $len); 2615 $pos = $this->_putString($data, $pos, $header); 2616 if (($len % 2) == 0) { // Even padding, including the length byte 2617 $pos = $this->_putByte($data, $pos, 0); 2618 } 2619 2620 $len = strlen($value); 2621 $pos = $this->_putLong($data, $pos, $len); 2622 $pos = $this->_putString($data, $pos, $value); 2623 if (($len % 2) != 0) { // Even padding 2624 $pos = $this->_putByte($data, $pos, 0); 2625 } 2626 return $pos; 2627 } 2628 2629 /*************************************************************/ 2630 function & _writeIPTC() { 2631 $data = " "; 2632 $pos = 0; 2633 2634 $IPTCNames =& $this->_iptcNameTags(); 2635 2636 foreach($this->_info['iptc'] as $label => $value) { 2637 $value =& $this->_info['iptc'][$label]; 2638 $type = -1; 2639 2640 if (isset($IPTCNames[$label])) { 2641 $type = $IPTCNames[$label]; 2642 } 2643 elseif (str_starts_with($label, 'IPTC_0x')) { 2644 $type = hexdec(substr($label, 7, 2)); 2645 } 2646 2647 if ($type != -1) { 2648 if (is_array($value)) { 2649 $vcnt = count($value); 2650 for ($i = 0; $i < $vcnt; $i++) { 2651 $pos = $this->_writeIPTCEntry($data, $pos, $type, $value[$i]); 2652 } 2653 } 2654 else { 2655 $pos = $this->_writeIPTCEntry($data, $pos, $type, $value); 2656 } 2657 } 2658 } 2659 2660 return $data; 2661 } 2662 2663 /*************************************************************/ 2664 2665 /** 2666 * @param mixed $data 2667 * @param integer $pos 2668 * 2669 * @param string $type 2670 * @param mixed $value 2671 * 2672 * @return int|mixed 2673 */ 2674 function _writeIPTCEntry(&$data, $pos, $type, &$value) { 2675 $pos = $this->_putShort($data, $pos, 0x1C02); 2676 $pos = $this->_putByte($data, $pos, $type); 2677 $pos = $this->_putShort($data, $pos, strlen($value)); 2678 $pos = $this->_putString($data, $pos, $value); 2679 2680 return $pos; 2681 } 2682 2683 /*************************************************************/ 2684 function _exifTagNames($mode) { 2685 $tags = array(); 2686 2687 if ($mode == 'ifd0') { 2688 $tags[0x010E] = 'ImageDescription'; 2689 $tags[0x010F] = 'Make'; 2690 $tags[0x0110] = 'Model'; 2691 $tags[0x0112] = 'Orientation'; 2692 $tags[0x011A] = 'XResolution'; 2693 $tags[0x011B] = 'YResolution'; 2694 $tags[0x0128] = 'ResolutionUnit'; 2695 $tags[0x0131] = 'Software'; 2696 $tags[0x0132] = 'DateTime'; 2697 $tags[0x013B] = 'Artist'; 2698 $tags[0x013E] = 'WhitePoint'; 2699 $tags[0x013F] = 'PrimaryChromaticities'; 2700 $tags[0x0211] = 'YCbCrCoefficients'; 2701 $tags[0x0212] = 'YCbCrSubSampling'; 2702 $tags[0x0213] = 'YCbCrPositioning'; 2703 $tags[0x0214] = 'ReferenceBlackWhite'; 2704 $tags[0x8298] = 'Copyright'; 2705 $tags[0x8769] = 'ExifIFDOffset'; 2706 $tags[0x8825] = 'GPSIFDOffset'; 2707 } 2708 if ($mode == 'ifd1') { 2709 $tags[0x00FE] = 'TIFFNewSubfileType'; 2710 $tags[0x00FF] = 'TIFFSubfileType'; 2711 $tags[0x0100] = 'TIFFImageWidth'; 2712 $tags[0x0101] = 'TIFFImageHeight'; 2713 $tags[0x0102] = 'TIFFBitsPerSample'; 2714 $tags[0x0103] = 'TIFFCompression'; 2715 $tags[0x0106] = 'TIFFPhotometricInterpretation'; 2716 $tags[0x0107] = 'TIFFThreshholding'; 2717 $tags[0x0108] = 'TIFFCellWidth'; 2718 $tags[0x0109] = 'TIFFCellLength'; 2719 $tags[0x010A] = 'TIFFFillOrder'; 2720 $tags[0x010E] = 'TIFFImageDescription'; 2721 $tags[0x010F] = 'TIFFMake'; 2722 $tags[0x0110] = 'TIFFModel'; 2723 $tags[0x0111] = 'TIFFStripOffsets'; 2724 $tags[0x0112] = 'TIFFOrientation'; 2725 $tags[0x0115] = 'TIFFSamplesPerPixel'; 2726 $tags[0x0116] = 'TIFFRowsPerStrip'; 2727 $tags[0x0117] = 'TIFFStripByteCounts'; 2728 $tags[0x0118] = 'TIFFMinSampleValue'; 2729 $tags[0x0119] = 'TIFFMaxSampleValue'; 2730 $tags[0x011A] = 'TIFFXResolution'; 2731 $tags[0x011B] = 'TIFFYResolution'; 2732 $tags[0x011C] = 'TIFFPlanarConfiguration'; 2733 $tags[0x0122] = 'TIFFGrayResponseUnit'; 2734 $tags[0x0123] = 'TIFFGrayResponseCurve'; 2735 $tags[0x0128] = 'TIFFResolutionUnit'; 2736 $tags[0x0131] = 'TIFFSoftware'; 2737 $tags[0x0132] = 'TIFFDateTime'; 2738 $tags[0x013B] = 'TIFFArtist'; 2739 $tags[0x013C] = 'TIFFHostComputer'; 2740 $tags[0x0140] = 'TIFFColorMap'; 2741 $tags[0x0152] = 'TIFFExtraSamples'; 2742 $tags[0x0201] = 'TIFFJFIFOffset'; 2743 $tags[0x0202] = 'TIFFJFIFLength'; 2744 $tags[0x0211] = 'TIFFYCbCrCoefficients'; 2745 $tags[0x0212] = 'TIFFYCbCrSubSampling'; 2746 $tags[0x0213] = 'TIFFYCbCrPositioning'; 2747 $tags[0x0214] = 'TIFFReferenceBlackWhite'; 2748 $tags[0x8298] = 'TIFFCopyright'; 2749 $tags[0x9286] = 'TIFFUserComment'; 2750 } elseif ($mode == 'exif') { 2751 $tags[0x829A] = 'ExposureTime'; 2752 $tags[0x829D] = 'FNumber'; 2753 $tags[0x8822] = 'ExposureProgram'; 2754 $tags[0x8824] = 'SpectralSensitivity'; 2755 $tags[0x8827] = 'ISOSpeedRatings'; 2756 $tags[0x8828] = 'OECF'; 2757 $tags[0x9000] = 'EXIFVersion'; 2758 $tags[0x9003] = 'DateTimeOriginal'; 2759 $tags[0x9004] = 'DateTimeDigitized'; 2760 $tags[0x9101] = 'ComponentsConfiguration'; 2761 $tags[0x9102] = 'CompressedBitsPerPixel'; 2762 $tags[0x9201] = 'ShutterSpeedValue'; 2763 $tags[0x9202] = 'ApertureValue'; 2764 $tags[0x9203] = 'BrightnessValue'; 2765 $tags[0x9204] = 'ExposureBiasValue'; 2766 $tags[0x9205] = 'MaxApertureValue'; 2767 $tags[0x9206] = 'SubjectDistance'; 2768 $tags[0x9207] = 'MeteringMode'; 2769 $tags[0x9208] = 'LightSource'; 2770 $tags[0x9209] = 'Flash'; 2771 $tags[0x920A] = 'FocalLength'; 2772 $tags[0x927C] = 'MakerNote'; 2773 $tags[0x9286] = 'UserComment'; 2774 $tags[0x9290] = 'SubSecTime'; 2775 $tags[0x9291] = 'SubSecTimeOriginal'; 2776 $tags[0x9292] = 'SubSecTimeDigitized'; 2777 $tags[0xA000] = 'FlashPixVersion'; 2778 $tags[0xA001] = 'ColorSpace'; 2779 $tags[0xA002] = 'PixelXDimension'; 2780 $tags[0xA003] = 'PixelYDimension'; 2781 $tags[0xA004] = 'RelatedSoundFile'; 2782 $tags[0xA005] = 'InteropIFDOffset'; 2783 $tags[0xA20B] = 'FlashEnergy'; 2784 $tags[0xA20C] = 'SpatialFrequencyResponse'; 2785 $tags[0xA20E] = 'FocalPlaneXResolution'; 2786 $tags[0xA20F] = 'FocalPlaneYResolution'; 2787 $tags[0xA210] = 'FocalPlaneResolutionUnit'; 2788 $tags[0xA214] = 'SubjectLocation'; 2789 $tags[0xA215] = 'ExposureIndex'; 2790 $tags[0xA217] = 'SensingMethod'; 2791 $tags[0xA300] = 'FileSource'; 2792 $tags[0xA301] = 'SceneType'; 2793 $tags[0xA302] = 'CFAPattern'; 2794 } elseif ($mode == 'interop') { 2795 $tags[0x0001] = 'InteroperabilityIndex'; 2796 $tags[0x0002] = 'InteroperabilityVersion'; 2797 $tags[0x1000] = 'RelatedImageFileFormat'; 2798 $tags[0x1001] = 'RelatedImageWidth'; 2799 $tags[0x1002] = 'RelatedImageLength'; 2800 } elseif ($mode == 'gps') { 2801 $tags[0x0000] = 'GPSVersionID'; 2802 $tags[0x0001] = 'GPSLatitudeRef'; 2803 $tags[0x0002] = 'GPSLatitude'; 2804 $tags[0x0003] = 'GPSLongitudeRef'; 2805 $tags[0x0004] = 'GPSLongitude'; 2806 $tags[0x0005] = 'GPSAltitudeRef'; 2807 $tags[0x0006] = 'GPSAltitude'; 2808 $tags[0x0007] = 'GPSTimeStamp'; 2809 $tags[0x0008] = 'GPSSatellites'; 2810 $tags[0x0009] = 'GPSStatus'; 2811 $tags[0x000A] = 'GPSMeasureMode'; 2812 $tags[0x000B] = 'GPSDOP'; 2813 $tags[0x000C] = 'GPSSpeedRef'; 2814 $tags[0x000D] = 'GPSSpeed'; 2815 $tags[0x000E] = 'GPSTrackRef'; 2816 $tags[0x000F] = 'GPSTrack'; 2817 $tags[0x0010] = 'GPSImgDirectionRef'; 2818 $tags[0x0011] = 'GPSImgDirection'; 2819 $tags[0x0012] = 'GPSMapDatum'; 2820 $tags[0x0013] = 'GPSDestLatitudeRef'; 2821 $tags[0x0014] = 'GPSDestLatitude'; 2822 $tags[0x0015] = 'GPSDestLongitudeRef'; 2823 $tags[0x0016] = 'GPSDestLongitude'; 2824 $tags[0x0017] = 'GPSDestBearingRef'; 2825 $tags[0x0018] = 'GPSDestBearing'; 2826 $tags[0x0019] = 'GPSDestDistanceRef'; 2827 $tags[0x001A] = 'GPSDestDistance'; 2828 } 2829 2830 return $tags; 2831 } 2832 2833 /*************************************************************/ 2834 function _exifTagTypes($mode) { 2835 $tags = array(); 2836 2837 if ($mode == 'ifd0') { 2838 $tags[0x010E] = array(2, 0); // ImageDescription -> ASCII, Any 2839 $tags[0x010F] = array(2, 0); // Make -> ASCII, Any 2840 $tags[0x0110] = array(2, 0); // Model -> ASCII, Any 2841 $tags[0x0112] = array(3, 1); // Orientation -> SHORT, 1 2842 $tags[0x011A] = array(5, 1); // XResolution -> RATIONAL, 1 2843 $tags[0x011B] = array(5, 1); // YResolution -> RATIONAL, 1 2844 $tags[0x0128] = array(3, 1); // ResolutionUnit -> SHORT 2845 $tags[0x0131] = array(2, 0); // Software -> ASCII, Any 2846 $tags[0x0132] = array(2, 20); // DateTime -> ASCII, 20 2847 $tags[0x013B] = array(2, 0); // Artist -> ASCII, Any 2848 $tags[0x013E] = array(5, 2); // WhitePoint -> RATIONAL, 2 2849 $tags[0x013F] = array(5, 6); // PrimaryChromaticities -> RATIONAL, 6 2850 $tags[0x0211] = array(5, 3); // YCbCrCoefficients -> RATIONAL, 3 2851 $tags[0x0212] = array(3, 2); // YCbCrSubSampling -> SHORT, 2 2852 $tags[0x0213] = array(3, 1); // YCbCrPositioning -> SHORT, 1 2853 $tags[0x0214] = array(5, 6); // ReferenceBlackWhite -> RATIONAL, 6 2854 $tags[0x8298] = array(2, 0); // Copyright -> ASCII, Any 2855 $tags[0x8769] = array(4, 1); // ExifIFDOffset -> LONG, 1 2856 $tags[0x8825] = array(4, 1); // GPSIFDOffset -> LONG, 1 2857 } 2858 if ($mode == 'ifd1') { 2859 $tags[0x00FE] = array(4, 1); // TIFFNewSubfileType -> LONG, 1 2860 $tags[0x00FF] = array(3, 1); // TIFFSubfileType -> SHORT, 1 2861 $tags[0x0100] = array(4, 1); // TIFFImageWidth -> LONG (or SHORT), 1 2862 $tags[0x0101] = array(4, 1); // TIFFImageHeight -> LONG (or SHORT), 1 2863 $tags[0x0102] = array(3, 3); // TIFFBitsPerSample -> SHORT, 3 2864 $tags[0x0103] = array(3, 1); // TIFFCompression -> SHORT, 1 2865 $tags[0x0106] = array(3, 1); // TIFFPhotometricInterpretation -> SHORT, 1 2866 $tags[0x0107] = array(3, 1); // TIFFThreshholding -> SHORT, 1 2867 $tags[0x0108] = array(3, 1); // TIFFCellWidth -> SHORT, 1 2868 $tags[0x0109] = array(3, 1); // TIFFCellLength -> SHORT, 1 2869 $tags[0x010A] = array(3, 1); // TIFFFillOrder -> SHORT, 1 2870 $tags[0x010E] = array(2, 0); // TIFFImageDescription -> ASCII, Any 2871 $tags[0x010F] = array(2, 0); // TIFFMake -> ASCII, Any 2872 $tags[0x0110] = array(2, 0); // TIFFModel -> ASCII, Any 2873 $tags[0x0111] = array(4, 0); // TIFFStripOffsets -> LONG (or SHORT), Any (one per strip) 2874 $tags[0x0112] = array(3, 1); // TIFFOrientation -> SHORT, 1 2875 $tags[0x0115] = array(3, 1); // TIFFSamplesPerPixel -> SHORT, 1 2876 $tags[0x0116] = array(4, 1); // TIFFRowsPerStrip -> LONG (or SHORT), 1 2877 $tags[0x0117] = array(4, 0); // TIFFStripByteCounts -> LONG (or SHORT), Any (one per strip) 2878 $tags[0x0118] = array(3, 0); // TIFFMinSampleValue -> SHORT, Any (SamplesPerPixel) 2879 $tags[0x0119] = array(3, 0); // TIFFMaxSampleValue -> SHORT, Any (SamplesPerPixel) 2880 $tags[0x011A] = array(5, 1); // TIFFXResolution -> RATIONAL, 1 2881 $tags[0x011B] = array(5, 1); // TIFFYResolution -> RATIONAL, 1 2882 $tags[0x011C] = array(3, 1); // TIFFPlanarConfiguration -> SHORT, 1 2883 $tags[0x0122] = array(3, 1); // TIFFGrayResponseUnit -> SHORT, 1 2884 $tags[0x0123] = array(3, 0); // TIFFGrayResponseCurve -> SHORT, Any (2^BitsPerSample) 2885 $tags[0x0128] = array(3, 1); // TIFFResolutionUnit -> SHORT, 1 2886 $tags[0x0131] = array(2, 0); // TIFFSoftware -> ASCII, Any 2887 $tags[0x0132] = array(2, 20); // TIFFDateTime -> ASCII, 20 2888 $tags[0x013B] = array(2, 0); // TIFFArtist -> ASCII, Any 2889 $tags[0x013C] = array(2, 0); // TIFFHostComputer -> ASCII, Any 2890 $tags[0x0140] = array(3, 0); // TIFFColorMap -> SHORT, Any (3 * 2^BitsPerSample) 2891 $tags[0x0152] = array(3, 0); // TIFFExtraSamples -> SHORT, Any (SamplesPerPixel - 3) 2892 $tags[0x0201] = array(4, 1); // TIFFJFIFOffset -> LONG, 1 2893 $tags[0x0202] = array(4, 1); // TIFFJFIFLength -> LONG, 1 2894 $tags[0x0211] = array(5, 3); // TIFFYCbCrCoefficients -> RATIONAL, 3 2895 $tags[0x0212] = array(3, 2); // TIFFYCbCrSubSampling -> SHORT, 2 2896 $tags[0x0213] = array(3, 1); // TIFFYCbCrPositioning -> SHORT, 1 2897 $tags[0x0214] = array(5, 6); // TIFFReferenceBlackWhite -> RATIONAL, 6 2898 $tags[0x8298] = array(2, 0); // TIFFCopyright -> ASCII, Any 2899 $tags[0x9286] = array(2, 0); // TIFFUserComment -> ASCII, Any 2900 } elseif ($mode == 'exif') { 2901 $tags[0x829A] = array(5, 1); // ExposureTime -> RATIONAL, 1 2902 $tags[0x829D] = array(5, 1); // FNumber -> RATIONAL, 1 2903 $tags[0x8822] = array(3, 1); // ExposureProgram -> SHORT, 1 2904 $tags[0x8824] = array(2, 0); // SpectralSensitivity -> ASCII, Any 2905 $tags[0x8827] = array(3, 0); // ISOSpeedRatings -> SHORT, Any 2906 $tags[0x8828] = array(7, 0); // OECF -> UNDEFINED, Any 2907 $tags[0x9000] = array(7, 4); // EXIFVersion -> UNDEFINED, 4 2908 $tags[0x9003] = array(2, 20); // DateTimeOriginal -> ASCII, 20 2909 $tags[0x9004] = array(2, 20); // DateTimeDigitized -> ASCII, 20 2910 $tags[0x9101] = array(7, 4); // ComponentsConfiguration -> UNDEFINED, 4 2911 $tags[0x9102] = array(5, 1); // CompressedBitsPerPixel -> RATIONAL, 1 2912 $tags[0x9201] = array(10, 1); // ShutterSpeedValue -> SRATIONAL, 1 2913 $tags[0x9202] = array(5, 1); // ApertureValue -> RATIONAL, 1 2914 $tags[0x9203] = array(10, 1); // BrightnessValue -> SRATIONAL, 1 2915 $tags[0x9204] = array(10, 1); // ExposureBiasValue -> SRATIONAL, 1 2916 $tags[0x9205] = array(5, 1); // MaxApertureValue -> RATIONAL, 1 2917 $tags[0x9206] = array(5, 1); // SubjectDistance -> RATIONAL, 1 2918 $tags[0x9207] = array(3, 1); // MeteringMode -> SHORT, 1 2919 $tags[0x9208] = array(3, 1); // LightSource -> SHORT, 1 2920 $tags[0x9209] = array(3, 1); // Flash -> SHORT, 1 2921 $tags[0x920A] = array(5, 1); // FocalLength -> RATIONAL, 1 2922 $tags[0x927C] = array(7, 0); // MakerNote -> UNDEFINED, Any 2923 $tags[0x9286] = array(7, 0); // UserComment -> UNDEFINED, Any 2924 $tags[0x9290] = array(2, 0); // SubSecTime -> ASCII, Any 2925 $tags[0x9291] = array(2, 0); // SubSecTimeOriginal -> ASCII, Any 2926 $tags[0x9292] = array(2, 0); // SubSecTimeDigitized -> ASCII, Any 2927 $tags[0xA000] = array(7, 4); // FlashPixVersion -> UNDEFINED, 4 2928 $tags[0xA001] = array(3, 1); // ColorSpace -> SHORT, 1 2929 $tags[0xA002] = array(4, 1); // PixelXDimension -> LONG (or SHORT), 1 2930 $tags[0xA003] = array(4, 1); // PixelYDimension -> LONG (or SHORT), 1 2931 $tags[0xA004] = array(2, 13); // RelatedSoundFile -> ASCII, 13 2932 $tags[0xA005] = array(4, 1); // InteropIFDOffset -> LONG, 1 2933 $tags[0xA20B] = array(5, 1); // FlashEnergy -> RATIONAL, 1 2934 $tags[0xA20C] = array(7, 0); // SpatialFrequencyResponse -> UNDEFINED, Any 2935 $tags[0xA20E] = array(5, 1); // FocalPlaneXResolution -> RATIONAL, 1 2936 $tags[0xA20F] = array(5, 1); // FocalPlaneYResolution -> RATIONAL, 1 2937 $tags[0xA210] = array(3, 1); // FocalPlaneResolutionUnit -> SHORT, 1 2938 $tags[0xA214] = array(3, 2); // SubjectLocation -> SHORT, 2 2939 $tags[0xA215] = array(5, 1); // ExposureIndex -> RATIONAL, 1 2940 $tags[0xA217] = array(3, 1); // SensingMethod -> SHORT, 1 2941 $tags[0xA300] = array(7, 1); // FileSource -> UNDEFINED, 1 2942 $tags[0xA301] = array(7, 1); // SceneType -> UNDEFINED, 1 2943 $tags[0xA302] = array(7, 0); // CFAPattern -> UNDEFINED, Any 2944 } elseif ($mode == 'interop') { 2945 $tags[0x0001] = array(2, 0); // InteroperabilityIndex -> ASCII, Any 2946 $tags[0x0002] = array(7, 4); // InteroperabilityVersion -> UNKNOWN, 4 2947 $tags[0x1000] = array(2, 0); // RelatedImageFileFormat -> ASCII, Any 2948 $tags[0x1001] = array(4, 1); // RelatedImageWidth -> LONG (or SHORT), 1 2949 $tags[0x1002] = array(4, 1); // RelatedImageLength -> LONG (or SHORT), 1 2950 } elseif ($mode == 'gps') { 2951 $tags[0x0000] = array(1, 4); // GPSVersionID -> BYTE, 4 2952 $tags[0x0001] = array(2, 2); // GPSLatitudeRef -> ASCII, 2 2953 $tags[0x0002] = array(5, 3); // GPSLatitude -> RATIONAL, 3 2954 $tags[0x0003] = array(2, 2); // GPSLongitudeRef -> ASCII, 2 2955 $tags[0x0004] = array(5, 3); // GPSLongitude -> RATIONAL, 3 2956 $tags[0x0005] = array(2, 2); // GPSAltitudeRef -> ASCII, 2 2957 $tags[0x0006] = array(5, 1); // GPSAltitude -> RATIONAL, 1 2958 $tags[0x0007] = array(5, 3); // GPSTimeStamp -> RATIONAL, 3 2959 $tags[0x0008] = array(2, 0); // GPSSatellites -> ASCII, Any 2960 $tags[0x0009] = array(2, 2); // GPSStatus -> ASCII, 2 2961 $tags[0x000A] = array(2, 2); // GPSMeasureMode -> ASCII, 2 2962 $tags[0x000B] = array(5, 1); // GPSDOP -> RATIONAL, 1 2963 $tags[0x000C] = array(2, 2); // GPSSpeedRef -> ASCII, 2 2964 $tags[0x000D] = array(5, 1); // GPSSpeed -> RATIONAL, 1 2965 $tags[0x000E] = array(2, 2); // GPSTrackRef -> ASCII, 2 2966 $tags[0x000F] = array(5, 1); // GPSTrack -> RATIONAL, 1 2967 $tags[0x0010] = array(2, 2); // GPSImgDirectionRef -> ASCII, 2 2968 $tags[0x0011] = array(5, 1); // GPSImgDirection -> RATIONAL, 1 2969 $tags[0x0012] = array(2, 0); // GPSMapDatum -> ASCII, Any 2970 $tags[0x0013] = array(2, 2); // GPSDestLatitudeRef -> ASCII, 2 2971 $tags[0x0014] = array(5, 3); // GPSDestLatitude -> RATIONAL, 3 2972 $tags[0x0015] = array(2, 2); // GPSDestLongitudeRef -> ASCII, 2 2973 $tags[0x0016] = array(5, 3); // GPSDestLongitude -> RATIONAL, 3 2974 $tags[0x0017] = array(2, 2); // GPSDestBearingRef -> ASCII, 2 2975 $tags[0x0018] = array(5, 1); // GPSDestBearing -> RATIONAL, 1 2976 $tags[0x0019] = array(2, 2); // GPSDestDistanceRef -> ASCII, 2 2977 $tags[0x001A] = array(5, 1); // GPSDestDistance -> RATIONAL, 1 2978 } 2979 2980 return $tags; 2981 } 2982 2983 /*************************************************************/ 2984 function _exifNameTags($mode) { 2985 $tags = $this->_exifTagNames($mode); 2986 return $this->_names2Tags($tags); 2987 } 2988 2989 /*************************************************************/ 2990 function _iptcTagNames() { 2991 $tags = array(); 2992 $tags[0x14] = 'SuplementalCategories'; 2993 $tags[0x19] = 'Keywords'; 2994 $tags[0x78] = 'Caption'; 2995 $tags[0x7A] = 'CaptionWriter'; 2996 $tags[0x69] = 'Headline'; 2997 $tags[0x28] = 'SpecialInstructions'; 2998 $tags[0x0F] = 'Category'; 2999 $tags[0x50] = 'Byline'; 3000 $tags[0x55] = 'BylineTitle'; 3001 $tags[0x6E] = 'Credit'; 3002 $tags[0x73] = 'Source'; 3003 $tags[0x74] = 'CopyrightNotice'; 3004 $tags[0x05] = 'ObjectName'; 3005 $tags[0x5A] = 'City'; 3006 $tags[0x5C] = 'Sublocation'; 3007 $tags[0x5F] = 'ProvinceState'; 3008 $tags[0x65] = 'CountryName'; 3009 $tags[0x67] = 'OriginalTransmissionReference'; 3010 $tags[0x37] = 'DateCreated'; 3011 $tags[0x0A] = 'CopyrightFlag'; 3012 3013 return $tags; 3014 } 3015 3016 /*************************************************************/ 3017 function & _iptcNameTags() { 3018 $tags = $this->_iptcTagNames(); 3019 return $this->_names2Tags($tags); 3020 } 3021 3022 /*************************************************************/ 3023 function _names2Tags($tags2Names) { 3024 $names2Tags = array(); 3025 3026 foreach($tags2Names as $tag => $name) { 3027 $names2Tags[$name] = $tag; 3028 } 3029 3030 return $names2Tags; 3031 } 3032 3033 /*************************************************************/ 3034 3035 /** 3036 * @param $data 3037 * @param integer $pos 3038 * 3039 * @return int 3040 */ 3041 function _getByte(&$data, $pos) { 3042 if (!isset($data[$pos])) { 3043 throw new Exception("Requested byte at ".$pos.". Reading outside of file's boundaries."); 3044 } 3045 3046 return ord($data[$pos]); 3047 } 3048 3049 /*************************************************************/ 3050 3051 /** 3052 * @param mixed $data 3053 * @param integer $pos 3054 * 3055 * @param mixed $val 3056 * 3057 * @return int 3058 */ 3059 function _putByte(&$data, $pos, $val) { 3060 $val = intval($val); 3061 3062 $data[$pos] = chr($val); 3063 3064 return $pos + 1; 3065 } 3066 3067 /*************************************************************/ 3068 function _getShort(&$data, $pos, $bigEndian = true) { 3069 if (!isset($data[$pos]) || !isset($data[$pos + 1])) { 3070 throw new Exception("Requested short at ".$pos.". Reading outside of file's boundaries."); 3071 } 3072 3073 if ($bigEndian) { 3074 return (ord($data[$pos]) << 8) 3075 + ord($data[$pos + 1]); 3076 } else { 3077 return ord($data[$pos]) 3078 + (ord($data[$pos + 1]) << 8); 3079 } 3080 } 3081 3082 /*************************************************************/ 3083 function _putShort(&$data, $pos = 0, $val = 0, $bigEndian = true) { 3084 $val = intval($val); 3085 3086 if ($bigEndian) { 3087 $data[$pos + 0] = chr(($val & 0x0000FF00) >> 8); 3088 $data[$pos + 1] = chr(($val & 0x000000FF) >> 0); 3089 } else { 3090 $data[$pos + 0] = chr(($val & 0x00FF) >> 0); 3091 $data[$pos + 1] = chr(($val & 0xFF00) >> 8); 3092 } 3093 3094 return $pos + 2; 3095 } 3096 3097 /*************************************************************/ 3098 3099 /** 3100 * @param mixed $data 3101 * @param integer $pos 3102 * 3103 * @param bool $bigEndian 3104 * 3105 * @return int 3106 */ 3107 function _getLong(&$data, $pos, $bigEndian = true) { 3108 // Assume that if the start and end bytes are defined, the bytes inbetween are defined as well. 3109 if (!isset($data[$pos]) || !isset($data[$pos + 3])){ 3110 throw new Exception("Requested long at ".$pos.". Reading outside of file's boundaries."); 3111 } 3112 if ($bigEndian) { 3113 return (ord($data[$pos]) << 24) 3114 + (ord($data[$pos + 1]) << 16) 3115 + (ord($data[$pos + 2]) << 8) 3116 + ord($data[$pos + 3]); 3117 } else { 3118 return ord($data[$pos]) 3119 + (ord($data[$pos + 1]) << 8) 3120 + (ord($data[$pos + 2]) << 16) 3121 + (ord($data[$pos + 3]) << 24); 3122 } 3123 } 3124 3125 /*************************************************************/ 3126 3127 /** 3128 * @param mixed $data 3129 * @param integer $pos 3130 * 3131 * @param mixed $val 3132 * @param bool $bigEndian 3133 * 3134 * @return int 3135 */ 3136 function _putLong(&$data, $pos, $val, $bigEndian = true) { 3137 $val = intval($val); 3138 3139 if ($bigEndian) { 3140 $data[$pos + 0] = chr(($val & 0xFF000000) >> 24); 3141 $data[$pos + 1] = chr(($val & 0x00FF0000) >> 16); 3142 $data[$pos + 2] = chr(($val & 0x0000FF00) >> 8); 3143 $data[$pos + 3] = chr(($val & 0x000000FF) >> 0); 3144 } else { 3145 $data[$pos + 0] = chr(($val & 0x000000FF) >> 0); 3146 $data[$pos + 1] = chr(($val & 0x0000FF00) >> 8); 3147 $data[$pos + 2] = chr(($val & 0x00FF0000) >> 16); 3148 $data[$pos + 3] = chr(($val & 0xFF000000) >> 24); 3149 } 3150 3151 return $pos + 4; 3152 } 3153 3154 /*************************************************************/ 3155 function & _getNullString(&$data, $pos) { 3156 $str = ''; 3157 $max = strlen($data); 3158 3159 while ($pos < $max) { 3160 if (!isset($data[$pos])) { 3161 throw new Exception("Requested null-terminated string at offset ".$pos.". File terminated before the null-byte."); 3162 } 3163 if (ord($data[$pos]) == 0) { 3164 return $str; 3165 } else { 3166 $str .= $data[$pos]; 3167 } 3168 $pos++; 3169 } 3170 3171 return $str; 3172 } 3173 3174 /*************************************************************/ 3175 function & _getFixedString(&$data, $pos, $length = -1) { 3176 if ($length == -1) { 3177 $length = strlen($data) - $pos; 3178 } 3179 3180 $rv = substr($data, $pos, $length); 3181 if (strlen($rv) != $length) { 3182 throw new ErrorException(sprintf( 3183 "JPEGMeta failed parsing image metadata of %s. Got %d instead of %d bytes at offset %d.", 3184 $this->_fileName, strlen($rv), $length, $pos 3185 ), 0, E_WARNING); 3186 } 3187 return $rv; 3188 } 3189 3190 /*************************************************************/ 3191 function _putString(&$data, $pos, &$str) { 3192 $len = strlen($str); 3193 for ($i = 0; $i < $len; $i++) { 3194 $data[$pos + $i] = $str[$i]; 3195 } 3196 3197 return $pos + $len; 3198 } 3199 3200 /*************************************************************/ 3201 function _hexDump(&$data, $start = 0, $length = -1) { 3202 if (($length == -1) || (($length + $start) > strlen($data))) { 3203 $end = strlen($data); 3204 } else { 3205 $end = $start + $length; 3206 } 3207 3208 $ascii = ''; 3209 $count = 0; 3210 3211 echo "<tt>\n"; 3212 3213 while ($start < $end) { 3214 if (($count % 16) == 0) { 3215 echo sprintf('%04d', $count) . ': '; 3216 } 3217 3218 $c = ord($data[$start]); 3219 $count++; 3220 $start++; 3221 3222 $aux = dechex($c); 3223 if (strlen($aux) == 1) 3224 echo '0'; 3225 echo $aux . ' '; 3226 3227 if ($c == 60) 3228 $ascii .= '<'; 3229 elseif ($c == 62) 3230 $ascii .= '>'; 3231 elseif ($c == 32) 3232 $ascii .= ' '; 3233 elseif ($c > 32) 3234 $ascii .= chr($c); 3235 else 3236 $ascii .= '.'; 3237 3238 if (($count % 4) == 0) { 3239 echo ' - '; 3240 } 3241 3242 if (($count % 16) == 0) { 3243 echo ': ' . $ascii . "<br>\n"; 3244 $ascii = ''; 3245 } 3246 } 3247 3248 if ($ascii != '') { 3249 while (($count % 16) != 0) { 3250 echo '-- '; 3251 $count++; 3252 if (($count % 4) == 0) { 3253 echo ' - '; 3254 } 3255 } 3256 echo ': ' . $ascii . "<br>\n"; 3257 } 3258 3259 echo "</tt>\n"; 3260 } 3261 3262 /*****************************************************************/ 3263} 3264 3265/* vim: set expandtab tabstop=4 shiftwidth=4: */ 3266