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

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

Change Locale class by L10n class ; add Readers for Nikon and Canon cameras ; some bug corrected

  • Property svn:executable set to *
File size: 20.9 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    /**
160     * return the offset for the next IFD block
161     * offset of next IFD is relative to the current TIFF block, not the Jpeg
162     * file
163     *
164     * @return ULong
165     */
166    public function getNextIFDOffset()
167    {
168      return($this->nextIFDOffset);
169    }
170
171    public function toString()
172    {
173      $returned="IFD Offset: ".sprintf("%08x", $this->dataOffset).
174                " ; NbEntries: ".sprintf("%02d", $this->nbEntries).
175                " ; next IFD Offset: 0x".sprintf("%08x", $this->nextIFDOffset);
176      return($returned);
177    }
178
179    /**
180     * initialize the definition for classic exif tags (in fact Tiff 6.0 and
181     * Exif 2.2 tags)
182     */
183    protected function initializeTagDef()
184    {
185      $this->tagDef = new IfdTags();
186    }
187
188    /**
189     * reads all the entries in the IFD block, and set the Tag properties for
190     * the entry.
191     *
192     * An entry is a IfdEntryReader object
193     *
194     * Add the entry to the entries array
195     *
196     */
197    protected function initializeEntries()
198    {
199      for($i=0;$i<$this->nbEntries;$i++)
200      {
201        $entry=new IfdEntryReader($this->data->readASCII(12), $this->byteOrder, $this->data, $this->dataOffset, $this->tagDef);
202        $this->setTagProperties($entry);
203        $this->entries[]=$entry;
204      }
205    }
206
207    /**
208     * Interprets the tag values into a 'human readable values'
209     *
210     * @param IfdEntryReader $entry
211     */
212    private function setTagProperties($entry)
213    {
214      /*
215       * if the given tag id is defined, analyzing its values
216       */
217      if($this->tagDef->tagIdExists($entry->getTagId()))
218      {
219        $tag=$this->tagDef->getTagById($entry->getTagId());
220
221        $entry->getTag()->setKnown(true);
222        $entry->getTag()->setName($tag['tagName']);
223        $entry->getTag()->setImplemented($tag['implemented']);
224        $entry->getTag()->setTranslatable($tag['translatable']);
225
226        /*
227         * if there is values defined for the tag, analyze it
228         */
229        if(array_key_exists('tagValues', $tag))
230        {
231          /*
232           * if the combiTag value equals 0 exploit it as this
233           */
234          if($tag['combiTag']==0)
235          {
236            if(array_key_exists($entry->getValue(), $tag['tagValues']))
237            {
238              $entry->getTag()->setLabel($tag['tagValues'][$entry->getValue()]);
239            }
240            else
241            {
242              $entry->getTag()->setLabel("[unknown value 0x".sprintf("%04x", $entry->getValue())."]");
243            }
244          }
245          else
246          {
247            /*
248             * the combiTag value does not equals 0, so exploit it as a combi tag
249             */
250            $combiValue=$this->processCombiTag($entry->getValue(), $tag['combiTag']);
251            if(array_key_exists($combiValue, $tag['tagValues']))
252            {
253              $entry->getTag()->setLabel($tag['tagValues'][$combiValue]);
254            }
255            else
256            {
257              $entry->getTag()->setLabel("[unknown combi value 0x".sprintf("%0".(2*$tag['combiTag'])."x", $combiValue)."]");
258            }
259          }
260        }
261        else
262        {
263          /*
264           * there is no values defined for the tag, analyzing it with dedicated
265           * function
266           */
267          $entry->getTag()->setLabel($this->processSpecialTag($entry->getTagId(), $entry->getValue(), $entry->getType(), $entry->getExtraDataOffset()));
268        }
269      }
270    }
271
272    /**
273     * Some values are obtained after combination of values
274     */
275    private function processCombiTag($values, $combi)
276    {
277      if(is_array($values))
278      {
279        $returned=0;
280        if($combi<=count($values))
281        {
282          for($i=0;$i<$combi;$i++)
283          {
284            $returned+=$values[$i]*pow(2,8*($combi-$i-1));
285          }
286        }
287      }
288      else
289      {
290        $returned=$values;
291      }
292      return($returned);
293    }
294
295    /**
296     * skip IFD header, if any
297     * (used by maker sub IFD classes)
298     */
299    protected function skipHeader($headerSize=0)
300    {
301      $this->data->seek($headerSize);
302    }
303
304    /**
305     * this function do the interpretation of specials tags
306     * must be overrided by derived classes
307     *
308     * the function return the interpreted value for the tag
309     *
310     * @param $tagId             : the id of the tag
311     * @param $values            : 'raw' value to be interpreted
312     * @param UByte $type        : if needed (for IFD structure) the type of data
313     * @param ULong $valueOffset : if needed, the offset of data in the jpeg file
314     * @return String or Array or DateTime or Integer or Float...
315     */
316    protected function processSpecialTag($tagId, $values, $type, $valuesOffset=0)
317    {
318      switch($tagId)
319      {
320        /*
321         * Tags managed
322         */
323        case 0x0100: // ImageWidth, tag 0x0100
324        case 0x0101: // ImageHeight, tag 0x0101
325        case 0x0102: // BitsPerSample, tag 0x0102
326        case 0x0115: // SamplesPerPixel, tag 0x0115
327        case 0x0116: // RowsPerStrip, tag 0x0116
328        case 0x0117: // StripByteCounts, tag 0x0117
329        case 0x0201: // JPEGInterchangeFormat, tag 0x0201
330        case 0x0202: // JPEGInterchangeFormatLength, tag 0x0202
331        case 0x8824: // SpectralSensitivity, tag 0x8824
332        case 0x8827: // ISOSpeedRatings, tag 0x8827
333        case 0xA002: // PixelXDimension, tag 0xA002
334        case 0xA003: // PixelYDimension, tag 0xA003
335          $returned=$values;
336          break;
337        case 0x000b: // ProcessingSoftware, tag 0x000b
338        case 0x010D: // DocumentName, tag 0x010D
339        case 0x010E: // ImageDescription, tag 0x010E
340        case 0x0131: // Software, tag 0x0131
341        case 0x013B: // Artist, tag 0x013B
342        case 0x8298: // Copyright, tag 0x8298
343        case 0x9290: // SubsecTime, tag 0x9290
344        case 0x9291: // SubsecTimeOriginal, tag 0x9291
345        case 0x9292: // SubsecTimeDigitized, tag 0x9292
346        case 0xA004: // RelatedSoundFile, tag 0xA004
347          /*
348           * null terminated strings
349           */
350          $returned=ConvertData::toStrings($values);
351          break;
352        case 0x010F: // Make, tag 0x010F
353        case 0x0110: // Model, tag 0x0110
354          /* Make and Model are null terminated strings
355           * memorize the maker & camera from the exif tag : it's used to
356           * recognize the Canon camera (no header in the maker note)
357           */
358          $returned=ConvertData::toStrings($values);
359          MakerNotesSignatures::setExifMaker($returned);
360          break;
361        case 0x011A: // XResolution, tag 0x011A
362        case 0x011B: // YResolution, tag 0x011B
363          if($values[1]>0)
364            $returned=$values[0]/$values[1];
365          else
366            $returned=$values[0];
367          break;
368        case 0x0132: // DateTime, tag 0x0132
369        case 0x9003: // DateTimeOriginal, tag 0x9003
370        case 0x9004: // DateTimeDigitized, tag 0x9004
371          /* formatted as "YYYY:MM:DD HH:MM:SS\x00"
372           * if date is defined, returns a DateTime object
373           * otherwise return "unknown date"
374           */
375          $returned=ConvertData::toDateTime($values);
376          break;
377        case 0x0212: // YCbCrSubSampling, tag 0x0212
378          /* is a rationnal number
379           * [1, 1] = YCbCr4:4:4
380           * [1, 2] = YCbCr4:4:0
381           * [1, 4] = YCbCr4:4:1
382           * [2, 1] = YCbCr4:2:2
383           * [2, 2] = YCbCr4:2:0
384           * [2, 4] = YCbCr4:2:1
385           * [4, 1] = YCbCr4:1:1
386           * [4, 2] = YCbCr4:1:0
387           */
388          $tag=$this->tagDef->getTagById(0x0212);
389          if(array_key_exists($values[0], $tags['tagValues.special']))
390          {
391            if(array_key_exists($values[1], $tags['tagValues.special'][$values[0]]))
392            {
393              $returned=$tags['tagValues.special'][$values[0]][$values[1]];
394            }
395            else
396            {
397              $returned="unknown";
398            }
399          }
400          else
401          {
402            $returned="unknown";
403          }
404          break;
405        case 0x829A: // ExposureTime, tag 0x829A
406          if($values[1]==0) $values[1]=1;
407          $returned=ConvertData::toExposureTime($values[0]/$values[1]);
408          break;
409        case 0x829D: // FNumber, tag 0x829D
410          if($values[1]==0) $values[1]=1;
411          $returned=ConvertData::toFNumber($values[0]/$values[1]);
412          break;
413        case 0x8769: // Exif IFD Pointer, tag 0x8769
414          /*
415           * the tag 0x8769 value is an offset to an EXIF sub IFD
416           * the returned value is a parsed sub IFD
417           */
418          $returned=new IfdReader($this->data->readASCII(-1,$values-$this->dataOffset), $values, $this->byteOrder);
419          break;
420        case 0x8825: // GPS IFD Pointer, tag 0x8825
421          /*
422           * the tag 0x8825 value is an offset to an EXIF sub IFD
423           * the returned value is a parsed sub IFD
424           */
425          require_once(JPEG_METADATA_DIR."Readers/GpsReader.class.php");
426          $returned=new GpsReader($this->data->readASCII(-1,$values-$this->dataOffset), $values, $this->byteOrder);
427          break;
428        case 0x9000: // ExifVersion, tag 0x9000
429        case 0xA000: // FlashpixVersion, tag 0xa0000
430          $returned=(int)substr($values,0,2).".".(int)substr($values,2);
431          break;
432        case 0x9101: // ComponentsConfiguration, tag0x9101
433          switch($values)
434          {
435            case "\x01\x02\x03\x00":
436              $returned="YCbCr";
437              break;
438            case "\x04\x05\x06\x00":
439              $returned="RGB";
440              break;
441            default:
442              $returned="unknown";
443              break;
444          }
445          break;
446        case 0x9102: // CompressedBitsPerPixel, tag0x9102
447        case 0x9203: // BrightnessValue, tag 0x9203 (APEX format)
448          if($values[1]==0) $values[1]=1;
449          $returned=round($values[0]/$values[1],4);
450          break;
451        case 0x9201: // ShutterSpeedValue, tag0x9201
452          if($values[1]==0) $values[1]=1;
453          $returned=ConvertData::toExposureTime(1/round(pow(2, $values[0]/$values[1]),0));
454          break;
455        case 0x9202: // ApertureValue, tag0x9202
456        case 0x9205: // MaxApertureValue, tag0x9205
457          if($values[1]==0) $values[1]=1;
458          $returned=ConvertData::toFNumber(pow(1.414213562, $values[0]/$values[1]));
459          break;
460        case 0x9204: // ExposureBiasValue, tag0x9204
461          if($values[1]==0) $values[1]=1;
462          $returned=ConvertData::toEV($values[0]/$values[1]);
463          break;
464        case 0x9206: // SubjectDistance, tag 0x9206
465          if($values[1]==0) $values[1]=1;
466          $returned=ConvertData::toDistance($values[0]/$values[1], "m");
467          break;
468        case 0x9209: // flash, tag 0x9209
469          $tag=$this->tagDef->getTagById(0x9209);
470          $returned=Array(
471            "computed" => (isset($tag['tagValues.computed'][$values])?$tag['tagValues.computed'][$values]:"unknown"),
472            "detail" => Array() );
473          foreach($tag['tagValues.specialNames'] as $key => $value)
474          {
475            $filter = $values & $key;
476            if(isset($tag['tagValues.specialValues'][$key][$filter]))
477              $returned["detail"][]=$tag['tagValues.specialValues'][$key][$filter];
478            else
479              $returned["detail"][]="unknown";
480          }
481          break;
482        case 0x920A: // FocalLength, tag 0x920A
483          if($values[1]==0) $values[1]=1;
484          $returned=ConvertData::toFocalLength($values[0]/$values[1]);
485          break;
486        case 0x927c: // MakerNote, tag 0x927c
487          /* try to return a specific maker sub ifd
488           *
489           * if $values is n
490           */
491          $makerSignature=MakerNotesSignatures::getMaker($values);
492          switch($makerSignature)
493          {
494            case MakerNotesSignatures::OlympusHeader:
495            case MakerNotesSignatures::Olympus2Header:
496              $returned="Olympus is not implemented yet";
497              break;
498            case MakerNotesSignatures::FujiFilmHeader:
499              $returned="FujiFilm is not implemented yet";
500              break;
501            case MakerNotesSignatures::Nikon2Header:
502            case MakerNotesSignatures::Nikon3Header:
503              require_once(JPEG_METADATA_DIR."Readers/NikonReader.class.php");
504              $returned=new NikonReader($values, $valuesOffset, $this->byteOrder, $makerSignature);
505              break;
506            case MakerNotesSignatures::PanasonicHeader:
507              $returned="Panasonic is not implemented yet";
508              break;
509            case MakerNotesSignatures::PentaxHeader:
510            case MakerNotesSignatures::Pentax2Header:
511              require_once(JPEG_METADATA_DIR."Readers/PentaxReader.class.php");
512              $returned=new PentaxReader($values, $valuesOffset, $this->byteOrder, $makerSignature);
513              break;
514            case MakerNotesSignatures::SigmaHeader:
515            case MakerNotesSignatures::Sigma2Header:
516              $returned="Sigma is not implemented yet";
517              break;
518            case MakerNotesSignatures::SonyHeader:
519              $returned="Sony is not implemented yet";
520              break;
521            default:
522              /*
523               * Canon maker notes don't have any header
524               * So, the only method to know if the maker note is from a Canon
525               * camera is looking the exif maker value equals "Canon" or
526               * the camera model contains "Canon"
527               */
528              if(preg_match("/.*canon.*/i",MakerNotesSignatures::getExifMaker()))
529              {
530                require_once(JPEG_METADATA_DIR."Readers/CanonReader.class.php");
531                $returned=new CanonReader($values, $valuesOffset, $this->byteOrder, "");
532              }
533              else
534              {
535                $returned="unknown maker => ".ConvertData::toHexDump($values, $type, 16);
536              }
537              break;
538          }
539          break;
540        case 0x9286: // UserComment, tag 0x9286
541          /*
542           * user comment format :
543           *  first 8 bytes : format type
544           *  other bytes : text
545           */
546          $returned=substr($values,8);
547          break;
548        case 0xA20B: // FlashEnergy, tag 0xA20B
549        case 0xA20E: // FocalPlaneXResolution, tag 0xA20E
550        case 0xA20F: // FocalPlaneYResolution, tag 0xA20F
551        case 0xA215: // ExposureIndex, tag 0xA20F
552        case 0xA404: // DigitalZoomRatio, tag 0xA404
553          if($values[1]==0) $values[1]=1;
554          $returned=round($values[0]/$values[1],2);
555          break;
556        case 0xA405: // FocalLengthIn35mmFilm, tag 0xA405
557          $returned=ConvertData::toFocalLength($values);
558          break;
559        /*
560         * Tags not managed
561         */
562        case 0x0111: // StripOffsets, tag 0x0111
563        case 0x012D: // TransferFunction, tag 0x012D
564        case 0x013E: // WhitePoint, tag 0x013E
565        case 0x013F: // PrimaryChromaticities, tag 0x013F
566        case 0x0211: // YCbCrCoefficients, tag 0x0211
567        case 0x0214: // ReferenceBlackWhite, tag 0x0214
568        case 0x8828: // ReferenceBlackWhite, tag 0x0214
569        case 0x9214: // SubjectArea, tag 0x9214
570        case 0xA20C: // SpatialFrequencyResponse, tag 0xA20C
571        case 0xA40B: // DeviceSettingDescription, tag 0xA40B
572        default:
573          $returned="Not yet implemented;".ConvertData::toHexDump($tagId, ByteType::USHORT)." => ".ConvertData::toHexDump($values, $type, 64);
574          break;
575      }
576      return($returned);
577    }
578  }
579
580
581?>
Note: See TracBrowser for help on using the repository browser.