* The SVGMetadataExtractor now based on XmlReader
authorDerk-Jan Hartman <hartman@users.mediawiki.org>
Thu, 4 Nov 2010 00:35:29 +0000 (00:35 +0000)
committerDerk-Jan Hartman <hartman@users.mediawiki.org>
Thu, 4 Nov 2010 00:35:29 +0000 (00:35 +0000)
* Retrieve title and desc of SVG files
* Retrieve metadata in raw form (not displayed)
* Detect SVG animations using SVG animation elements (http://www.w3.org/TR/SVG/animate.html)

RELEASE-NOTES
includes/media/SVG.php
includes/media/SVGMetadataExtractor.php

index 734683d..6a412f9 100644 (file)
@@ -197,6 +197,7 @@ LocalSettings.php. The specific bugs are listed below in the general notes.
 * (bug 10596) Allow installer to enable extensions already in extensions folder
 * (bug 17394) Make installer check for latest version against MediaWiki.org
 * (bug 20627) Installer should be in languages other than English
+* Support for metadata in SVG files (title, description).
 
 === Bug fixes in 1.17 ===
 * (bug 17560) Half-broken deletion moved image files to deletion archive
index 3858820..6c2d980 100644 (file)
@@ -12,7 +12,7 @@
  * @ingroup Media
  */
 class SvgHandler extends ImageHandler {
-       const SVG_METADATA_VERSION = 1;
+       const SVG_METADATA_VERSION = 2;
 
        function isEnabled() {
                global $wgSVGConverters, $wgSVGConverter;
@@ -32,8 +32,15 @@ class SvgHandler extends ImageHandler {
                return true;
        }
 
-       function isAnimatedImage( $image ) {
+       function isAnimatedImage( $file ) {
                # TODO: detect animated SVGs
+               $metadata = $file->getMetadata();
+               if ( $metadata ) {
+                       $metadata = $this->unpackMetadata( $metadata );
+                       if( isset( $metadata['animated'] ) ) {
+                               return $metadata['animated'];
+                       }
+               }
                return false;
        }
 
@@ -72,7 +79,7 @@ class SvgHandler extends ImageHandler {
                        return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
                                wfMsg( 'thumbnail_dest_directory' ) );
                }
-               
+
                $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight );
                if( $status === true ) {
                        return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
@@ -80,7 +87,7 @@ class SvgHandler extends ImageHandler {
                        return $status; // MediaTransformError
                }
        }
-       
+
        /*
        * Transform an SVG file to PNG
        * This function can be called outside of thumbnail contexts
@@ -142,10 +149,6 @@ class SvgHandler extends ImageHandler {
                        $wgLang->formatSize( $file->getSize() ) );
        }
 
-       function formatMetadata( $file ) {
-               return false;
-       }
-       
        function getMetadata( $file, $filename ) {
                $metadata = array();
                try {
@@ -158,7 +161,7 @@ class SvgHandler extends ImageHandler {
                $metadata['version'] = self::SVG_METADATA_VERSION;
                return serialize( $metadata );
        }
-       
+
        function unpackMetadata( $metadata ) {
                $unser = @unserialize( $metadata );
                if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
@@ -175,4 +178,44 @@ class SvgHandler extends ImageHandler {
        function isMetadataValid( $image, $metadata ) {
                return $this->unpackMetadata( $metadata ) !== false;
        }
+
+       function visibleMetadataFields() {
+               $fields = array( 'title', 'description', 'animated' );
+               return $fields;
+       }
+
+       function formatMetadata( $file ) {
+               $result = array(
+                       'visible' => array(),
+                       'collapsed' => array()
+               );
+               $metadata = $file->getMetadata();
+               if ( !$metadata ) {
+                       return false;
+               }
+               $metadata = $this->unpackMetadata( $metadata );
+               if ( !$metadata ) {
+                       return false;
+               }
+               unset( $metadata['version'] );
+               unset( $metadata['metadata'] ); /* non-formatted XML */
+
+               /* TODO: add a formatter
+               $format = new FormatSVG( $metadata );
+               $formatted = $format->getFormattedData();
+               */
+
+               // Sort fields into visible and collapsed
+               $visibleFields = $this->visibleMetadataFields();
+               foreach ( $metadata as $name => $value ) {
+                       $tag = strtolower( $name );
+                       self::addMeta( $result,
+                               in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
+                               'svg',
+                               $tag,
+                               $value
+                       );
+               }
+               return $result;
+       }
 }
