source: extensions/AMetaData/JpegMetaData/Readers/XmpReader.class.php @ 5237

Last change on this file since 5237 was 5237, checked in by grum, 15 years ago

fix GPS managment with XMP metadata ; and fix some minor bugs

  • Property svn:executable set to *
File size: 23.1 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 XmpReader class is the dedicated class to read XMP from the APP1 segment
36 *
37 * -----------------------------------------------------------------------------
38 *
39 * .. Notes ..
40 *
41 * The XmpReader class is derived from the GenericReader class.
42 *
43 * ======> See GenericReader.class.php to know more about common methods <======
44 *
45 * -----------------------------------------------------------------------------
46 */
47
48  require_once(JPEG_METADATA_DIR."Common/Data.class.php");
49  require_once(JPEG_METADATA_DIR."Common/XmlData.class.php");
50  require_once(JPEG_METADATA_DIR."Readers/GenericReader.class.php");
51  require_once(JPEG_METADATA_DIR."TagDefinitions/XmpTags.class.php");
52
53  class XmpReader extends GenericReader
54  {
55    private $xmlData = NULL;
56
57    private $xmpTag2Exif = NULL;
58
59    /**
60     * The constructor needs, like the ancestor, the datas to be parsed
61     *
62     * @param String $data
63     */
64    function __construct($data)
65    {
66      /**
67       * XML data are given from an APP1 segement.
68       *
69       * The XMP header ends with a \x00 char, so XML data can be extracted
70       * after the \x00 char
71       *
72       */
73      $this->data=explode("\x00", $data);
74      if(count($this->data)>1)
75      {
76        $this->data=$this->data[1];
77      }
78      else
79      {
80        $this->data="";
81      }
82
83      parent::__construct($this->data);
84
85      $this->xmpTag2Exif = new XmpTag2Exif("", 0, 0);
86
87      $this->xmlData = new XmlData($this->data->readASCII(-1,0));
88      $this->tagDef = new XmpTags();
89
90      $this->loadTags();
91
92      $this->isValid = $this->xmlData->isValid();
93      $this->isLoaded = $this->xmlData->isLoaded();
94
95    }
96
97    function __destruct()
98    {
99      unset($this->xmlData);
100      unset($this->xmpTag2Exif);
101      parent::__destruct();
102    }
103
104    public function toString()
105    {
106      $returned=$this->data->readASCII(-1,0);
107      return($returned);
108    }
109
110    /**
111     * This function load tags from the xml tree
112     */
113    private function loadTags()
114    {
115      if($this->xmlData->hasNodes())
116      {
117        $this->processNode($this->xmlData->getFirstNode());
118      }
119    }
120
121    /**
122     * this function analyze the node properties
123     *  - attributes are converted in Tag through a call to the addTag function
124     *  - childs are processed by a specific call to the processChildNode
125     *    function
126     *  - next node is processed by a recursive call
127     *
128     * @param XmlNode $node
129     */
130    private function processNode($node)
131    {
132      if(!is_null($node))
133      {
134        $node->setName($this->processNodeName($node->getName()));
135
136        foreach($node->getAttributes() as $key => $val)
137        {
138          $val['name']=$this->processNodeName($val['name']);
139          $this->addTag($val['name'], $val['value']);
140        }
141
142        if(!is_null($node->getValue()))
143        {
144          $this->addTag($node->getName(), $node->getValue());
145        }
146
147        if($node->hasChilds())
148        {
149          $this->processChildNode($node);
150        }
151
152        $this->processNode($node->getNextNode());
153      }
154    }
155
156
157    /**
158     * 'xap' schemas have to be interpreted as a 'xmp' schemas
159     * so, this function rename all 'xap' nodes in 'xmp' nodes
160     *
161     * @param String $nodeName
162     */
163    private function processNodeName($nodeName)
164    {
165      $name=preg_replace(Array("/^(xap:)/", "/^(xapMM:)/"), Array("xmp:", "xmpMM:"), $nodeName);
166
167      return($name);
168    }
169
170
171    /**
172     * childs node are 'seq', 'alt' or 'bag' type and needs a specific
173     * interpretation
174     *
175     * this function process this kind of data, and add it in the Tag entries
176     *
177     * if child node is not a special a specific node, process it as a normal
178     * node
179     *
180     * @param XmlNode $node
181     */
182    private function processChildNode($node)
183    {
184      switch($node->getName())
185      {
186        /*
187         * child must be 'rdf:Seq', 'rdf:Bag' or 'rdf:Alt'
188         */
189        case "dc:contributor" : // bag
190        case "dc:creator" : // seq
191        case "dc:date" : // seq
192        case "dc:description" : // alt
193        case "dc:language" : // bag
194        case "dc:publisher" : // bag
195        case "dc:relation" : // bag
196        case "dc:rights" : // alt
197        case "dc:subject" : // bag
198        case "dc:title" : // alt
199        case "dc:Type" : // bag
200        case "xmp:Advisory" : // bag
201        case "xmp:Identifier" : // bag
202        case "xmp:Thumbnails" : // alt
203        case "xmpRights:Owner" : // bag
204        case "xmpRights:UsageTerms" : // alt
205        case "xmpMM:History" : // seq
206        case "xmpMM:Versions" : // seq
207        case "xmpBJ:JobRef" : // bag
208        case "xmpTPg:Fonts" : // bag
209        case "xmpTPg:Colorants" : // seq
210        case "xmpTPg:PlateNames" : // seq
211        case "photoshop:SupplementalCategories" : // bag
212        case "crs:ToneCurve" : // seq
213        case "tiff:BitsPerSample" : // seq
214        case "tiff:YCbCrSubSampling" : // seq
215        case "tiff:TransferFunction" : // seq
216        case "tiff:WhitePoint" : // seq
217        case "tiff:PrimaryChromaticities" : // seq
218        case "tiff:YCbCrCoefficients" : // seq
219        case "tiff:ReferenceBlackWhite" : // seq
220        case "tiff:ImageDescription" : // alt
221        case "tiff:Copyright" : // alt
222        case "exif:ComponentsConfiguration" : // seq
223        case "exif:UserComment" : // alt
224        case "exif:ISOSpeedRatings" : // seq
225        case "exif:SubjectArea" : // seq
226        case "exif:SubjectLocation" : // seq
227        case "Iptc4xmpCore:Scene": //bag
228        case "Iptc4xmpCore:SubjectCode": //bag
229          $child=$node->getFirstChild();
230          switch($child->getName())
231          {
232            case "rdf:Seq":
233              $type="seq";
234              break;
235            case "rdf:Bag":
236              $type="bag";
237              break;
238            case "rdf:Alt":
239              $type="alt";
240              break;
241            default:
242              $type="n/a";
243              break;
244          }
245          if($type=="seq" or $type=="bag" or $type="alt")
246          {
247            $value=Array('type' => $type, 'values' => Array());
248            $childNode=$child->getFirstChild();
249            while(!is_null($childNode))
250            {
251              if($childNode->getName() == "rdf:li")
252              {
253                if($type=="alt")
254                {
255                  $attributes=$childNode->getAttributes();
256                  if(count($attributes)>0)
257                  {
258                    $attributes=$attributes[0];
259                  }
260                  else
261                  {
262                    $attributes="n/a";
263                  }
264                  $value['values'][]=Array('type' => $attributes, 'value' => $childNode->getValue());
265                }
266                else
267                {
268                  $value['values'][]=$childNode->getValue();
269                }
270              }
271              $childNode=$childNode->getNextNode();
272            }
273            $this->addTag($node->getName(), $value);
274          }
275          unset($child);
276          break;
277        default:
278          $this->processNode($node->getFirstChild());
279          break;
280      }
281    }
282
283    /**
284     * add a Tag to the entries.
285     * name and value are needed, the function made the rest (interpret the
286     * value into a 'human readable' value)
287     *
288     * @param String $name : the name of the tag
289     * @param $value       : can be of any type
290     */
291    private function addTag($name, $value)
292    {
293      $tag=new Tag($name, $value, $name);
294
295      if($this->tagDef->tagIdExists($name))
296      {
297        $tagProperties=$this->tagDef->getTagById($name);
298
299        $tag->setKnown(true);
300        $tag->setImplemented($tagProperties['implemented']);
301        $tag->setTranslatable($tagProperties['translatable']);
302
303
304        if(array_key_exists('name', $tagProperties))
305        {
306          $tag->setName($tagProperties['name']);
307        }
308
309        /*
310         * if there is values defined for the tag, analyze it
311         */
312        if(array_key_exists('tagValues', $tagProperties))
313        {
314          if(array_key_exists($value, $tagProperties['tagValues']))
315          {
316            $tag->setLabel($tagProperties['tagValues'][$value]);
317          }
318          else
319          {
320            $tag->setLabel("[unknown value '".$value."']");
321          }
322        }
323        else
324        {
325          /*
326           * there is no values defined for the tag, analyzing it with dedicated
327           * function
328           */
329          if(array_key_exists('exifTag', $tagProperties))
330          {
331            $tag->setLabel($this->xmp2Exif($name, $tagProperties['exifTag'], $value));
332          }
333          else
334          {
335            $tag->setLabel($this->processSpecialTag($name, $value));
336          }
337
338        }
339        unset($tagProperties);
340      }
341
342      $this->entries[]=$tag;
343      unset($tag);
344    }
345
346    /**
347     * this function do the interpretation of specials tags
348     *
349     * the function return the interpreted value for the tag
350     *
351     * @param $name              : the name of the tag
352     * @param $value             : 'raw' value to be interpreted
353     */
354    private function processSpecialTag($name, $value)
355    {
356      /*
357       * Note : exif & tiff values are not processed in this function
358       */
359      switch($name)
360      {
361        case "x:xmptk":
362        case "aux:Lens":
363        case "aux:Firmware":
364        case "aux:SerialNumber":
365        case "dc:coverage":
366        case "dc:format":
367        case "dc:identifier":
368        case "dc:relation":
369        case "dc:source":
370        case "dc:title":
371        case "dc:CreatorTool":
372        case "xmp:BaseURL":
373        case "xmp:CreatorTool":
374        case "xmp:Label":
375        case "xmp:Nickname":
376        case "xmp:Rating:":
377        case "xmpRights:Certificate":
378        case "xmpRights:Marked":
379        case "xmpRights:UsageTerms":
380        case "xmpRights:WebStatement":
381        case "xmpMM:DocumentID":
382        case "xmpMM:InstanceID":
383        case "xmpMM:Manager":
384        case "xmpMM:ManageTo":
385        case "xmpMM:ManageUI":
386        case "xmpMM:ManagerVariant":
387        case "xmpMM:RenditionParams":
388        case "xmpMM:VersionID":
389        case "xmpMM:LastURL":
390        case "photoshop:AuthorsPosition":
391        case "photoshop:CaptionWriter":
392        case "photoshop:Category":
393        case "photoshop:City":
394        case "photoshop:Country":
395        case "photoshop:Credit":
396        case "photoshop:Headline":
397        case "photoshop:Instructions":
398        case "photoshop:Source":
399        case "photoshop:State":
400        case "photoshop:TransmissionReference":
401        case "photoshop:ICCProfile":
402        case "crs:AutoBrightness":
403        case "crs:AutoContrast":
404        case "crs:AutoExposure":
405        case "crs:AutoShadows":
406        case "crs:BlueHue":
407        case "crs:BlueSaturation":
408        case "crs:Brightness":
409        case "crs:CameraProfile":
410        case "crs:ChromaticAberrationB":
411        case "crs:ChromaticAberrationR":
412        case "crs:ColorNoiseReduction":
413        case "crs:Contrast":
414        case "crs:CropTop":
415        case "crs:CropLeft":
416        case "crs:CropBottom":
417        case "crs:CropRight":
418        case "crs:CropAngle":
419        case "crs:CropWidth":
420        case "crs:CropHeight":
421        case "crs:Exposure":
422        case "crs:GreenHue":
423        case "crs:GreenSaturation":
424        case "crs:HasCrop":
425        case "crs:HasSettings":
426        case "crs:LuminanceSmoothing":
427        case "crs:RawFileName":
428        case "crs:RedHue":
429        case "crs:RedSaturation":
430        case "crs:Saturation":
431        case "crs:Shadows":
432        case "crs:ShadowTint":
433        case "crs:Sharpness":
434        case "crs:Temperature":
435        case "crs:Tint":
436        case "crs:Version":
437        case "crs:VignetteAmount":
438        case "crs:VignetteMidpoint":
439        case "Iptc4xmpCore:CountryCode":
440        case "Iptc4xmpCore:Location":
441        case "Iptc4xmpCore:CiEmailWork":
442        case "Iptc4xmpCore:CiUrlWork":
443        case "Iptc4xmpCore:CiTelWork":
444        case "Iptc4xmpCore:CiAdrExtadr":
445        case "Iptc4xmpCore:CiAdrPcode":
446        case "Iptc4xmpCore:CiAdrCity":
447        case "Iptc4xmpCore:CiAdrCtry":
448          $returned=$value;
449          break;
450        case "Iptc4xmpCore:IntellectualGenre":
451          $returned=explode(":", $value);
452          break;
453        case "exif:GPSLatitude":
454        case "exif:GPSLongitude":
455        case "exif:GPSDestLatitude":
456        case "exif:GPSDestLongitude":
457          $returned=Array('coord' => "", 'card'=>"");
458          preg_match_all('/(\d{1,3}),(\d{1,2})(?:\.(\d*)){0,1}(N|S|E|W)/', $value, $result);
459          $returned['coord']=$result[1][0]."° ".$result[2][0]."' ";
460          if(trim($result[3][0])!="")
461          {
462            $returned['coord'].= round(("0.".$result[3][0])*60,2)."\"";
463          }
464          switch($result[4][0])
465          {
466            case "N":
467              $returned['card']="North";
468              break;
469            case "S":
470              $returned['card']="South";
471              break;
472            case "E":
473              $returned['card']="East";
474              break;
475            case "W":
476              $returned['card']="West";
477              break;
478          }
479          $type=ByteType::UNDEFINED;
480          break;
481        case "xmp:CreateDate":
482        case "xmp:ModifyDate":
483        case "xmp:MetadataDate":
484        case "tiff:DateTime":
485        case "photoshop:DateCreated":
486        case "Iptc4xmpCore:DateCreated":
487          $returned=ConvertData::toDateTime($value);
488          break;
489        case "dc:contributor" : // bag
490        case "dc:creator" : // seq
491        case "dc:date" : // seq
492        case "dc:description" : // alt
493        case "dc:language" : // bag
494        case "dc:publisher" : // bag
495        case "dc:relation" : // bag
496        case "dc:rights" : // alt
497        case "dc:subject" : // bag
498        case "dc:title" : // alt
499        case "dc:Type" : // bag
500        case "xmp:Advisory" : // bag
501        case "xmp:Identifier" : // bag
502        case "xmp:Thumbnails" : // alt
503        case "xmpRights:Owner" : // bag
504        case "xmpRights:UsageTerms" : // alt
505        case "xmpMM:History" : // seq
506        case "xmpMM:Versions" : // seq
507        case "xmpBJ:JobRef" : // bag
508        case "xmpTPg:Fonts" : // bag
509        case "xmpTPg:Colorants" : // seq
510        case "xmpTPg:PlateNames" : // seq
511        case "photoshop:SupplementalCategories" : // bag
512        case "crs:ToneCurve" : // seq
513        case "tiff:BitsPerSample" : // seq
514        case "tiff:YCbCrSubSampling" : // seq
515        case "tiff:TransferFunction" : // seq
516        case "tiff:WhitePoint" : // seq
517        case "tiff:PrimaryChromaticities" : // seq
518        case "tiff:YCbCrCoefficients" : // seq
519        case "tiff:ReferenceBlackWhite" : // seq
520        case "tiff:ImageDescription" : // alt
521        case "tiff:Copyright" : // alt
522        case "exif:ComponentsConfiguration" : // seq
523        case "exif:UserComment" : // alt
524        case "exif:ISOSpeedRatings" : // seq
525        case "exif:SubjectArea" : // seq
526        case "exif:SubjectLocation" : // seq
527          $returned=$value;
528          break;
529        case "Iptc4xmpCore:Scene": //bag
530          $tag=$this->tagDef->getTagById('Iptc4xmpCore:Scene');
531          $returned=$value;
532          foreach($returned['values'] as $key=>$val)
533          {
534            if(array_key_exists($val, $tag['tagValues.special']))
535              $returned['values'][$key]=$tag['tagValues.special'][$val];
536          }
537          unset($tag);
538          break;
539        case "Iptc4xmpCore:SubjectCode": //bag
540          $returned=$value;
541          foreach($returned['values'] as $key=>$val)
542          {
543            $tmp=explode(":", $val);
544
545            $returned['values'][$key]=Array();
546
547            if(count($tmp)>=1)
548              $returned['values'][$key]['IPR']=$tmp[0];
549
550            if(count($tmp)>=2)
551              $returned['values'][$key]['subjectCode']=$tmp[1];
552
553            if(count($tmp)>=3)
554              $returned['values'][$key]['subjectName']=$tmp[2];
555
556            if(count($tmp)>=4)
557              $returned['values'][$key]['subjectMatterName']=$tmp[3];
558
559            if(count($tmp)>=5)
560              $returned['values'][$key]['subjectDetailName']=$tmp[4];
561
562            unset($tmp);
563          }
564          break;
565        default:
566          $returned="Not yet implemented; $name";
567          break;
568      }
569      return($returned);
570    }
571
572    /**
573     * this function convert the value from XMP 'exif' or XMP 'tiff' data by
574     * using an instance of a XmpTag2Exif object (using exif tag object allows
575     * not coding the same things twice)
576     */
577    private function xmp2Exif($tagName, $exifTag, $xmpValue)
578    {
579      switch($tagName)
580      {
581        /* integers */
582        case "tiff:ImageWidth":
583        case "tiff:ImageLength":
584        case "tiff:PhotometricInterpretation":
585        case "tiff:Orientation":
586        case "tiff:SamplesPerPixel":
587        case "tiff:PlanarConfiguration":
588        case "tiff:YCbCrSubSampling":
589        case "tiff:YCbCrPositioning":
590        case "tiff:ResolutionUnit":
591        case "exif:ColorSpace":
592        case "exif:PixelXDimension":
593        case "exif:PixelYDimension":
594        case "exif:MeteringMode":
595        case "exif:LightSource":
596        case "exif:FlashEnergy":
597        case "exif:FocalPlaneResolutionUnit":
598        case "exif:SensingMethod":
599        case "exif:FileSource":
600        case "exif:SceneType":
601        case "exif:CustomRendered":
602        case "exif:ExposureMode":
603        case "exif:Balance":
604        case "exif:FocalLengthIn35mmFilm":
605        case "exif:SceneCaptureType":
606        case "exif:GainControl":
607        case "exif:Contrast":
608        case "exif:Saturation":
609        case "exif:Sharpness":
610        case "exif:SubjectDistanceRange":
611        case "exif:GPSAltitudeRef":
612        case "exif:GPSDifferential":
613          $returned=(int)$xmpValue;
614          $type=ByteType::ULONG;
615          break;
616        /* specials */
617        case "tiff:BitsPerSample":
618        case "tiff:Compression":
619        case "tiff:TransferFunction":
620        case "tiff:WhitePoint":
621        case "tiff:PrimaryChromaticities":
622        case "tiff:YCbCrCoefficients":
623        case "tiff:ReferenceBlackWhite":
624        case "exif:ComponentsConfiguration":
625        case "exif:ExposureProgram":
626        case "exif:ISOSpeedRatings":
627        case "exif:OECF":
628        case "exif:Flash":
629        case "exif:SubjectArea":
630        case "exif:SpatialFrequencyResponse":
631        case "exif:SubjectLocation":
632        case "exif:CFAPattern":
633        case "exif:DeviceSettingDescription":
634          $returned=$xmpValue;
635          $type=ByteType::UNDEFINED;
636          break;
637        /* rational */
638        case "tiff:XResolution":
639        case "tiff:YResolution":
640        case "exif:CompressedBitsPerPixel":
641        case "exif:ExposureTime":
642        case "exif:FNumber":
643        case "exif:ShutterSpeedValue":
644        case "exif:ApertureValue":
645        case "exif:BrightnessValue":
646        case "exif:ExposureBiasValue":
647        case "exif:MaxApertureValue":
648        case "exif:SubjectDistance":
649        case "exif:FocalLength":
650        case "exif:FocalPlaneXResolution":
651        case "exif:FocalPlaneYResolution":
652        case "exif:ExposureIndex":
653        case "exif:DigitalZoomRatio":
654        case "exif:GPSAltitude":
655        case "exif:GPSDOP":
656        case "exif:GPSSpeed":
657        case "exif:GPSTrack":
658        case "exif:GPSImgDirection":
659        case "exif:GPSDestBearing":
660        case "exif:GPSDestDistance":
661          $computed=explode("/", $xmpValue);
662          $returned=Array((int)$computed[0], (int)$computed[1]);
663          $type=ByteType::URATIONAL;
664          unset($computed);
665          break;
666        /* dates & texts */
667        case "tiff:DateTime":
668        case "tiff:ImageDescription":
669        case "tiff:Make":
670        case "tiff:Model":
671        case "tiff:Software":
672        case "tiff:Artist":
673        case "tiff:Copyright":
674        case "exif:ExifVersion":
675        case "exif:FlashpixVersion":
676        case "exif:UserComment":
677        case "exif:RelatedSoundFile":
678        case "exif:DateTimeOriginal":
679        case "exif:DateTimeDigitized":
680        case "exif:SpectralSensitivity":
681        case "exif:ImageUniqueID":
682        case "exif:GPSVersionID":
683        case "exif:GPSTimeStamp":
684        case "exif:GPSSatellites":
685        case "exif:GPSStatus":
686        case "exif:GPSMeasureMode":
687        case "exif:GPSSpeedRef":
688        case "exif:GPSTrackRef":
689        case "exif:GPSImgDirectionRef":
690        case "exif:GPSMapDatum":
691        case "exif:GPSDestBearingRef":
692        case "exif:GPSDestDistanceRef":
693        case "exif:GPSProcessingMethod":
694        case "exif:GPSAreaInformation":
695          $returned=$xmpValue;
696          $type=ByteType::ASCII;
697          break;
698        default:
699          $returned="Unknown tag: $tagName => $xmpValue";
700          break;
701      }
702      if(is_array($returned))
703      {
704        if(array_key_exists('type', $returned))
705        {
706          if($returned['type']=='alt')
707          {
708          }
709          else
710          {
711            foreach($returned['values'] as $key => $val)
712            {
713              $returned['values'][$key]['value']=$this->xmpTag2Exif->convert($exifTag, $val['value'], $type);
714            }
715          }
716        }
717        else
718        {
719          return($this->xmpTag2Exif->convert($exifTag, $returned, $type));
720        }
721      }
722      return($returned);
723    }
724
725  }
726
727  /**
728   * The XmpTag2Exif is derived from the IfdReader class
729   *
730   * This function provides the public function 'convert', allowing to convert
731   * 'exif' datas in 'human readable' values
732   *
733   */
734  class XmpTag2Exif extends IfdReader
735  {
736    public function convert($exifTag, $value, $type)
737    {
738      return($this->processSpecialTag($exifTag, $value, $type));
739    }
740  }
741
742?>
Note: See TracBrowser for help on using the repository browser.