From 37013dc9207a585a82e066ddddc406a471378a33 Mon Sep 17 00:00:00 2001 From: Derk-Jan Hartman Date: Thu, 4 Nov 2010 00:35:29 +0000 Subject: [PATCH] * The SVGMetadataExtractor now based on XmlReader * 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 | 1 + includes/media/SVG.php | 61 ++++- includes/media/SVGMetadataExtractor.php | 281 +++++++++++++++++++----- 3 files changed, 283 insertions(+), 60 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 734683d436..6a412f96bf 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -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 diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 38588209b4..6c2d98059a 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -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; + } } diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index 7862225f52..66dc1259dc 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -2,74 +2,253 @@ /** * 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 + * @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 tag, got ". + $this->reader->name ); + } + $this->debug( " 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 + */ + 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, -- 2.20.1