Index: trunk/phase3/includes/media/SVGMetadataExtractor.php |
— | — | @@ -2,74 +2,253 @@ |
3 | 3 | /** |
4 | 4 | * SVGMetadataExtractor.php |
5 | 5 | * |
| 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 | + * |
6 | 21 | * @file |
7 | 22 | * @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 |
8 | 27 | */ |
9 | 28 | |
10 | 29 | class SVGMetadataExtractor { |
11 | 30 | 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(); |
20 | 33 | } |
21 | 34 | } |
22 | 35 | |
23 | | -class XmlSizeFilter { |
| 36 | +class SVGReader { |
24 | 37 | const DEFAULT_WIDTH = 512; |
25 | 38 | 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; |
48 | 103 | } |
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(); |
51 | 111 | } |
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; |
54 | 135 | } |
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 | + } |
63 | 183 | } |
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 | + } |
68 | 229 | } |
69 | | - |
70 | | - $this->first = false; |
71 | 230 | } |
| 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 | + } |
72 | 251 | } |
73 | | - |
| 252 | + |
74 | 253 | /** |
75 | 254 | * Return a rounded pixel equivalent for a labeled CSS/SVG length. |
76 | 255 | * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers |
— | — | @@ -78,7 +257,7 @@ |
79 | 258 | * @param $viewportSize: Float optional scale for percentage units... |
80 | 259 | * @return float: length in pixels |
81 | 260 | */ |
82 | | - function scaleSVGUnit( $length, $viewportSize=512 ) { |
| 261 | + static function scaleSVGUnit( $length, $viewportSize=512 ) { |
83 | 262 | static $unitLength = array( |
84 | 263 | 'px' => 1.0, |
85 | 264 | 'pt' => 1.25, |
Index: trunk/phase3/includes/media/SVG.php |
— | — | @@ -12,7 +12,7 @@ |
13 | 13 | * @ingroup Media |
14 | 14 | */ |
15 | 15 | class SvgHandler extends ImageHandler { |
16 | | - const SVG_METADATA_VERSION = 1; |
| 16 | + const SVG_METADATA_VERSION = 2; |
17 | 17 | |
18 | 18 | function isEnabled() { |
19 | 19 | global $wgSVGConverters, $wgSVGConverter; |
— | — | @@ -32,8 +32,15 @@ |
33 | 33 | return true; |
34 | 34 | } |
35 | 35 | |
36 | | - function isAnimatedImage( $image ) { |
| 36 | + function isAnimatedImage( $file ) { |
37 | 37 | # 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 | + } |
38 | 45 | return false; |
39 | 46 | } |
40 | 47 | |
— | — | @@ -72,7 +79,7 @@ |
73 | 80 | return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, |
74 | 81 | wfMsg( 'thumbnail_dest_directory' ) ); |
75 | 82 | } |
76 | | - |
| 83 | + |
77 | 84 | $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight ); |
78 | 85 | if( $status === true ) { |
79 | 86 | return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); |
— | — | @@ -80,7 +87,7 @@ |
81 | 88 | return $status; // MediaTransformError |
82 | 89 | } |
83 | 90 | } |
84 | | - |
| 91 | + |
85 | 92 | /* |
86 | 93 | * Transform an SVG file to PNG |
87 | 94 | * This function can be called outside of thumbnail contexts |
— | — | @@ -142,10 +149,6 @@ |
143 | 150 | $wgLang->formatSize( $file->getSize() ) ); |
144 | 151 | } |
145 | 152 | |
146 | | - function formatMetadata( $file ) { |
147 | | - return false; |
148 | | - } |
149 | | - |
150 | 153 | function getMetadata( $file, $filename ) { |
151 | 154 | $metadata = array(); |
152 | 155 | try { |
— | — | @@ -158,7 +161,7 @@ |
159 | 162 | $metadata['version'] = self::SVG_METADATA_VERSION; |
160 | 163 | return serialize( $metadata ); |
161 | 164 | } |
162 | | - |
| 165 | + |
163 | 166 | function unpackMetadata( $metadata ) { |
164 | 167 | $unser = @unserialize( $metadata ); |
165 | 168 | if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) { |
— | — | @@ -175,4 +178,44 @@ |
176 | 179 | function isMetadataValid( $image, $metadata ) { |
177 | 180 | return $this->unpackMetadata( $metadata ) !== false; |
178 | 181 | } |
| 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 | + } |
179 | 222 | } |
Index: trunk/phase3/RELEASE-NOTES |
— | — | @@ -197,6 +197,7 @@ |
198 | 198 | * (bug 10596) Allow installer to enable extensions already in extensions folder |
199 | 199 | * (bug 17394) Make installer check for latest version against MediaWiki.org |
200 | 200 | * (bug 20627) Installer should be in languages other than English |
| 201 | +* Support for metadata in SVG files (title, description). |
201 | 202 | |
202 | 203 | === Bug fixes in 1.17 === |
203 | 204 | * (bug 17560) Half-broken deletion moved image files to deletion archive |