r75968 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r75967‎ | r75968 | r75969 >
Date:00:35, 4 November 2010
Author:hartman
Status:ok
Tags:
Comment:
* 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)
Modified paths:
  • /trunk/phase3/RELEASE-NOTES (modified) (history)
  • /trunk/phase3/includes/media/SVG.php (modified) (history)
  • /trunk/phase3/includes/media/SVGMetadataExtractor.php (modified) (history)

Diff [purge]

Index: trunk/phase3/includes/media/SVGMetadataExtractor.php
@@ -2,74 +2,253 @@
33 /**
44 * SVGMetadataExtractor.php
55 *
 6+ * This program is free software; you can redistribute it and/or modify
 7+ * it under the terms of the GNU General Public License as published by
 8+ * the Free Software Foundation; either version 2 of the License, or
 9+ * (at your option) any later version.
 10+ *
 11+ * This program is distributed in the hope that it will be useful,
 12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14+ * GNU General Public License for more details.
 15+ *
 16+ * You should have received a copy of the GNU General Public License along
 17+ * with this program; if not, write to the Free Software Foundation, Inc.,
 18+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 19+ * http://www.gnu.org/copyleft/gpl.html
 20+ *
621 * @file
722 * @ingroup Media
 23+ * @author Derk-Jan Hartman <hartman _at_ videolan d0t org>
 24+ * @author Brion Vibber
 25+ * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman
 26+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
827 */
928
1029 class SVGMetadataExtractor {
1130 static function getMetadata( $filename ) {
12 - $filter = new XmlSizeFilter();
13 - $xml = new XmlTypeCheck( $filename, array( $filter, 'filter' ) );
14 - if( $xml->wellFormed ) {
15 - return array(
16 - 'width' => $filter->width,
17 - 'height' => $filter->height
18 - );
19 - }
 31+ $svg = new SVGReader( $filename );
 32+ return $svg->getMetadata();
2033 }
2134 }
2235
23 -class XmlSizeFilter {
 36+class SVGReader {
2437 const DEFAULT_WIDTH = 512;
2538 const DEFAULT_HEIGHT = 512;
26 - var $first = true;
27 - var $width = self::DEFAULT_WIDTH;
28 - var $height = self::DEFAULT_HEIGHT;
29 - function filter( $name, $attribs ) {
30 - if( $this->first ) {
31 - $defaultWidth = self::DEFAULT_WIDTH;
32 - $defaultHeight = self::DEFAULT_HEIGHT;
33 - $aspect = 1.0;
34 - $width = null;
35 - $height = null;
36 -
37 - if( isset( $attribs['viewBox'] ) ) {
38 - // min-x min-y width height
39 - $viewBox = preg_split( '/\s+/', trim( $attribs['viewBox'] ) );
40 - if( count( $viewBox ) == 4 ) {
41 - $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
42 - $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
43 - if( $viewWidth > 0 && $viewHeight > 0 ) {
44 - $aspect = $viewWidth / $viewHeight;
45 - $defaultHeight = $defaultWidth / $aspect;
46 - }
47 - }
 39+
 40+ private $reader = null;
 41+ private $mDebug = false;
 42+ private $metadata = Array();
 43+
 44+ /**
 45+ * Constructor
 46+ *
 47+ * Creates an SVGReader drawing from the source provided
 48+ * @param $source String: URI from which to read
 49+ */
 50+ function __construct( $source ) {
 51+ $this->reader = new XMLReader();
 52+ $this->reader->open( $source );
 53+
 54+ $this->metadata['width'] = self::DEFAULT_WIDTH;
 55+ $this->metadata['height'] = self::DEFAULT_HEIGHT;
 56+
 57+ $this->read();
 58+ }
 59+
 60+ /*
 61+ * @return Array with the known metadata
 62+ */
 63+ public function getMetadata() {
 64+ return $this->metadata;
 65+ }
 66+
 67+ /*
 68+ * Read the SVG
 69+ */
 70+ public function read() {
 71+ $this->reader->read();
 72+
 73+ if ( $this->reader->name != 'svg' ) {
 74+ throw new MWException( "Expected <svg> tag, got ".
 75+ $this->reader->name );
 76+ }
 77+ $this->debug( "<svg> tag is correct." );
 78+
 79+ $this->debug( "Starting primary dump processing loop." );
 80+ $this->handleSVGAttribs();
 81+ $exitDepth = $this->reader->depth;
 82+
 83+ $keepReading = $this->reader->read();
 84+ $skip = false;
 85+ while ( $keepReading ) {
 86+ $tag = $this->reader->name;
 87+ $type = $this->reader->nodeType;
 88+
 89+ $this->debug( "$tag" );
 90+
 91+ if ( $tag == 'svg' && $type == XmlReader::END_ELEMENT && $this->reader->depth <= $exitDepth ) {
 92+ break;
 93+ } elseif ( $tag == 'title' ) {
 94+ $this->readField( $tag, 'title' );
 95+ } elseif ( $tag == 'desc' ) {
 96+ $this->readField( $tag, 'description' );
 97+ } elseif ( $tag == 'metadata' && $type == XmlReader::ELEMENT ) {
 98+ $this->readXml( $tag, 'metadata' );
 99+ } elseif ( $tag !== '#text' ) {
 100+ $this->debug( "Unhandled top-level XML tag $tag" );
 101+ $this->animateFilter( $tag );
 102+ //$skip = true;
48103 }
49 - if( isset( $attribs['width'] ) ) {
50 - $width = $this->scaleSVGUnit( $attribs['width'], $defaultWidth );
 104+
 105+ if ($skip) {
 106+ $keepReading = $this->reader->next();
 107+ $skip = false;
 108+ $this->debug( "Skip" );
 109+ } else {
 110+ $keepReading = $this->reader->read();
51111 }
52 - if( isset( $attribs['height'] ) ) {
53 - $height = $this->scaleSVGUnit( $attribs['height'], $defaultHeight );
 112+ }
 113+
 114+ return true;
 115+ }
 116+
 117+ /*
 118+ * Read a textelement from an element
 119+ *
 120+ * @param String $name of the element that we are reading from
 121+ * @param String $metafield that we will fill with the result
 122+ */
 123+ private function readField( $name, $metafield=null ) {
 124+ $this->debug ( "Read field $metafield" );
 125+ if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
 126+ return;
 127+ }
 128+ $keepReading = $this->reader->read();
 129+ while( $keepReading ) {
 130+ if( $this->reader->name == $name && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
 131+ $keepReading = false;
 132+ break;
 133+ } elseif( $this->reader->nodeType == XmlReader::TEXT ){
 134+ $this->metadata[$metafield] = $this->reader->value;
54135 }
55 -
56 - if( !isset( $width ) && !isset( $height ) ) {
57 - $width = $defaultWidth;
58 - $height = $width / $aspect;
59 - } elseif( isset( $width ) && !isset( $height ) ) {
60 - $height = $width / $aspect;
61 - } elseif( isset( $height ) && !isset( $width ) ) {
62 - $width = $height * $aspect;
 136+ $keepReading = $this->reader->read();
 137+ }
 138+ }
 139+
 140+ /*
 141+ * Read an XML snippet from an element
 142+ *
 143+ * @param String $metafield that we will fill with the result
 144+ */
 145+ private function readXml( $metafield=null ) {
 146+ $this->debug ( "Read top level metadata" );
 147+ if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
 148+ return;
 149+ }
 150+ // TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf"
 151+ $this->metadata[$metafield] = $this->reader->readInnerXML();
 152+ $this->reader->next();
 153+ }
 154+
 155+ /*
 156+ * Filter all children, looking for animate elements
 157+ *
 158+ * @param String $name of the element that we are reading from
 159+ */
 160+ private function animateFilter( $name ) {
 161+ $this->debug ( "animate filter" );
 162+ if( $this->reader->nodeType != XmlReader::ELEMENT ) {
 163+ return;
 164+ }
 165+ $exitDepth = $this->reader->depth;
 166+ $keepReading = $this->reader->read();
 167+ while( $keepReading ) {
 168+ if( $this->reader->name == $name && $this->reader->depth <= $exitDepth
 169+ && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
 170+ $keepReading = false;
 171+ break;
 172+ } elseif( $this->reader->nodeType == XmlReader::ELEMENT ){
 173+ switch( $this->reader->name ) {
 174+ case 'animate':
 175+ case 'set':
 176+ case 'animateMotion':
 177+ case 'animateColor':
 178+ case 'animateTransform':
 179+ $this->debug( "HOUSTON WE HAVE ANIMATION" );
 180+ $this->metadata['animated'] = true;
 181+ break;
 182+ }
63183 }
64 -
65 - if( $width > 0 && $height > 0 ) {
66 - $this->width = intval( round( $width ) );
67 - $this->height = intval( round( $height ) );
 184+ $keepReading = $this->reader->read();
 185+ }
 186+ }
 187+
 188+ private function throwXmlError( $err ) {
 189+ $this->debug( "FAILURE: $err" );
 190+ wfDebug( "SVGReader XML error: $err\n" );
 191+ }
 192+
 193+ private function debug( $data ) {
 194+ if( $this->mDebug ) {
 195+ wfDebug( "SVGReader: $data\n" );
 196+ }
 197+ }
 198+
 199+ private function warn( $data ) {
 200+ wfDebug( "SVGReader: $data\n" );
 201+ }
 202+
 203+ private function notice( $data ) {
 204+ wfDebug( "SVGReader WARN: $data\n" );
 205+ }
 206+
 207+ /*
 208+ * Parse the attributes of an SVG element
 209+ *
 210+ * The parser has to be in the start element of <svg>
 211+ */
 212+ private function handleSVGAttribs( ) {
 213+ $defaultWidth = self::DEFAULT_WIDTH;
 214+ $defaultHeight = self::DEFAULT_HEIGHT;
 215+ $aspect = 1.0;
 216+ $width = null;
 217+ $height = null;
 218+
 219+ if( $this->reader->getAttribute('viewBox') ) {
 220+ // min-x min-y width height
 221+ $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute('viewBox') ) );
 222+ if( count( $viewBox ) == 4 ) {
 223+ $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
 224+ $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
 225+ if( $viewWidth > 0 && $viewHeight > 0 ) {
 226+ $aspect = $viewWidth / $viewHeight;
 227+ $defaultHeight = $defaultWidth / $aspect;
 228+ }
68229 }
69 -
70 - $this->first = false;
71230 }
 231+ if( $this->reader->getAttribute('width') ) {
 232+ $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth );
 233+ }
 234+ if( $this->reader->getAttribute('height') ) {
 235+ $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight );
 236+ }
 237+
 238+ if( !isset( $width ) && !isset( $height ) ) {
 239+ $width = $defaultWidth;
 240+ $height = $width / $aspect;
 241+ } elseif( isset( $width ) && !isset( $height ) ) {
 242+ $height = $width / $aspect;
 243+ } elseif( isset( $height ) && !isset( $width ) ) {
 244+ $width = $height * $aspect;
 245+ }
 246+
 247+ if( $width > 0 && $height > 0 ) {
 248+ $this->metadata['width'] = intval( round( $width ) );
 249+ $this->metadata['height'] = intval( round( $height ) );
 250+ }
72251 }
73 -
 252+
74253 /**
75254 * Return a rounded pixel equivalent for a labeled CSS/SVG length.
76255 * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
@@ -78,7 +257,7 @@
79258 * @param $viewportSize: Float optional scale for percentage units...
80259 * @return float: length in pixels
81260 */
82 - function scaleSVGUnit( $length, $viewportSize=512 ) {
 261+ static function scaleSVGUnit( $length, $viewportSize=512 ) {
83262 static $unitLength = array(
84263 'px' => 1.0,
85264 'pt' => 1.25,
Index: trunk/phase3/includes/media/SVG.php
@@ -12,7 +12,7 @@
1313 * @ingroup Media
1414 */
1515 class SvgHandler extends ImageHandler {
16 - const SVG_METADATA_VERSION = 1;
 16+ const SVG_METADATA_VERSION = 2;
1717
1818 function isEnabled() {
1919 global $wgSVGConverters, $wgSVGConverter;
@@ -32,8 +32,15 @@
3333 return true;
3434 }
3535
36 - function isAnimatedImage( $image ) {
 36+ function isAnimatedImage( $file ) {
3737 # TODO: detect animated SVGs
 38+ $metadata = $file->getMetadata();
 39+ if ( $metadata ) {
 40+ $metadata = $this->unpackMetadata( $metadata );
 41+ if( isset( $metadata['animated'] ) ) {
 42+ return $metadata['animated'];
 43+ }
 44+ }
3845 return false;
3946 }
4047
@@ -72,7 +79,7 @@
7380 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
7481 wfMsg( 'thumbnail_dest_directory' ) );
7582 }
76 -
 83+
7784 $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight );
7885 if( $status === true ) {
7986 return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath );
@@ -80,7 +87,7 @@
8188 return $status; // MediaTransformError
8289 }
8390 }
84 -
 91+
8592 /*
8693 * Transform an SVG file to PNG
8794 * This function can be called outside of thumbnail contexts
@@ -142,10 +149,6 @@
143150 $wgLang->formatSize( $file->getSize() ) );
144151 }
145152
146 - function formatMetadata( $file ) {
147 - return false;
148 - }
149 -
150153 function getMetadata( $file, $filename ) {
151154 $metadata = array();
152155 try {
@@ -158,7 +161,7 @@
159162 $metadata['version'] = self::SVG_METADATA_VERSION;
160163 return serialize( $metadata );
161164 }
162 -
 165+
163166 function unpackMetadata( $metadata ) {
164167 $unser = @unserialize( $metadata );
165168 if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
@@ -175,4 +178,44 @@
176179 function isMetadataValid( $image, $metadata ) {
177180 return $this->unpackMetadata( $metadata ) !== false;
178181 }
 182+
 183+ function visibleMetadataFields() {
 184+ $fields = array( 'title', 'description', 'animated' );
 185+ return $fields;
 186+ }
 187+
 188+ function formatMetadata( $file ) {
 189+ $result = array(
 190+ 'visible' => array(),
 191+ 'collapsed' => array()
 192+ );
 193+ $metadata = $file->getMetadata();
 194+ if ( !$metadata ) {
 195+ return false;
 196+ }
 197+ $metadata = $this->unpackMetadata( $metadata );
 198+ if ( !$metadata ) {
 199+ return false;
 200+ }
 201+ unset( $metadata['version'] );
 202+ unset( $metadata['metadata'] ); /* non-formatted XML */
 203+
 204+ /* TODO: add a formatter
 205+ $format = new FormatSVG( $metadata );
 206+ $formatted = $format->getFormattedData();
 207+ */
 208+
 209+ // Sort fields into visible and collapsed
 210+ $visibleFields = $this->visibleMetadataFields();
 211+ foreach ( $metadata as $name => $value ) {
 212+ $tag = strtolower( $name );
 213+ self::addMeta( $result,
 214+ in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
 215+ 'svg',
 216+ $tag,
 217+ $value
 218+ );
 219+ }
 220+ return $result;
 221+ }
179222 }
Index: trunk/phase3/RELEASE-NOTES
@@ -197,6 +197,7 @@
198198 * (bug 10596) Allow installer to enable extensions already in extensions folder
199199 * (bug 17394) Make installer check for latest version against MediaWiki.org
200200 * (bug 20627) Installer should be in languages other than English
 201+* Support for metadata in SVG files (title, description).
201202
202203 === Bug fixes in 1.17 ===
203204 * (bug 17560) Half-broken deletion moved image files to deletion archive

Follow-up revisions

RevisionCommit summaryAuthorDate
r82048(Follow-up r75968) r75968 started extracting and displaying some svg metadata...bawolff04:55, 13 February 2011

Status & tagging log