source: extensions/AMetaData/JpegMetaData/JpegMetaData.class.php @ 6722

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

bug:1686, feature:1718, feature:1719, feature:1688, feature:1692

  • Picture analysis finish with an Error 500 or with a problem of memory limit
  • Coding a DateTime class
  • Make JpegMetadata class tests images lighter
  • Improve performance when the database is filled
  • Add possibility for user to build their own tags
  • ajax management entirely rewritted
  • Property svn:executable set to *
File size: 24.3 KB
Line 
1<?php
2/**
3 * --:: JPEG MetaDatas ::-------------------------------------------------------
4 *
5 * Version : 1.0.1
6 * Date    : 2010-07-24
7 *
8 *  Author    : Grum
9 *   email    : grum at piwigo.org
10 *   website  : http://photos.grum.fr
11 *
12 *   << May the Little SpaceFrog be with you ! >>
13 *
14 * +-----------------------------------------------------------------------+
15 * | JpegMetaData - a PHP based Jpeg Metadata manager                      |
16 * +-----------------------------------------------------------------------+
17 * | Copyright(C) 2010  Grum - http://www.grum.fr                          |
18 * +-----------------------------------------------------------------------+
19 * | This program is free software; you can redistribute it and/or modify  |
20 * | it under the terms of the GNU General Public License as published by  |
21 * | the Free Software Foundation                                          |
22 * |                                                                       |
23 * | This program is distributed in the hope that it will be useful, but   |
24 * | WITHOUT ANY WARRANTY; without even the implied warranty of            |
25 * | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
26 * | General Public License for more details.                              |
27 * |                                                                       |
28 * | You should have received a copy of the GNU General Public License     |
29 * | along with this program; if not, write to the Free Software           |
30 * | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
31 * | USA.                                                                  |
32 * +-----------------------------------------------------------------------+
33 *
34 *
35 * +-:: HISTORY ::--------+-----------------------------------------------------
36 * |         |            |
37 * | Release | Date       |
38 * +---------+------------+-----------------------------------------------------
39 * | 0.1.0a  | 2009-12-26 |
40 * |         |            |
41 * | 1.0.0   |            | * first public release
42 * |         |            |
43 * | 1.0.1   | 2010-07-24 | * mantis bug:1686
44 * |         |            |   . bug reported on IfdReader
45 * |         |            |     When sub IFD (0x8769) refers to a sub IFD with
46 * |         |            |     an offset lower than the current IFD, the reader
47 * |         |            |     loops on the current data block and terminate
48 * |         |            |     with an error 500 ; fix consist to ignore this
49 * |         |            |     kind of offset, but it's not the right solution
50 * |         |            |     (right solution: to be able to read negative
51 * |         |            |     offset)
52 * |         |            | * mantis feature : 1719
53 * |         |            |   . Coding a DateTime class ; used only if there is
54 * |         |            |     no PHP built-in DateTime class
55 * |         |            |
56 * |         |            |
57 * |         |            |
58 * |         |            |
59 * |         |            |
60 * |         |            |
61 * |         |            |
62 * +---------+------------+-----------------------------------------------------
63 *
64 *
65 * -----------------------------------------------------------------------------
66 *
67 * References about definition & interpretation of metadata tags :
68 *  - EXIF 2.20 Specification    => http://www.exif.org/Exif2-2.PDF
69 *  - TIFF 6.0 Specification     => http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
70 *  - Exiftool by Phil Harvey    => http://www.sno.phy.queensu.ca/~phil/exiftool/
71 *                                  http://owl.phy.queensu.ca/~phil/exiftool/TagNames
72 *  - Exiv2 by Andreas Huggel    => http://www.exiv2.org/
73 *  - MetaData working group     => http://www.metadataworkinggroup.org/specs/
74 *  - Adobe XMP Developer Center => http://www.adobe.com/devnet/xmp/
75 *  - Gezuz                      => http://gezus.one.free.fr/?Plugin-EXIF-pour-Spip-1-9-2
76 *  - JPEG format                => http://crousseau.free.fr/imgfmt_jpeg.htm
77 *  - International Press Telecomunication Council specifications
78 *                               => http://www.iptc.org/
79 *  - IPTC headers structure     => http://www.codeproject.com/KB/graphics/iptc.aspx?msg=1014929
80 *  - CPAN                       => http://search.cpan.org/dist/Image-MetaData-JPEG/lib/Image/MetaData/JPEG/Structures.pod
81 *                               => http://search.cpan.org/~bettelli/Image-MetaData-JPEG/lib/Image/MetaData/JPEG/MakerNotes.pod
82 *
83 * -----------------------------------------------------------------------------
84 * To support internationalization the JpegMetaData package uses ".po" and ".mo"
85 * files, and use "php-gettext"
86 * ==> See files in External/php-gettext for more information about this project
87 * -----------------------------------------------------------------------------
88 *
89 * The JpegMetaData is the main class for reading metadata of a Jpeg file
90 *
91 * It provides two essentialy high level functions to read different kind of
92 * metadata (EXIF, IPTC, XMP) :
93 *  - (static) getTagList
94 *  - load
95 *  - getTags
96 *
97 * -----------------------------------------------------------------------------
98 *
99 * .. Notes ..
100 *
101 * About tags and translation in local lang
102 * With the 'getTags()' method, the JpegMetaData returns an array of Tag objects
103 * found in the jpeg file.
104 *
105 * A Tag object have 2 properties that can be translated into a local language :
106 *  - the name, getted with 'getName()'
107 *  - the valueLabel, getted with 'getLabel()'
108 *
109 * Theses properties ARE NOT translated automatically.
110 *
111 * You can translate it with the Locale class, by using the static method 'get'
112 *
113 * Example :
114 *  Locale::get($myTag->getName()) will return the translated name of the Tag
115 *  Locale::get($myTag->getLabel()) will return the translated value of the Tag
116 *
117 * ===========> See Tag.class.php to know more about the Tag class <============
118 * ========> See Locale.class.php to know more about the Locale class <=========
119 *
120 *
121 * -----------------------------------------------------------------------------
122 */
123
124  define("JPEG_METADATA_DIR", dirname(__FILE__)."/");
125
126  require_once(JPEG_METADATA_DIR."Common/DateTime.class.php");
127  require_once(JPEG_METADATA_DIR."Readers/JpegReader.class.php");
128  require_once(JPEG_METADATA_DIR."TagDefinitions/MagicTags.class.php");
129
130  class JpegMetaData
131  {
132    const TAGFILTER_KNOWN       = 0x01;
133    const TAGFILTER_IMPLEMENTED = 0x02;
134    const TAGFILTER_ALL         = 0x03;
135
136    const KEY_EXIF_TIFF = "exif.tiff";
137    const KEY_EXIF_EXIF = "exif.exif";
138    const KEY_EXIF_GPS  = "exif.gps";
139    const KEY_EXIF  = "exif";
140    const KEY_IPTC  = "iptc";
141    const KEY_XMP   = "xmp";
142    const KEY_MAGIC = "magic";
143
144    private $jpeg = null;
145    protected $tags = Array();
146    private $options = Array();
147
148    /**
149     * this static function returns an array of tags definitions
150     *
151     * the only parameter is an array to determine filter options
152     *
153     * ---------------------+---------------------------------------------------
154     * key                  | descriptions/values
155     * ---------------------+---------------------------------------------------
156     * filter               | Integer
157     *                      | This options is used to filter implemented tag
158     *                      |  JpegMetaData::TAGFILTER_ALL
159     *                      |  => returns all the tags
160     *                      |  JpegMetaData::TAGFILTER_IMPLEMENTED
161     *                      |  => returns only the implemented tags
162     *                      |
163     * optimizeIptcDateTime | Boolean
164     *                      | IPTC Date/Time are separated into 2 tags
165     *                      | if this option is set to true, only dates tags are
166     *                      | returned (assuming this option is used when an
167     *                      | image file is loaded)
168     *                      |
169     * exif                 | Boolean
170     * iptc                 | If set to true, the function returns all the tags
171     *                      | known for the specified type tag
172     * xmp                  |
173     * maker                | maker => returns specifics tags from all the known
174     * magic                |          makers
175     *                      |
176     * ---------------------+---------------------------------------------------
177     *
178     * returned value is an array
179     * each keys is a tag name and the associated value is a 2-level array
180     *  'implemented' => Boolean, allowing to know if the tags is implemented or
181     *                   not
182     *  'translatable'=> Boolean, allowing to know if the tag value can be
183     *                   translated
184     *  'name'        => String, the tag name translated in locale language
185     *
186     * @Param Array $options  (optional)
187     * @return Array(keyName => Array('implemented' => Boolean, 'name' => String))
188     */
189    static public function getTagList($options=Array())
190    {
191      $default=Array(
192        'filter' => self::TAGFILTER_ALL,
193        'optimizeIptcDateTime' => false,
194        'exif'  => true,
195        'iptc'  => true,
196        'xmp'   => true,
197        'maker' => true,
198        'magic' => true,
199      );
200
201      foreach($default as $key => $val)
202      {
203        if(array_key_exists($key, $options))
204          $default[$key]=$options[$key];
205      }
206
207      $list=Array();
208      $returned=Array();
209
210      if($default['exif'])
211      {
212        $list[]="exif";
213        $list[]="gps";
214      }
215
216      if($default['maker'])
217      {
218        $list[]=MAKER_PENTAX;
219        $list[]=MAKER_NIKON;
220        $list[]=MAKER_CANON;
221      }
222
223      if($default['iptc'])
224        $list[]="iptc";
225
226      if($default['xmp'])
227        $list[]="xmp";
228
229      if($default['magic'])
230        $list[]="magic";
231
232      foreach($list as $val)
233      {
234        unset($tmp);
235
236        switch($val)
237        {
238          case "exif":
239            $tmp=new IfdTags();
240            $schema="exif";
241            break;
242          case "gps":
243            $tmp=new GpsTags();
244            $schema="exif.gps";
245            break;
246          case "iptc":
247            $tmp=new IptcTags();
248            $schema="iptc";
249            break;
250          case "xmp":
251            $tmp=new XmpTags();
252            $schema="xmp";
253            break;
254          case "magic":
255            $tmp=new MagicTags();
256            $schema="magic";
257            break;
258          case MAKER_PENTAX:
259            include_once(JPEG_METADATA_DIR."TagDefinitions/PentaxTags.class.php");
260            $tmp=new PentaxTags();
261            $schema="exif.".MAKER_PENTAX;
262            break;
263          case MAKER_NIKON:
264            include_once(JPEG_METADATA_DIR."TagDefinitions/NikonTags.class.php");
265            $tmp=new NikonTags();
266            $schema="exif.".MAKER_NIKON;
267            break;
268          case MAKER_CANON:
269            include_once(JPEG_METADATA_DIR."TagDefinitions/CanonTags.class.php");
270            $tmp=new CanonTags();
271            $schema="exif.".MAKER_CANON;
272            break;
273          default:
274            $tmp=null;
275            $schema="?";
276            break;
277        }
278
279        if(!is_null($tmp))
280          foreach($tmp->getTags() as $key => $tag)
281          {
282            if(self::filter(true, $tag['implemented'], $default['filter']))
283            {
284              if(array_key_exists('tagName', $tag))
285                $name=$tag['tagName'];
286              else
287                $name=$key;
288
289              if(array_key_exists('schema', $tag) and $val=="exif")
290                $subSchema=".".$tag['schema'];
291              else
292                $subSchema="";
293
294              if($val=='xmp')
295                $keyName=$schema.$subSchema.".".$key;
296              else
297                $keyName=$schema.$subSchema.".".$name;
298              $returned[$keyName]=Array(
299                'implemented' => $tag['implemented'],
300                'translatable' => $tag['translatable'],
301                'name' => $name
302              );
303            }
304          }
305      }
306
307      ksort($returned);
308
309      return($returned);
310    }
311
312
313    /**
314     * the filter function is used by the classe to determine if a tag is
315     * filtered or not
316     *
317     * @Param Boolean $known
318     * @Param Boolean $implemented
319     * @Param Integer $filter
320     *
321     */
322    static public function filter($known, $implemented, $filter)
323    {
324      return(($known and (($filter & self::TAGFILTER_KNOWN) == self::TAGFILTER_KNOWN )) or
325                ($implemented and (($filter & self::TAGFILTER_IMPLEMENTED) == self::TAGFILTER_IMPLEMENTED )));
326    }
327
328    /**
329     * the constructor need an optional filename and options
330     *
331     * if no filename is given, you can use the "load" function after the object
332     * is instancied
333     *
334     * if no options are given, the class use the default values
335     *
336     * ---------------------+---------------------------------------------------
337     * key                  | descriptions/values
338     * ---------------------+---------------------------------------------------
339     * filter               | Integer
340     *                      | This options is used to filter implemented tag
341     *                      |  JpegMetaData::TAGFILTER_ALL
342     *                      |  => returns all the tags
343     *                      |  JpegMetaData::TAGFILTER_IMPLEMENTED
344     *                      |  => returns only the implemented tags, not
345     *                      |     implemented tag are excluded
346     *                      |  JpegMetaData::TAGFILTER_KNOWN
347     *                      |  => returns only the known tags (implemented or
348     *                      |     not), unknown tag are excluded
349     *                      |
350     * optimizeIptcDateTime | Boolean
351     *                      | IPTC Date/Time are separated into 2 tags
352     *                      | if this option is set to true, only dates tags are
353     *                      | returned (in this case, time is included is the
354     *                      | date)
355     *                      |
356     * exif                 | Boolean
357     * iptc                 | If set to true, the function returns all the tags
358     * xmp                  | known for the specified type tag
359     * magic                | the exif parameter include the maker tags
360     *                      |
361     * ---------------------+---------------------------------------------------
362     *
363     * @Param String $file    (optional)
364     * @Param Array  $options (optional)
365     *
366     */
367    function __construct($file = "", $options = Array())
368    {
369      $this->load($file, $options);
370    }
371
372    function __destruct()
373    {
374      $this->unsetAll();
375    }
376
377    /**
378     * load a file
379     *
380     * options values are the same than the constructor's options
381     *
382     * @Param String $file
383     * @Param Array  $options (optional)
384     *
385     */
386    public function load($file, $options = Array())
387    {
388      $this->unsetAll();
389
390      $this->initializeOptions($options);
391      $this->tags = Array();
392      $this->jpeg = new JpegReader($file);
393
394      if($this->jpeg->isLoaded() and $this->jpeg->isValid())
395      {
396        foreach($this->jpeg->getAppMarkerSegments() as $key => $appMarkerSegment)
397        {
398          if($appMarkerSegment->dataLoaded())
399          {
400            $data=$appMarkerSegment->getData();
401
402            if($data instanceof TiffReader)
403            {
404              /*
405               * Load Exifs tags from Tiff block
406               */
407              if($data->getNbIFDs()>0)
408              {
409                $this->loadIfdTags($data->getIFD(0), self::KEY_EXIF_TIFF);
410              }
411            }
412            elseif($data instanceof XmpReader)
413            {
414              /*
415               * Load Xmp tags from Xmp block
416               */
417              $this->loadTags($data->getTags(), self::KEY_XMP);
418            }
419            elseif($data instanceof IptcReader)
420            {
421              /*
422               * Load IPTC tags from IPTC block
423               */
424              if($this->options['optimizeIptcDateTime'])
425                $data->optimizeDateTime();
426
427              $this->loadTags($data->getTags(), self::KEY_IPTC);
428            }
429          }
430        }
431
432        if($this->options['magic'])
433        {
434          $this->processMagicTags();
435        }
436
437        ksort($this->tags);
438      }
439    }
440
441    /**
442     * This function returns an array of tags found in the loaded file
443     *
444     * It's possible to made a second selection to filter items
445     *
446     * ---------------------+---------------------------------------------------
447     * key                  | descriptions/values
448     * ---------------------+---------------------------------------------------
449     * tagFilter            | Integer
450     *                      | This options is used to filter implemented tag
451     *                      |  JpegMetaData::TAGFILTER_ALL
452     *                      |  => returns all the tags
453     *                      |  JpegMetaData::TAGFILTER_IMPLEMENTED
454     *                      |  => returns only the implemented tags, not
455     *                      |     implemented tag are excluded
456     *                      |  JpegMetaData::TAGFILTER_KNOWN
457     *                      |  => returns only the known tags (implemented or
458     *                      |     not), unknown tag are excluded
459     *                      |
460     * ---------------------+---------------------------------------------------
461     *
462     * Note, the filter is applied on loaded tags. If a filter was applied when
463     * the file was loaded, you cannot expand the tag list, only reduce
464     * example :
465     *  $jpegmd = new JpegMetadata($file, Array('filter' => JpegMetaData::TAGFILTER_IMPLEMENTED));
466     *     => the unknown tag are not loaded
467     *  $jpegmd->getTags(JpegMetaData::TAGFILTER_ALL)
468     *     => unknown tag will not be restitued because they are not loaded...
469     *
470     * the function returns an array of Tag.
471     *
472     *
473     * ===========> See the Tag.class.php to know all about a tag <=============
474     *
475     * @Param Integer $tagFilter (optional)
476     *
477     */
478    public function getTags($tagFilter = self::TAGFILTER_ALL)
479    {
480      $returned=Array();
481      foreach($this->tags as $key => $val)
482      {
483        if(self::filter($val->isKnown(), $val->isImplemented(), $tagFilter))
484        {
485          $returned[$key]=$val;
486        }
487      }
488      return($returned);
489    }
490
491    /**
492     * initialize the options...
493     *
494     * @Param Array $options (optional)
495     *
496     */
497    private function initializeOptions($options=Array())
498    {
499      $this->options = Array(
500        'filter' => self::TAGFILTER_ALL,
501        'optimizeIptcDateTime' => false,
502        'exif'  => true,
503        'iptc'  => true,
504        'xmp'   => true,
505        'magic' => true
506      );
507
508      foreach($this->options as $key => $val)
509      {
510        if(array_key_exists($key, $options))
511          $this->options[$key]=$options[$key];
512      }
513    }
514
515    /**
516     * load tags from an IFD structure
517     *
518     * see Tiff.class.php and IfdReader.class.php for more informations
519     *
520     * @Param IfdReader $ifd
521     * @Param String    $exifKey
522     *
523     */
524    private function loadIfdTags($ifd, $exifKey)
525    {
526      foreach($ifd->getTags() as $key => $tag)
527      {
528        if((self::filter($tag->getTag()->isKnown(), $tag->getTag()->isImplemented(), $this->options['filter'])) or
529           ($tag->getTag()->getName()=='Exif IFD Pointer' or
530            $tag->getTag()->getName()=='MakerNote' or
531            $tag->getTag()->getName()=='GPS IFD Pointer'))
532        {
533          /*
534           * only tag responding to the filter are selected
535           * note the tags 'Exif IFD Pointer', 'MakerNote' & 'GPS IFD Pointer'
536           * are not declared as implemented (otherwise they are visible with
537           * the static 'getTagList' function) but must be selected even if
538           * filter says "implemented only"
539           */
540          if($tag->getTag()->getLabel() instanceof IfdReader)
541          {
542            switch($tag->getTag()->getName())
543            {
544              case 'Exif IFD Pointer':
545                $exifKey2=self::KEY_EXIF_EXIF;
546                break;
547              case 'MakerNote':
548                $exifKey2=self::KEY_EXIF.".".$tag->getTag()->getLabel()->getMaker();
549                break;
550              case 'GPS IFD Pointer':
551                $exifKey2=self::KEY_EXIF_GPS;
552                break;
553              default:
554                $exifKey2=$exifKey;
555                break;
556            }
557            $this->loadIfdTags($tag->getTag()->getLabel(), $exifKey2);
558          }
559          else
560          {
561            $this->tags[$exifKey.".".$tag->getTag()->getName()]=$tag->getTag();
562          }
563        }
564      }
565    }
566
567    /**
568     * Used to load tags from an IPTc or XMP structure
569     *
570     * see IptcReader.class.php and XmpReader.class.php
571     *
572     * @Param Tag[]  $ifd
573     * @Param String $tagKey
574     *
575     */
576    private function loadTags($tags, $tagKey)
577    {
578      foreach($tags as $key => $tag)
579      {
580        if(self::filter($tag->isKnown(), $tag->isImplemented(), $this->options['filter']))
581        {
582          $this->tags[$tagKey.".".$tag->getName()]=$tag;
583        }
584      }
585    }
586
587    /**
588     * MagicTags are build with this function
589     */
590    protected function processMagicTags()
591    {
592      $magicTags=new MagicTags();
593
594      foreach($magicTags->getTags() as $key => $val)
595      {
596        $tag=new Tag($key,0,$key);
597
598        for($i=0; $i<count($val['tagValues']); $i++)
599        {
600          $found=true;
601          preg_match_all('/{([a-z0-9:\.\s\/]*)(\[.*\])?}/i', $val['tagValues'][$i], $returned, PREG_PATTERN_ORDER);
602          foreach($returned[1] as $testKey)
603          {
604            $found=$found & array_key_exists($testKey, $this->tags);
605          }
606          if(count($returned[1])==0) $found=false;
607
608          if($found)
609          {
610            $returned=trim(preg_replace_callback(
611                '/{([a-z0-9:\.\s\/\[\]]*)}/i',
612                Array(&$this, "processMagicTagsCB"),
613                $val['tagValues'][$i]
614            ));
615
616            $returned=$this->processSpecialMagicTag($key, $returned);
617
618            $tag->setValue($returned);
619            $tag->setLabel($returned);
620            $tag->setKnown(true);
621            $tag->setImplemented($val['implemented']);
622            $tag->setTranslatable($val['translatable']);
623
624            $i=count($val['tagValues']);
625          }
626        }
627
628        if($tag->isImplemented() and $found)
629        {
630          $this->tags["magic.".$key]=$tag;
631        }
632
633        unset($tag);
634      }
635      unset($magicTags);
636    }
637
638    /**
639     * this function is called by the processMagicTags to replace tagId by the
640     * tag values
641     *
642     * @param Array $matches : array[1] = the tagId
643     * @return String : the tag value
644     */
645    protected function processMagicTagsCB($matches)
646    {
647      $label="";
648      preg_match_all('/([a-z0-9:\.\s\/]*)\[(.*)\]/i', $matches[1], $result, PREG_PATTERN_ORDER);
649      if(count($result[0])>0)
650      {
651
652        if(array_key_exists($result[1][0], $this->tags))
653        {
654          $tag=$this->tags[$result[1][0]]->getLabel();
655
656          preg_match_all('/([a-z0-9:\.\s\/]*)\[(.*)\]/i', $result[2][0], $result2, PREG_PATTERN_ORDER);
657
658          if(count($result2[0])>0)
659          {
660            if(array_key_exists($result2[2][0], $tag[$result2[1][0]] ))
661              $label=$tag[$result2[1][0]][$result2[2][0]];
662          }
663          else
664          {
665            if(array_key_exists($result[2][0], $tag))
666              $label=$tag[$result[2][0]];
667          }
668        }
669      }
670      else
671      {
672        if(array_key_exists($matches[1], $this->tags))
673        {
674          $label=$this->tags[$matches[1]]->getLabel();
675        }
676      }
677
678      if($label instanceof DateTime)
679        return($label->format("Y-m-d H:i:s"));
680
681      $label=XmpTags::getAltValue($label, L10n::getLanguage());
682
683      if(is_array($label))
684        return(implode(", ", $label));
685
686      return(trim($label));
687    }
688
689    /**
690     *
691     *
692     */
693    protected function processSpecialMagicTag($key, $value)
694    {
695      switch($key)
696      {
697        case "GPS.LatitudeNum":
698        case "GPS.LongitudeNum":
699          preg_match_all('/(\d{1,3})°\s*(\d{1,2})\'(?:\s*(\d+(?:\.\d+)?)")?[\s\|\.]*(N|S|E|W)/i', $value, $result);
700          $num=(float)$result[1][0] + ((float) $result[2][0])/60;
701          if($result[3][0]!="") $num+= ((float) $result[3][0])/3600;
702          if($result[4][0]=="W" or $result[4][0]=="S") $num=-$num;
703          return($num);
704        default:
705          return($value);
706      }
707    }
708
709
710    /**
711     * used by the destructor to clean variables
712     */
713    private function unsetAll()
714    {
715      unset($this->tags);
716      unset($this->jpeg);
717      unset($this->options);
718    }
719
720
721  } // class JpegMetaData
722
723?>
Note: See TracBrowser for help on using the repository browser.