* @ingroup Media
*/
class SvgHandler extends ImageHandler {
- const SVG_METADATA_VERSION = 1;
+ const SVG_METADATA_VERSION = 2;
function isEnabled() {
global $wgSVGConverters, $wgSVGConverter;
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;
}
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 );
return $status; // MediaTransformError
}
}
-
+
/*
* Transform an SVG file to PNG
* This function can be called outside of thumbnail contexts
$wgLang->formatSize( $file->getSize() ) );
}
- function formatMetadata( $file ) {
- return false;
- }
-
function getMetadata( $file, $filename ) {
$metadata = array();
try {
$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 ) {
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;
+ }
}
/**
* 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
* @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,