source: extensions/AMetaData/JpegMetaData/Readers/IfdReader.class.php @ 4904

Last change on this file since 4904 was 4904, checked in by grum, 14 years ago

Optimize some memory leak and some bugged lines of code

  • Property svn:executable set to *
File size: 21.7 KB
Line 
1<?php
2/*
3 * --:: JPEG MetaDatas ::-------------------------------------------------------
4 *
5 *  Author    : Grum
6 *   email    : grum at piwigo.org
7 *   website  : http://photos.grum.fr
8 *
9 *   << May the Little SpaceFrog be with you ! >>
10 *
11 *
12 * +-----------------------------------------------------------------------+
13 * | JpegMetaData - a PHP based Jpeg Metadata manager                      |
14 * +-----------------------------------------------------------------------+
15 * | Copyright(C) 2010  Grum - http://www.grum.fr                          |
16 * +-----------------------------------------------------------------------+
17 * | This program is free software; you can redistribute it and/or modify  |
18 * | it under the terms of the GNU General Public License as published by  |
19 * | the Free Software Foundation                                          |
20 * |                                                                       |
21 * | This program is distributed in the hope that it will be useful, but   |
22 * | WITHOUT ANY WARRANTY; without even the implied warranty of            |
23 * | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
24 * | General Public License for more details.                              |
25 * |                                                                       |
26 * | You should have received a copy of the GNU General Public License     |
27 * | along with this program; if not, write to the Free Software           |
28 * | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
29 * | USA.                                                                  |
30 * +-----------------------------------------------------------------------+
31 *
32 *
33 * -----------------------------------------------------------------------------
34 *
35 * The IfdReader class is the dedicated to read IFDs from a TIFF structure
36 *
37 * =====> See TiffReader.class.php to know more about a Tiff structure <========
38 *
39 *
40 * An IFD is formatted as this :
41 *  - number of IFD entries : 2 bytes
42 *  - entries               : 12 bytes x number of entries
43 *  - next IFD offset       : 4 bytes
44 *  - extra datas           : number of bytes depends of the stored extra datas
45 *
46 * ====> See IfdEntryReader.class.php to know more about an IFD structure <=====
47 *
48 * -----------------------------------------------------------------------------
49 *
50 * .. Notes ..
51 *
52 *
53 * The Ifdreader class is derived from the GenericReader class.
54 *
55 * ======> See GenericReader.class.php to know more about common methods <======
56 *
57 *
58 * Derived classes :
59 *  - GpsReader         (for exif GPS tags)
60 *  - MakerNotesReader  (for maker notes tags)
61 *  - PentaxReader      (for Pentax exif tags, in fact derived from
62 *                      MakerNotesReader)
63 *
64 *
65 * This class provides theses public functions :
66 *  - (static) convert
67 *  - getNextIFDOffset
68 *
69 * -----------------------------------------------------------------------------
70 */
71
72  require_once(JPEG_METADATA_DIR."Common/Data.class.php");
73  require_once(JPEG_METADATA_DIR."Common/MakerNotesSignatures.class.php");
74  require_once(JPEG_METADATA_DIR."Readers/GenericReader.class.php");
75  require_once(JPEG_METADATA_DIR."Readers/IfdEntryReader.class.php");
76  require_once(JPEG_METADATA_DIR."TagDefinitions/IfdTags.class.php");
77
78
79  class IfdReader extends GenericReader
80  {
81    protected $byteOrder = BYTE_ORDER_LITTLE_ENDIAN;
82
83    private $nextIFDOffset = 0;
84
85    private $dataOffset = 0;
86
87    /**
88     * The constructor needs, like the ancestor, the datas to be parsed
89     *
90     * Some datas are offfset on extra data, and this offset can be (some time)
91     * absolute inside the IFD, or relative. So, the offset of the IFD structure
92     * is needed
93     *
94     * The byte order of data is the same byte order than the TIFF structure
95     *
96     * @param String $data
97     * @param ULong $offset : offset of IFD block in the jpeg file
98     * @param String $byteOrder
99     */
100    function __construct($data, $offset, $byteOrder)
101    {
102      parent::__construct($data);
103
104      $this->data->setByteOrder($byteOrder);
105      $this->byteOrder=$byteOrder;
106      $this->dataOffset=$offset;
107
108      $this->skipHeader($this->headerSize);
109
110      $dataPointer = $this->data->offset();
111      /*
112       * number of entries is defined byte an UShort at the begining of the
113       * data structure
114       */
115      $this->nbEntries=$this->data->readUShort();
116
117      /*
118       * if the file is written in little_endian, we assume the maker note
119       * values are stored with little_endian byte order too
120       *
121       * in some case, software modify the exif and rewrite the file with the
122       * big_endian byte order. but the maker note stays in little_endian
123       *
124       * this code try to determine the maker note byte order : if number of
125       * entries is higher than 256, we can think that the maker note byte order
126       * is inverted...
127       * (the constructor for maker note is not overrided, so this trick is
128       * placed here)
129       *
130       */
131      if($this->nbEntries>0xff)
132      {
133        $this->data->seek($dataPointer);
134        if($this->byteOrder==BYTE_ORDER_BIG_ENDIAN)
135          $this->byteOrder = BYTE_ORDER_LITTLE_ENDIAN;
136        else
137          $this->byteOrder = BYTE_ORDER_BIG_ENDIAN;
138
139        $this->data->setByteOrder($this->byteOrder);
140        $this->nbEntries=$this->data->readUShort();
141        if($this->nbEntries>0xff)
142        {
143          /*
144           * if number of entries is always higher than 0xFF after reverting the
145           * byte order, set num entries to 0
146           * (at now, unable to manage this)
147           */
148          $this->nbEntries=0;
149        }
150
151      }
152
153      $this->initializeEntries();
154
155      /* Next IFD Offset is defined after the entries  */
156      $this->nextIFDOffset=$this->data->readULong();
157    }
158
159    function __destruct()
160    {
161      parent::__destruct();
162    }
163
164    /**
165     * return the offset for the next IFD block
166     * offset of next IFD is relative to the current TIFF block, not the Jpeg
167     * file
168     *
169     * @return ULong
170     */
171    public function getNextIFDOffset()
172    {
173      return($this->nextIFDOffset);
174    }
175
176    public function toString()
177    {
178      $returned="IFD Offset: ".sprintf("%08x", $this->dataOffset).
179                " ; NbEntries: ".sprintf("%02d", $this->nbEntries).
180                " ; next IFD Offset: 0x".sprintf("%08x", $this->nextIFDOffset);
181      return($returned);
182    }
183
184    /**
185     * initialize the definition for classic exif tags (in fact Tiff 6.0 and
186     * Exif 2.2 tags)
187     */
188    protected function initializeTagDef()
189    {
190      $this->tagDef = new IfdTags();
191    }
192
193    /**
194     * reads all the entries in the IFD block, and set the Tag properties for
195     * the entry.
196     *
197     * An entry is a IfdEntryReader object
198     *
199     * Add the entry to the entries array
200     *
201     */
202    protected function initializeEntries()
203    {
204      for($i=0;$i<$this->nbEntries;$i++)
205      {
206        $entry=new IfdEntryReader($this->data->readASCII(12), $this->byteOrder, $this->data, $this->dataOffset, $this->tagDef);
207        $this->setTagProperties($entry);
208        $this->entries[]=$entry;
209      }
210    }
211
212    /**
213     * Interprets the tag values into a 'human readable values'
214     *
215     * @param IfdEntryReader $entry
216     */
217    private function setTagProperties($entry)
218    {
219      /*
220       * if the given tag id is defined, analyzing its values
221       */
222      if($this->tagDef->tagIdExists($entry->getTagId()))
223      {
224        $tag=$this->tagDef->getTagById($entry->getTagId());
225
226        $entry->getTag()->setKnown(true);
227        $entry->getTag()->setName($tag['tagName']);
228        $entry->getTag()->setImplemented($tag['implemented']);
229        $entry->getTag()->setTranslatable($tag['translatable']);
230
231        /*
232         * if there is values defined for the tag, analyze it
233         */
234        if(array_key_exists('tagValues', $tag))
235        {
236          /*
237           * if the combiTag value equals 0 exploit it as this
238           */
239          if($tag['combiTag']==0 and !is_array($entry->getValue()))
240          {
241            if(array_key_exists($entry->getValue(), $tag['tagValues']))
242            {
243              $entry->getTag()->setLabel($tag['tagValues'][$entry->getValue()]);
244            }
245            else
246            {
247              $entry->getTag()->setLabel("[unknown value 0x".sprintf("%04x", $entry->getValue())."]");
248            }
249          }
250          else
251          {
252            /*
253             * the combiTag value does not equals 0, so exploit it as a combi tag
254             */
255            $combiValue=$this->processCombiTag($entry->getValue(), ($tag['combiTag']==0)?1:$tag['combiTag'] );
256            if(array_key_exists($combiValue, $tag['tagValues']))
257            {
258              $entry->getTag()->setLabel($tag['tagValues'][$combiValue]);
259            }
260            else
261            {
262              $entry->getTag()->setLabel("[unknown combi value 0x".sprintf("%0".(2*$tag['combiTag'])."x", $combiValue)."]");
263            }
264          }
265        }
266        else
267        {
268          /*
269           * there is no values defined for the tag, analyzing it with dedicated
270           * function
271           */
272          $entry->getTag()->setLabel($this->processSpecialTag($entry->getTagId(), $entry->getValue(), $entry->getType(), $entry->getExtraDataOffset()));
273        }
274      }
275    }
276
277    /**
278     * Some values are obtained after combination of values
279     */
280    private function processCombiTag($values, $combi)
281    {
282      if(is_array($values))
283      {
284        $returned=0;
285        if($combi<=count($values))
286        {
287          for($i=0;$i<$combi;$i++)
288          {
289            $returned+=$values[$i]*pow(2,8*($combi-$i-1));
290          }
291        }
292      }
293      else
294      {
295        $returned=$values;
296      }
297      return($returned);
298    }
299
300    /**
301     * skip IFD header, if any
302     * (used by maker sub IFD classes)
303     */
304    protected function skipHeader($headerSize=0)
305    {
306      $this->data->seek($headerSize);
307    }
308
309    /**
310     * this function do the interpretation of specials tags
311     * must be overrided by derived classes
312     *
313     * the function return the interpreted value for the tag
314     *
315     * @param $tagId             : the id of the tag
316     * @param $values            : 'raw' value to be interpreted
317     * @param UByte $type        : if needed (for IFD structure) the type of data
318     * @param ULong $valueOffset : if needed, the offset of data in the jpeg file
319     * @return String or Array or DateTime or Integer or Float...
320     */
321    protected function processSpecialTag($tagId, $values, $type, $valuesOffset=0)
322    {
323      switch($tagId)
324      {
325        /*
326         * Tags managed
327         */
328        case 0x0100: // ImageWidth, tag 0x0100
329        case 0x0101: // ImageHeight, tag 0x0101
330        case 0x0102: // BitsPerSample, tag 0x0102
331        case 0x0115: // SamplesPerPixel, tag 0x0115
332        case 0x0116: // RowsPerStrip, tag 0x0116
333        case 0x0117: // StripByteCounts, tag 0x0117
334        case 0x0201: // JPEGInterchangeFormat, tag 0x0201
335        case 0x0202: // JPEGInterchangeFormatLength, tag 0x0202
336        case 0x8824: // SpectralSensitivity, tag 0x8824
337        case 0x8827: // ISOSpeedRatings, tag 0x8827
338        case 0xA002: // PixelXDimension, tag 0xA002
339        case 0xA003: // PixelYDimension, tag 0xA003
340          $returned=$values;
341          break;
342        case 0x000b: // ProcessingSoftware, tag 0x000b
343        case 0x010D: // DocumentName, tag 0x010D
344        case 0x010E: // ImageDescription, tag 0x010E
345        case 0x0131: // Software, tag 0x0131
346        case 0x013B: // Artist, tag 0x013B
347        case 0x8298: // Copyright, tag 0x8298
348        case 0x9290: // SubsecTime, tag 0x9290
349        case 0x9291: // SubsecTimeOriginal, tag 0x9291
350        case 0x9292: // SubsecTimeDigitized, tag 0x9292
351        case 0xA004: // RelatedSoundFile, tag 0xA004
352          /*
353           * null terminated strings
354           */
355          $returned=ConvertData::toStrings($values);
356          break;
357        case 0x010F: // Make, tag 0x010F
358        case 0x0110: // Model, tag 0x0110
359          /* Make and Model are null terminated strings
360           * memorize the maker & camera from the exif tag : it's used to
361           * recognize the Canon camera (no header in the maker note)
362           */
363          $returned=ConvertData::toStrings($values);
364          MakerNotesSignatures::setExifMaker($returned);
365          break;
366        case 0x011A: // XResolution, tag 0x011A
367        case 0x011B: // YResolution, tag 0x011B
368          if($values[1]>0)
369            $returned=$values[0]/$values[1];
370          else
371            $returned=$values[0];
372          break;
373        case 0x0132: // DateTime, tag 0x0132
374        case 0x9003: // DateTimeOriginal, tag 0x9003
375        case 0x9004: // DateTimeDigitized, tag 0x9004
376          /* formatted as "YYYY:MM:DD HH:MM:SS\x00"
377           * if date is defined, returns a DateTime object
378           * otherwise return "unknown date"
379           */
380          $returned=ConvertData::toDateTime($values);
381          break;
382        case 0x0212: // YCbCrSubSampling, tag 0x0212
383          /* is a rationnal number
384           * [1, 1] = YCbCr4:4:4
385           * [1, 2] = YCbCr4:4:0
386           * [1, 4] = YCbCr4:4:1
387           * [2, 1] = YCbCr4:2:2
388           * [2, 2] = YCbCr4:2:0
389           * [2, 4] = YCbCr4:2:1
390           * [4, 1] = YCbCr4:1:1
391           * [4, 2] = YCbCr4:1:0
392           */
393          $tag=$this->tagDef->getTagById(0x0212);
394          if(array_key_exists($values[0], $tags['tagValues.special']))
395          {
396            if(array_key_exists($values[1], $tags['tagValues.special'][$values[0]]))
397            {
398              $returned=$tags['tagValues.special'][$values[0]][$values[1]];
399            }
400            else
401            {
402              $returned="unknown";
403            }
404          }
405          else
406          {
407            $returned="unknown";
408          }
409          break;
410        case 0x829A: // ExposureTime, tag 0x829A
411          if($values[1]==0) $values[1]=1;
412          $returned=ConvertData::toExposureTime($values[0]/$values[1]);
413          break;
414        case 0x829D: // FNumber, tag 0x829D
415          if($values[1]==0) $values[1]=1;
416          $returned=ConvertData::toFNumber($values[0]/$values[1]);
417          break;
418        case 0x8769: // Exif IFD Pointer, tag 0x8769
419          /*
420           * the tag 0x8769 value is an offset to an EXIF sub IFD
421           * the returned value is a parsed sub IFD
422           */
423          $returned=new IfdReader($this->data->readASCII(-1,$values-$this->dataOffset), $values, $this->byteOrder);
424          break;
425        case 0x8825: // GPS IFD Pointer, tag 0x8825
426          /*
427           * the tag 0x8825 value is an offset to an EXIF sub IFD
428           * the returned value is a parsed sub IFD
429           */
430          require_once(JPEG_METADATA_DIR."Readers/GpsReader.class.php");
431          $returned=new GpsReader($this->data->readASCII(-1,$values-$this->dataOffset), $values, $this->byteOrder);
432          break;
433        case 0x9000: // ExifVersion, tag 0x9000
434        case 0xA000: // FlashpixVersion, tag 0xa0000
435          $returned=(int)substr($values,0,2).".".(int)substr($values,2);
436          break;
437        case 0x9101: // ComponentsConfiguration, tag0x9101
438          switch($values)
439          {
440            case "\x01\x02\x03\x00":
441              $returned="YCbCr";
442              break;
443            case "\x04\x05\x06\x00":
444              $returned="RGB";
445              break;
446            default:
447              $returned="unknown";
448              break;
449          }
450          break;
451        case 0x9102: // CompressedBitsPerPixel, tag0x9102
452        case 0x9203: // BrightnessValue, tag 0x9203 (APEX format)
453          if($values[1]==0) $values[1]=1;
454          $returned=round($values[0]/$values[1],4);
455          break;
456        case 0x9201: // ShutterSpeedValue, tag0x9201
457          if($values[1]==0) $values[1]=1;
458          /*
459           * the formula to compute the shutter speed value is 1/pow(2, x)
460           *
461           * Because of rounding errors, the result obtained using this formula
462           * is sometimes incorrect.
463           *
464           * We consider that if the result is greater than the second, the
465           * result is rounded to 2 hundredths of a second (to avoid something
466           * like 3.0000124500015s)
467           *
468           * in other case (result is less than 1 second) there is no rules
469           * result can be strange, but for now I don't know how to resolve this
470           * problem
471           *
472           */
473
474          $value=1/pow(2, $values[0]/$values[1]);
475          if($value>1)
476          {
477            $value=round($value,2);
478          }
479          $returned=ConvertData::toExposureTime($value);
480          break;
481        case 0x9202: // ApertureValue, tag0x9202
482        case 0x9205: // MaxApertureValue, tag0x9205
483          if($values[1]==0) $values[1]=1;
484          $returned=ConvertData::toFNumber(pow(1.414213562, $values[0]/$values[1]));
485          break;
486        case 0x9204: // ExposureBiasValue, tag0x9204
487          if($values[1]==0) $values[1]=1;
488          $returned=ConvertData::toEV($values[0]/$values[1]);
489          break;
490        case 0x9206: // SubjectDistance, tag 0x9206
491          if($values[1]==0) $values[1]=1;
492          $returned=ConvertData::toDistance($values[0]/$values[1], "m");
493          break;
494        case 0x9209: // flash, tag 0x9209
495          $tag=$this->tagDef->getTagById(0x9209);
496          $returned=Array(
497            "computed" => (isset($tag['tagValues.computed'][$values])?$tag['tagValues.computed'][$values]:"unknown"),
498            "detail" => Array() );
499          foreach($tag['tagValues.specialNames'] as $key => $value)
500          {
501            $filter = $values & $key;
502            if(isset($tag['tagValues.specialValues'][$key][$filter]))
503              $returned["detail"][]=$tag['tagValues.specialValues'][$key][$filter];
504            else
505              $returned["detail"][]="unknown";
506          }
507          break;
508        case 0x920A: // FocalLength, tag 0x920A
509          if($values[1]==0) $values[1]=1;
510          $returned=ConvertData::toFocalLength($values[0]/$values[1]);
511          break;
512        case 0x927c: // MakerNote, tag 0x927c
513          /* try to return a specific maker sub ifd
514           *
515           * if $values is n
516           */
517          $makerSignature=MakerNotesSignatures::getMaker($values);
518          switch($makerSignature)
519          {
520            case MakerNotesSignatures::OlympusHeader:
521            case MakerNotesSignatures::Olympus2Header:
522              $returned="Olympus is not implemented yet";
523              break;
524            case MakerNotesSignatures::FujiFilmHeader:
525              $returned="FujiFilm is not implemented yet";
526              break;
527            case MakerNotesSignatures::Nikon2Header:
528            case MakerNotesSignatures::Nikon3Header:
529              require_once(JPEG_METADATA_DIR."Readers/NikonReader.class.php");
530              $returned=new NikonReader($values, $valuesOffset, $this->byteOrder, $makerSignature);
531              break;
532            case MakerNotesSignatures::PanasonicHeader:
533              $returned="Panasonic is not implemented yet";
534              break;
535            case MakerNotesSignatures::PentaxHeader:
536            case MakerNotesSignatures::Pentax2Header:
537              require_once(JPEG_METADATA_DIR."Readers/PentaxReader.class.php");
538              $returned=new PentaxReader($values, $valuesOffset, $this->byteOrder, $makerSignature);
539              break;
540            case MakerNotesSignatures::SigmaHeader:
541            case MakerNotesSignatures::Sigma2Header:
542              $returned="Sigma is not implemented yet";
543              break;
544            case MakerNotesSignatures::SonyHeader:
545              $returned="Sony is not implemented yet";
546              break;
547            default:
548              /*
549               * Canon maker notes don't have any header
550               * So, the only method to know if the maker note is from a Canon
551               * camera is looking the exif maker value equals "Canon" or
552               * the camera model contains "Canon"
553               */
554              if(preg_match("/.*canon.*/i",MakerNotesSignatures::getExifMaker()))
555              {
556                require_once(JPEG_METADATA_DIR."Readers/CanonReader.class.php");
557                $returned=new CanonReader($values, $valuesOffset, $this->byteOrder, "");
558              }
559              else
560              {
561                $returned="unknown maker => ".ConvertData::toHexDump($values, $type, 16);
562              }
563              break;
564          }
565          break;
566        case 0x9286: // UserComment, tag 0x9286
567          /*
568           * user comment format :
569           *  first 8 bytes : format type
570           *  other bytes : text
571           */
572          $returned=substr($values,8);
573          break;
574        case 0xA20B: // FlashEnergy, tag 0xA20B
575        case 0xA20E: // FocalPlaneXResolution, tag 0xA20E
576        case 0xA20F: // FocalPlaneYResolution, tag 0xA20F
577        case 0xA215: // ExposureIndex, tag 0xA20F
578        case 0xA404: // DigitalZoomRatio, tag 0xA404
579          if($values[1]==0) $values[1]=1;
580          $returned=round($values[0]/$values[1],2);
581          break;
582        case 0xA405: // FocalLengthIn35mmFilm, tag 0xA405
583          $returned=ConvertData::toFocalLength($values);
584          break;
585        /*
586         * Tags not managed
587         */
588        case 0x0111: // StripOffsets, tag 0x0111
589        case 0x012D: // TransferFunction, tag 0x012D
590        case 0x013E: // WhitePoint, tag 0x013E
591        case 0x013F: // PrimaryChromaticities, tag 0x013F
592        case 0x0211: // YCbCrCoefficients, tag 0x0211
593        case 0x0214: // ReferenceBlackWhite, tag 0x0214
594        case 0x8828: // ReferenceBlackWhite, tag 0x0214
595        case 0x9214: // SubjectArea, tag 0x9214
596        case 0xA20C: // SpatialFrequencyResponse, tag 0xA20C
597        case 0xA40B: // DeviceSettingDescription, tag 0xA40B
598        default:
599          $returned="Not yet implemented;".ConvertData::toHexDump($tagId, ByteType::USHORT)." => ".ConvertData::toHexDump($values, $type, 64);
600          break;
601      }
602      return($returned);
603    }
604  }
605
606
607?>
Note: See TracBrowser for help on using the repository browser.