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

Last change on this file since 6729 was 6729, checked in by grum, 11 years ago

feature:1777

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