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 ) {
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 $status = $this->reader->XML( $contents,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
78 $status = $this->reader->open(
$source,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
93 $oldDisable = @libxml_disable_entity_loader(
true );
94 $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES,
true );
96 $this->metadata[
'width'] = self::DEFAULT_WIDTH;
97 $this->metadata[
'height'] = self::DEFAULT_HEIGHT;
102 $this->metadata[
'originalWidth'] =
'100%';
103 $this->metadata[
'originalHeight'] =
'100%';
108 AtEase::suppressWarnings();
114 libxml_disable_entity_loader( $oldDisable );
115 AtEase::restoreWarnings();
123 return $this->metadata;
132 $keepReading = $this->reader->read();
135 while ( $keepReading && $this->reader->nodeType !== XMLReader::ELEMENT ) {
136 $keepReading = $this->reader->read();
139 if ( $this->reader->localName !==
'svg' || $this->reader->namespaceURI !== self::NS_SVG ) {
141 $this->reader->localName .
" in NS " . $this->reader->namespaceURI );
143 $this->debug(
'<svg> tag is correct.' );
144 $this->handleSVGAttribs();
146 $exitDepth = $this->reader->depth;
147 $keepReading = $this->reader->read();
148 while ( $keepReading ) {
149 $tag = $this->reader->localName;
150 $type = $this->reader->nodeType;
151 $isSVG = ( $this->reader->namespaceURI === self::NS_SVG );
153 $this->debug(
"$tag" );
155 if ( $isSVG && $tag ===
'svg' && $type === XMLReader::END_ELEMENT
156 && $this->reader->depth <= $exitDepth
161 if ( $isSVG && $tag ===
'title' ) {
162 $this->readField( $tag,
'title' );
163 } elseif ( $isSVG && $tag ===
'desc' ) {
164 $this->readField( $tag,
'description' );
165 } elseif ( $isSVG && $tag ===
'metadata' && $type === XMLReader::ELEMENT ) {
166 $this->readXml(
'metadata' );
167 } elseif ( $isSVG && $tag ===
'script' ) {
171 $this->metadata[
'animated'] =
true;
172 } elseif ( $tag !==
'#text' ) {
173 $this->debug(
"Unhandled top-level XML tag $tag" );
176 $this->animateFilterAndLang( $tag );
180 $keepReading = $this->reader->next();
183 $this->reader->close();
185 $this->metadata[
'translations'] = $this->languages + $this->languagePrefixes;
196 private function readField( $name, $metafield =
null ) {
197 $this->debug(
"Read field $metafield" );
198 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
201 $keepReading = $this->reader->read();
202 while ( $keepReading ) {
203 if ( $this->reader->localName === $name
204 && $this->reader->namespaceURI === self::NS_SVG
205 && $this->reader->nodeType === XMLReader::END_ELEMENT
210 if ( $this->reader->nodeType === XMLReader::TEXT ) {
211 $this->metadata[$metafield] = trim( $this->reader->value );
213 $keepReading = $this->reader->read();
222 private function readXml( $metafield =
null ) {
223 $this->debug(
"Read top level metadata" );
224 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
228 $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
230 $this->reader->next();
239 private function animateFilterAndLang( $name ) {
240 $this->debug(
"animate filter for tag $name" );
241 if ( $this->reader->nodeType !== XMLReader::ELEMENT ) {
244 if ( $this->reader->isEmptyElement ) {
247 $exitDepth = $this->reader->depth;
248 $keepReading = $this->reader->read();
249 while ( $keepReading ) {
250 if ( $this->reader->localName === $name && $this->reader->depth <= $exitDepth
251 && $this->reader->nodeType === XMLReader::END_ELEMENT
256 if ( $this->reader->namespaceURI === self::NS_SVG
257 && $this->reader->nodeType === XMLReader::ELEMENT
259 $sysLang = $this->reader->getAttribute(
'systemLanguage' );
260 if ( $sysLang !==
null && $sysLang !==
'' ) {
262 $langList = explode(
',', $sysLang );
263 foreach ( $langList as $langItem ) {
264 $langItem = trim( $langItem );
265 if ( LanguageCode::isWellFormedLanguageTag( $langItem ) ) {
266 $this->languages[$langItem] = self::LANG_FULL_MATCH;
274 $dash = strpos( $langItem,
'-' );
277 $itemPrefix = substr( $langItem, 0, $dash );
278 if ( LanguageCode::isWellFormedLanguageTag( $itemPrefix ) ) {
279 $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
284 switch ( $this->reader->localName ) {
292 case 'animateMotion':
294 case 'animateTransform':
295 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
296 $this->metadata[
'animated'] =
true;
300 $keepReading = $this->reader->read();
304 private function debug( $data ) {
305 if ( $this->mDebug ) {
315 private function handleSVGAttribs() {
316 $defaultWidth = self::DEFAULT_WIDTH;
317 $defaultHeight = self::DEFAULT_HEIGHT;
322 if ( $this->reader->getAttribute(
'viewBox' ) ) {
324 $viewBox = preg_split(
'/\s*[\s,]\s*/', trim( $this->reader->getAttribute(
'viewBox' ) ??
'' ) );
325 if ( count( $viewBox ) === 4 ) {
326 $viewWidth = self::scaleSVGUnit( $viewBox[2] );
327 $viewHeight = self::scaleSVGUnit( $viewBox[3] );
328 if ( $viewWidth > 0 && $viewHeight > 0 ) {
329 $aspect = $viewWidth / $viewHeight;
330 $defaultHeight = $defaultWidth / $aspect;
334 if ( $this->reader->getAttribute(
'width' ) ) {
335 $width = self::scaleSVGUnit( $this->reader->getAttribute(
'width' ) ??
'', $defaultWidth );
336 $this->metadata[
'originalWidth'] = $this->reader->getAttribute(
'width' );
338 if ( $this->reader->getAttribute(
'height' ) ) {
339 $height = self::scaleSVGUnit( $this->reader->getAttribute(
'height' ) ??
'', $defaultHeight );
340 $this->metadata[
'originalHeight'] = $this->reader->getAttribute(
'height' );
343 if ( !isset( $width ) && !isset( $height ) ) {
344 $width = $defaultWidth;
345 $height = $width / $aspect;
346 } elseif ( isset( $width ) && !isset( $height ) ) {
347 $height = $width / $aspect;
348 } elseif ( isset( $height ) && !isset( $width ) ) {
349 $width = $height * $aspect;
352 if ( $width > 0 && $height > 0 ) {
353 $this->metadata[
'width'] = (int)round( $width );
354 $this->metadata[
'height'] = (int)round( $height );
367 static $unitLength = [
380 '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/',
386 if ( $unit ===
'%' ) {
387 return $length * 0.01 * $viewportSize;
390 return $length * $unitLength[$unit];
394 return (
float)$length;