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

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

Translate JpegMetaData tags in french

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