]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/media/SVGMetadataExtractor.php
MediaWiki 1.17.0
[autoinstallsdev/mediawiki.git] / includes / media / SVGMetadataExtractor.php
1 <?php
2 /**
3  * SVGMetadataExtractor.php
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Media
22  * @author Derk-Jan Hartman <hartman _at_ videolan d0t org>
23  * @author Brion Vibber
24  * @copyright Copyright © 2010-2010 Brion Vibber, Derk-Jan Hartman
25  * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
26  */
27
28 class SVGMetadataExtractor {
29         static function getMetadata( $filename ) {
30                 $svg = new SVGReader( $filename );
31                 return $svg->getMetadata();
32         }
33 }
34
35 class SVGReader {
36         const DEFAULT_WIDTH = 512;
37         const DEFAULT_HEIGHT = 512;
38         const NS_SVG = 'http://www.w3.org/2000/svg';
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                 global $wgSVGMetadataCutoff;
52                 $this->reader = new XMLReader();
53
54                 // Don't use $file->getSize() since file object passed to SVGHandler::getMetadata is bogus.
55                 $size = filesize( $source );
56                 if ( $size === false ) {
57                         throw new MWException( "Error getting filesize of SVG." );
58                 } 
59
60                 if ( $size > $wgSVGMetadataCutoff ) {
61                         $this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." );
62                         $contents = file_get_contents( $source, false, null, -1, $wgSVGMetadataCutoff );
63                         if ($contents === false) {
64                                 throw new MWException( 'Error reading SVG file.' );
65                         }
66                         $this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING );
67                 } else {
68                         $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING );
69                 }
70
71                 $this->metadata['width'] = self::DEFAULT_WIDTH;
72                 $this->metadata['height'] = self::DEFAULT_HEIGHT;
73
74                 // Because we cut off the end of the svg making an invalid one. Complicated
75                 // try catch thing to make sure warnings get restored. Seems like there should
76                 // be a better way.
77                 wfSuppressWarnings();
78                 try {
79                         $this->read();
80                 } catch( Exception $e ) {
81                         wfRestoreWarnings();
82                         throw $e;
83                 }
84                 wfRestoreWarnings();
85         }
86
87         /*
88          * @return Array with the known metadata
89          */
90         public function getMetadata() {
91                 return $this->metadata;
92         }
93
94         /*
95          * Read the SVG
96          */
97         public function read() {
98                 $keepReading = $this->reader->read();
99
100                 /* Skip until first element */
101                 while( $keepReading && $this->reader->nodeType != XmlReader::ELEMENT ) {
102                         $keepReading = $this->reader->read();
103                 }
104
105                 if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) {
106                         throw new MWException( "Expected <svg> tag, got ".
107                                 $this->reader->localName . " in NS " . $this->reader->namespaceURI );
108                 }
109                 $this->debug( "<svg> tag is correct." );
110                 $this->handleSVGAttribs();
111
112                 $exitDepth =  $this->reader->depth;
113                 $keepReading = $this->reader->read();
114                 while ( $keepReading ) {
115                         $tag = $this->reader->localName;
116                         $type = $this->reader->nodeType;
117                         $isSVG = ($this->reader->namespaceURI == self::NS_SVG);
118
119                         $this->debug( "$tag" );
120
121                         if ( $isSVG && $tag == 'svg' && $type == XmlReader::END_ELEMENT && $this->reader->depth <= $exitDepth ) {
122                                 break;
123                         } elseif ( $isSVG && $tag == 'title' ) {
124                                 $this->readField( $tag, 'title' );
125                         } elseif ( $isSVG && $tag == 'desc' ) {
126                                 $this->readField( $tag, 'description' );
127                         } elseif ( $isSVG && $tag == 'metadata' && $type == XmlReader::ELEMENT ) {
128                                 $this->readXml( $tag, 'metadata' );
129                         } elseif ( $tag !== '#text' ) {
130                                 $this->debug( "Unhandled top-level XML tag $tag" );
131
132                                 if ( !isset( $this->metadata['animated'] ) ) {
133                                         // Recurse into children of current tag, looking for animation.
134                                         $this->animateFilter( $tag );
135                                 }
136                         }
137
138                         // Goto next element, which is sibling of current (Skip children).
139                         $keepReading = $this->reader->next();
140                 }
141
142                 return true;
143         }
144
145         /*
146          * Read a textelement from an element
147          *
148          * @param String $name of the element that we are reading from
149          * @param String $metafield that we will fill with the result
150          */
151         private function readField( $name, $metafield=null ) {
152                 $this->debug ( "Read field $metafield" );
153                 if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
154                         return;
155                 }
156                 $keepReading = $this->reader->read();
157                 while( $keepReading ) {
158                         if( $this->reader->localName == $name && $this->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
159                                 break;
160                         } elseif( $this->reader->nodeType == XmlReader::TEXT ){
161                                 $this->metadata[$metafield] = trim( $this->reader->value );
162                         }
163                         $keepReading = $this->reader->read();
164                 }
165         }
166
167         /*
168          * Read an XML snippet from an element
169          *
170          * @param String $metafield that we will fill with the result
171          */
172         private function readXml( $metafield=null ) {
173                 $this->debug ( "Read top level metadata" );
174                 if( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) {
175                         return;
176                 }
177                 // TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf"
178                 $this->metadata[$metafield] = trim( $this->reader->readInnerXML() );
179                 $this->reader->next();
180         }
181
182         /*
183          * Filter all children, looking for animate elements
184          *
185          * @param String $name of the element that we are reading from
186          */
187         private function animateFilter( $name ) {
188                 $this->debug ( "animate filter" );
189                 if( $this->reader->nodeType != XmlReader::ELEMENT ) {
190                         return;
191                 }
192                 $exitDepth =  $this->reader->depth;
193                 $keepReading = $this->reader->read();
194                 while( $keepReading ) {
195                         if( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
196                                 && $this->reader->nodeType == XmlReader::END_ELEMENT ) {
197                                 break;
198                         } elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) {
199                                 switch( $this->reader->localName ) {
200                                         case 'animate':
201                                         case 'set':
202                                         case 'animateMotion':
203                                         case 'animateColor':
204                                         case 'animateTransform':
205                                                 $this->debug( "HOUSTON WE HAVE ANIMATION" );
206                                                 $this->metadata['animated'] = true;
207                                                 break;
208                                 }
209                         }
210                         $keepReading = $this->reader->read();
211                 }
212         }
213
214         private function throwXmlError( $err ) {
215                 $this->debug( "FAILURE: $err" );
216                 wfDebug( "SVGReader XML error: $err\n" );
217         }
218
219         private function debug( $data ) {
220                 if( $this->mDebug ) {
221                         wfDebug( "SVGReader: $data\n" );
222                 }
223         }
224
225         private function warn( $data ) {
226                 wfDebug( "SVGReader: $data\n" );
227         }
228
229         private function notice( $data ) {
230                 wfDebug( "SVGReader WARN: $data\n" );
231         }
232
233         /*
234          * Parse the attributes of an SVG element
235          *
236          * The parser has to be in the start element of <svg>
237          */
238         private function handleSVGAttribs( ) {
239                 $defaultWidth = self::DEFAULT_WIDTH;
240                 $defaultHeight = self::DEFAULT_HEIGHT;
241                 $aspect = 1.0;
242                 $width = null;
243                 $height = null;
244
245                 if( $this->reader->getAttribute('viewBox') ) {
246                         // min-x min-y width height
247                         $viewBox = preg_split( '/\s+/', trim( $this->reader->getAttribute('viewBox') ) );
248                         if( count( $viewBox ) == 4 ) {
249                                 $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
250                                 $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
251                                 if( $viewWidth > 0 && $viewHeight > 0 ) {
252                                         $aspect = $viewWidth / $viewHeight;
253                                         $defaultHeight = $defaultWidth / $aspect;
254                                 }
255                         }
256                 }
257                 if( $this->reader->getAttribute('width') ) {
258                         $width = $this->scaleSVGUnit( $this->reader->getAttribute('width'), $defaultWidth );
259                 }
260                 if( $this->reader->getAttribute('height') ) {
261                         $height = $this->scaleSVGUnit( $this->reader->getAttribute('height'), $defaultHeight );
262                 }
263
264                 if( !isset( $width ) && !isset( $height ) ) {
265                         $width = $defaultWidth;
266                         $height = $width / $aspect;
267                 } elseif( isset( $width ) && !isset( $height ) ) {
268                         $height = $width / $aspect;
269                 } elseif( isset( $height ) && !isset( $width ) ) {
270                         $width = $height * $aspect;
271                 }
272
273                 if( $width > 0 && $height > 0 ) {
274                         $this->metadata['width'] = intval( round( $width ) );
275                         $this->metadata['height'] = intval( round( $height ) );
276                 }
277         }
278
279         /**
280          * Return a rounded pixel equivalent for a labeled CSS/SVG length.
281          * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers
282          *
283          * @param $length String: CSS/SVG length.
284          * @param $viewportSize: Float optional scale for percentage units...
285          * @return float: length in pixels
286          */
287         static function scaleSVGUnit( $length, $viewportSize=512 ) {
288                 static $unitLength = array(
289                         'px' => 1.0,
290                         'pt' => 1.25,
291                         'pc' => 15.0,
292                         'mm' => 3.543307,
293                         'cm' => 35.43307,
294                         'in' => 90.0,
295                         'em' => 16.0, // fake it?
296                         'ex' => 12.0, // fake it?
297                         ''   => 1.0, // "User units" pixels by default
298                         );
299                 $matches = array();
300                 if( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) {
301                         $length = floatval( $matches[1] );
302                         $unit = $matches[2];
303                         if( $unit == '%' ) {
304                                 return $length * 0.01 * $viewportSize;
305                         } else {
306                                 return $length * $unitLength[$unit];
307                         }
308                 } else {
309                         // Assume pixels
310                         return floatval( $length );
311                 }
312         }
313 }