index 7862225..66dc125 100644 (file)
 /**
  * SVGMetadataExtractor.php
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Media
+ * @author Derk-Jan Hartman <hartman _at_ videolan d0t org>
+ * @author Brion Vibber
+ * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
  */
 
 class SVGMetadataExtractor {
        static function getMetadata( $filename ) {
-               $filter = new XmlSizeFilter();
-               $xml = new XmlTypeCheck( $filename, array( $filter, 'filter' ) );
-               if( $xml->wellFormed ) {
-                       return array(
-                               'width' => $filter->width,
-                               'height' => $filter->height
-                       );
-               }
+               $svg = new SVGReader( $filename );
+               return $svg->getMetadata();
        }
 }
 
-class XmlSizeFilter {
+class SVGReader {
        const DEFAULT_WIDTH = 512;
        const DEFAULT_HEIGHT = 512;
-       var $first = true;
-       var $width = self::DEFAULT_WIDTH;
-       var $height = self::DEFAULT_HEIGHT;
-       function filter( $name, $attribs ) {
-               if( $this->first ) {
-                       $defaultWidth = self::DEFAULT_WIDTH;
-                       $defaultHeight = self::DEFAULT_HEIGHT;
-                       $aspect = 1.0;
-                       $width = null;
-                       $height = null;
-                       
-                       if( isset( $attribs['viewBox'] ) ) {
-                               // min-x min-y width height
-                               $viewBox = preg_split( '/\s+/', trim( $attribs['viewBox'] ) );
-                               if( count( $viewBox ) == 4 ) {
-                                       $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
-                                       $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
-                                       if( $viewWidth > 0 && $viewHeight > 0 ) {
-                                               $aspect = $viewWidth / $viewHeight;
-                                               $defaultHeight = $defaultWidth / $aspect;
-                                       }
-                               }
+
+       private $reader = null;
+       private $mDebug = false;
+       private $metadata = Array();
+
+       /**
+        * Constructor
+        *
+        * Creates an SVGReader drawing from the source provided
+        * @param $source String: URI from which to read
+        */
+       function __construct( $source ) {
+               $this->reader = new XMLReader();
+               $this->reader->open( $source );
+
+               $this->metadata['width'] = self::DEFAULT_WIDTH;
+               $this->metadata['height'] = self::DEFAULT_HEIGHT;
+
+               $this->read();
+       }
+
+       /*
+        * @return Array with the known metadata
+        */
+       public function getMetadata() {
+               return $this->metadata;
+       }
+
+       /*
+        * Read the SVG
+        */
+       public function read() {
+               $this->reader->read();
+
+               if ( $this->reader->name != 'svg' ) {
+                       throw new MWException( "Expected <svg> tag, got ".
+                               $this->reader->name );
+               }
+               $this->debug( "<svg> tag is correct." );
+
+               $this->debug( "Starting primary dump processing loop." );
+               $this->handleSVGAttribs();
+               $exitDepth =  $this->reader->depth;
+
+               $keepReading = $this->reader->read();
+               $skip = false;
+               while ( $keepReading ) {
+                       $tag = $this->reader->name;
+                       $type = $this->reader->nodeType;
+
+                       $this->debug( "$tag" );
+
+                       if ( $tag == 'svg' && $type == XmlReader::END_ELEMENT && $this->reader->depth <= $exitDepth ) {
+                               break;
+                       } elseif ( $tag == 'title' ) {
+                               $this->readField( $tag, 'title' );
+                       } elseif ( $tag == 'desc' ) {
+                               $this->readField( $tag, 'description' );
+                       } elseif ( $tag == 'metadata' && $type == XmlReader::ELEMENT ) {
+                               $this->readXml( $tag, 'metadata' );
+                       } elseif ( $tag !== '#text' ) {
+                               $this->debug( "Unhandled top-level XML tag $tag" );
+                               $this->animateFilter( $tag );
+                               //$skip = true;
                        }
-                       if( isset( $attribs['width'] ) ) {
-                               $width = $this->scaleSVGUnit( $attribs['width'], $defaultWidth );
+
+                       if ($skip) {
+                               $keepReading = $this->reader->next();
+                               $skip = false;
+                               $this->debug( "Skip" );
+                       } else {
+                               $keepReading = $this->reader->read();
                        }
-                       if( isset( $attribs['height'] ) ) {
-                               $height = $this->scaleSVGUnit( $attribs['height'], $defaultHeight );
+               }
+
+               return true;
+       }
+
+       /*
+        * Read a textelement from an element
+        *
+        * @param String $name of the element that we are reading from
+        * @param String $metafield that we will fill with the result
+        */
+       private function readField( $name, $metafield=null ) {
+               $this->debug ( "Read field $metafield" );
+               if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
+                       return;
+               }
+               $keepReading = $this->reader->read();
+               while( $keepReading ) {
+                       if( $this->reader->name == $name && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
+                               $keepReading = false;
+                               break;
+                       } elseif( $this->reader->nodeType == XmlReader::TEXT ){
+                               $this->metadata[$metafield] = $this->reader->value;
                        }
-                       
-                       if( !isset( $width ) && !isset( $height ) ) {
-                               $width = $defaultWidth;
-                               $height = $width / $aspect;
-                       } elseif( isset( $width ) && !isset( $height ) ) {
-                               $height = $width / $aspect;
-                       } elseif( isset( $height ) && !isset( $width ) ) {
-                               $width = $height * $aspect;
+                       $keepReading = $this->reader->read();
+               }
+       }
+
+       /*
+        * Read an XML snippet from an element
+        *
+        * @param String $metafield that we will fill with the result
+        */
+       private function readXml( $metafield=null ) {
+               $this->debug ( "Read top level metadata" );
+               if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
+                       return;
+               }
+               // TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf"
+               $this->metadata[$metafield] = $this->reader->readInnerXML();
+               $this->reader->next();
+       }
+
+       /*
+        * Filter all children, looking for animate elements
+        *
+        * @param String $name of the element that we are reading from
+        */
+       private function animateFilter( $name ) {
+               $this->debug ( "animate filter" );
+               if( $this->reader->nodeType != XmlReader::ELEMENT ) {
+                       return;
+               }
+               $exitDepth =  $this->reader->depth;
+               $keepReading = $this->reader->read();
+               while( $keepReading ) {
+                       if( $this->reader->name == $name && $this->reader->depth <= $exitDepth
+                               && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
+                               $keepReading = false;
+                               break;
+                       } elseif( $this->reader->nodeType == XmlReader::ELEMENT ){
+                               switch( $this->reader->name ) {
+                                       case 'animate':
+                                       case 'set':
+                                       case 'animateMotion':
+                                       case 'animateColor':
+                                       case 'animateTransform':
+                                               $this->debug( "HOUSTON WE HAVE ANIMATION" );
+                                               $this->metadata['animated'] = true;
+                                               break;
+                               }
                        }
-                       
-                       if( $width > 0 && $height > 0 ) {
-                               $this->width = intval( round( $width ) );
-                               $this->height = intval( round( $height ) );
+                       $keepReading = $this->reader->read();
+               }
+       }
+
+       private function throwXmlError( $err ) {
+               $this->debug( "FAILURE: $err" );
+               wfDebug( "SVGReader XML error: $err\n" );
+       }
+
+       private function debug( $data ) {
+               if( $this->mDebug ) {
+                       wfDebug( "SVGReader: $data\n" );
+               }
+       }
+
+       private function warn( $data ) {
+               wfDebug( "SVGReader: $data\n" );
+       }
+
+       private function notice( $data ) {
+               wfDebug( "SVGReader WARN: $data\n" );
+       }
+
+       /*
+        * Parse the attributes of an SVG element
+        *
+        * The parser has to be in the start element of <svg>
+        */
+       private function handleSVGAttribs( ) {
+               $defaultWidth = self::DEFAULT_WIDTH;
+               $defaultHeight = self::DEFAULT_HEIGHT;
+               $aspect = 1.0;
+               $width = null;
+               $height = null;
+
+               if( $this->reader->getAttribute('viewBox') ) {
+                       // min-x min-y width height
+                       $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute('viewBox') ) );
+                       if( count( $viewBox ) == 4 ) {
+                               $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
+                               $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
+                               if( $viewWidth > 0 && $viewHeight > 0 ) {
+                                       $aspect = $viewWidth / $viewHeight;
+                                       $defaultHeight = $defaultWidth / $aspect;
+                               }
                        }
-                       
-                       $this->first = false;
+               }
+               if( $this->reader->getAttribute('width') ) {
+                       $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth );
+               }
+               if( $this->reader->getAttribute('height') ) {
+                       $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight );
+               }
+
+               if( !isset( $width ) && !isset( $height ) ) {
+                       $width = $defaultWidth;
+                       $height = $width / $aspect;
+               } elseif( isset( $width ) && !isset( $height ) ) {
+                       $height = $width / $aspect;
+               } elseif( isset( $height ) && !isset( $width ) ) {
+                       $width = $height * $aspect;
+               }
+
+               if( $width > 0 && $height > 0 ) {
+                       $this->metadata['width'] = intval( round( $width ) );
+                       $this->metadata['height'] = intval( round( $height ) );
                }
        }
-       
+
        /**
         * Return a rounded pixel equivalent for a labeled CSS/SVG length.
         * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
@@ -78,7 +257,7 @@ class XmlSizeFilter {
         * @param $viewportSize: Float optional scale for percentage units...
         * @return float: length in pixels
         */
-       function scaleSVGUnit( $length, $viewportSize=512 ) {
+       static function scaleSVGUnit( $length, $viewportSize=512 ) {
                static $unitLength = array(
                        'px' => 1.0,
                        'pt' => 1.25,