37 private const DEFAULT_WIDTH = 512;
38 private const DEFAULT_HEIGHT = 512;
39 private const NS_SVG =
'http://www.w3.org/2000/svg';
47 private $mDebug =
false;
50 private $metadata = [];
51 private $languages = [];
52 private $languagePrefixes = [];
60 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
61 ->get( MainConfigNames::SVGMetadataCutoff );
62 $this->reader =
new XMLReader();
66 if ( $size ===
false ) {
67 throw new MWException(
"Error getting filesize of SVG." );
70 if ( $size > $svgMetadataCutoff ) {
71 $this->debug(
"SVG is $size bytes, which is bigger than {$svgMetadataCutoff}. Truncating." );
72 $contents = file_get_contents(
$source,
false,
null, 0, $svgMetadataCutoff );
73 if ( $contents ===
false ) {
76 $this->reader->XML( $contents,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
78 $this->reader->open(
$source,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
90 $oldDisable = @libxml_disable_entity_loader(
true );
91 $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES,
true );
93 $this->metadata[
'width'] = self::DEFAULT_WIDTH;
94 $this->metadata[
'height'] = self::DEFAULT_HEIGHT;
99 $this->metadata[
'originalWidth'] =
'100%';
100 $this->metadata[
'originalHeight'] =
'100%';
105 AtEase::suppressWarnings();
108 }
catch ( Exception $e ) {
113 libxml_disable_entity_loader( $oldDisable );
114 AtEase::restoreWarnings();
122 return $this->metadata;
131 $keepReading = $this->reader->read();
134 while ( $keepReading && $this->reader->nodeType != XMLReader::ELEMENT ) {
135 $keepReading = $this->reader->read();
138 if ( $this->reader->localName !=
'svg' || $this->reader->namespaceURI != self::NS_SVG ) {
139 throw new MWException(
"Expected <svg> tag, got " .
140 $this->reader->localName .
" in NS " . $this->reader->namespaceURI );
142 $this->debug(
"<svg> tag is correct." );
143 $this->handleSVGAttribs();
145 $exitDepth = $this->reader->depth;
146 $keepReading = $this->reader->read();
147 while ( $keepReading ) {
148 $tag = $this->reader->localName;
149 $type = $this->reader->nodeType;
150 $isSVG = ( $this->reader->namespaceURI == self::NS_SVG );
152 $this->debug(
"$tag" );
154 if ( $isSVG && $tag ==
'svg' &&
$type == XMLReader::END_ELEMENT
155 && $this->reader->depth <= $exitDepth
158 } elseif ( $isSVG && $tag ==
'title' ) {
159 $this->readField( $tag,
'title' );
160 } elseif ( $isSVG && $tag ==
'desc' ) {
161 $this->readField( $tag,
'description' );
162 } elseif ( $isSVG && $tag ==
'metadata' &&
$type == XMLReader::ELEMENT ) {
163 $this->readXml(
'metadata' );
164 } elseif ( $isSVG && $tag ==
'script' ) {
168 $this->metadata[
'animated'] =
true;
169 } elseif ( $tag !==
'#text' ) {
170 $this->debug(
"Unhandled top-level XML tag $tag" );
173 $this->animateFilterAndLang( $tag );
177 $keepReading = $this->reader->next();
180 $this->reader->close();
182 $this->metadata[
'translations'] = $this->languages + $this->languagePrefixes;
193 private function readField( $name, $metafield =
null ) {
194 $this->debug(
"Read field $metafield" );
195 if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
198 $keepReading = $this->reader->read();
199 while ( $keepReading ) {
200 if ( $this->reader->localName == $name
201 && $this->reader->namespaceURI == self::NS_SVG
202 && $this->reader->nodeType == XMLReader::END_ELEMENT
205 } elseif ( $this->reader->nodeType == XMLReader::TEXT ) {
206 $this->metadata[$metafield] = trim( $this->reader->value );
208 $keepReading = $this->reader->read();
218 private function readXml( $metafield =
null ) {
219 $this->debug(
"Read top level metadata" );
220 if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
224 $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
226 $this->reader->next();
235 private function animateFilterAndLang( $name ) {
236 $this->debug(
"animate filter for tag $name" );
237 if ( $this->reader->nodeType != XMLReader::ELEMENT ) {
240 if ( $this->reader->isEmptyElement ) {
243 $exitDepth = $this->reader->depth;
244 $keepReading = $this->reader->read();
245 while ( $keepReading ) {
246 if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
247 && $this->reader->nodeType == XMLReader::END_ELEMENT
250 } elseif ( $this->reader->namespaceURI == self::NS_SVG
251 && $this->reader->nodeType == XMLReader::ELEMENT
253 $sysLang = $this->reader->getAttribute(
'systemLanguage' );
254 if ( $sysLang !==
null && $sysLang !==
'' ) {
256 $langList = explode(
',', $sysLang );
257 foreach ( $langList as $langItem ) {
258 $langItem = trim( $langItem );
259 if ( LanguageCode::isWellFormedLanguageTag( $langItem ) ) {
260 $this->languages[$langItem] = self::LANG_FULL_MATCH;
268 $dash = strpos( $langItem,
'-' );
271 $itemPrefix = substr( $langItem, 0, $dash );
272 if ( LanguageCode::isWellFormedLanguageTag( $itemPrefix ) ) {
273 $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
278 switch ( $this->reader->localName ) {
286 case 'animateMotion':
288 case 'animateTransform':
289 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
290 $this->metadata[
'animated'] =
true;
294 $keepReading = $this->reader->read();
298 private function debug( $data ) {
299 if ( $this->mDebug ) {
309 private function handleSVGAttribs() {
310 $defaultWidth = self::DEFAULT_WIDTH;
311 $defaultHeight = self::DEFAULT_HEIGHT;
316 if ( $this->reader->getAttribute(
'viewBox' ) ) {
318 $viewBox = preg_split(
'/\s*[\s,]\s*/', trim( $this->reader->getAttribute(
'viewBox' ) ??
'' ) );
319 if ( count( $viewBox ) == 4 ) {
322 if ( $viewWidth > 0 && $viewHeight > 0 ) {
323 $aspect = $viewWidth / $viewHeight;
324 $defaultHeight = $defaultWidth / $aspect;
328 if ( $this->reader->getAttribute(
'width' ) ) {
329 $width = $this->
scaleSVGUnit( $this->reader->getAttribute(
'width' ) ??
'', $defaultWidth );
330 $this->metadata[
'originalWidth'] = $this->reader->getAttribute(
'width' );
332 if ( $this->reader->getAttribute(
'height' ) ) {
333 $height = $this->
scaleSVGUnit( $this->reader->getAttribute(
'height' ) ??
'', $defaultHeight );
334 $this->metadata[
'originalHeight'] = $this->reader->getAttribute(
'height' );
337 if ( !isset( $width ) && !isset( $height ) ) {
338 $width = $defaultWidth;
339 $height = $width / $aspect;
340 } elseif ( isset( $width ) && !isset( $height ) ) {
341 $height = $width / $aspect;
342 } elseif ( isset( $height ) && !isset( $width ) ) {
343 $width = $height * $aspect;
346 if ( $width > 0 && $height > 0 ) {
347 $this->metadata[
'width'] = intval( round( $width ) );
348 $this->metadata[
'height'] = intval( round( $height ) );
361 static $unitLength = [
374 '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/',
380 if ( $unit ==
'%' ) {
381 return $length * 0.01 * $viewportSize;
383 return $length * $unitLength[$unit];
387 return floatval( $length );