38 private const DEFAULT_WIDTH = 512;
39 private const DEFAULT_HEIGHT = 512;
40 private const NS_SVG =
'http://www.w3.org/2000/svg';
48 private $mDebug =
false;
51 private $metadata = [];
53 private $languages = [];
55 private $languagePrefixes = [];
63 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
64 ->get( MainConfigNames::SVGMetadataCutoff );
65 $this->reader =
new XMLReader();
69 if ( $size ===
false ) {
73 if ( $size > $svgMetadataCutoff ) {
74 $this->debug(
"SVG is $size bytes, which is bigger than {$svgMetadataCutoff}. Truncating." );
75 $contents = file_get_contents(
$source,
false,
null, 0, $svgMetadataCutoff );
76 if ( $contents ===
false ) {
79 $status = $this->reader->XML( $contents,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
81 $status = $this->reader->open(
$source,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
96 $oldDisable = @libxml_disable_entity_loader(
true );
97 $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES,
true );
99 $this->metadata[
'width'] = self::DEFAULT_WIDTH;
100 $this->metadata[
'height'] = self::DEFAULT_HEIGHT;
105 $this->metadata[
'originalWidth'] =
'100%';
106 $this->metadata[
'originalHeight'] =
'100%';
111 AtEase::suppressWarnings();
117 libxml_disable_entity_loader( $oldDisable );
118 AtEase::restoreWarnings();
126 return $this->metadata;
135 $keepReading = $this->reader->read();
138 while ( $keepReading && $this->reader->nodeType !== XMLReader::ELEMENT ) {
139 $keepReading = $this->reader->read();
142 if ( $this->reader->localName !==
'svg' || $this->reader->namespaceURI !== self::NS_SVG ) {
144 $this->reader->localName .
" in NS " . $this->reader->namespaceURI );
146 $this->debug(
'<svg> tag is correct.' );
147 $this->handleSVGAttribs();
149 $exitDepth = $this->reader->depth;
150 $keepReading = $this->reader->read();
151 while ( $keepReading ) {
152 $tag = $this->reader->localName;
153 $type = $this->reader->nodeType;
154 $isSVG = ( $this->reader->namespaceURI === self::NS_SVG );
156 $this->debug(
"$tag" );
158 if ( $isSVG && $tag ===
'svg' && $type === XMLReader::END_ELEMENT
159 && $this->reader->depth <= $exitDepth
164 if ( $isSVG && $tag ===
'title' ) {
165 $this->readField( $tag,
'title' );
166 } elseif ( $isSVG && $tag ===
'desc' ) {
167 $this->readField( $tag,
'description' );
168 } elseif ( $isSVG && $tag ===
'metadata' && $type === XMLReader::ELEMENT ) {
169 $this->readXml(
'metadata' );
170 } elseif ( $isSVG && $tag ===
'script' ) {
174 $this->metadata[
'animated'] =
true;
175 } elseif ( $tag !==
'#text' ) {
176 $this->debug(
"Unhandled top-level XML tag $tag" );
179 $this->animateFilterAndLang( $tag );
183 $keepReading = $this->reader->next();
186 $this->reader->close();
188 $this->metadata[
'translations'] = $this->languages + $this->languagePrefixes;
199 private function readField( $name, $metafield =
null ) {
200 $this->debug(
"Read field $metafield" );
201 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
204 $keepReading = $this->reader->read();
205 while ( $keepReading ) {
206 if ( $this->reader->localName === $name
207 && $this->reader->namespaceURI === self::NS_SVG
208 && $this->reader->nodeType === XMLReader::END_ELEMENT
213 if ( $this->reader->nodeType === XMLReader::TEXT ) {
214 $this->metadata[$metafield] = trim( $this->reader->value );
216 $keepReading = $this->reader->read();
225 private function readXml( $metafield =
null ) {
226 $this->debug(
"Read top level metadata" );
227 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
231 $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
233 $this->reader->next();
242 private function animateFilterAndLang( $name ) {
243 $this->debug(
"animate filter for tag $name" );
244 if ( $this->reader->nodeType !== XMLReader::ELEMENT ) {
247 if ( $this->reader->isEmptyElement ) {
250 $exitDepth = $this->reader->depth;
251 $keepReading = $this->reader->read();
252 while ( $keepReading ) {
253 if ( $this->reader->localName === $name && $this->reader->depth <= $exitDepth
254 && $this->reader->nodeType === XMLReader::END_ELEMENT
259 if ( $this->reader->namespaceURI === self::NS_SVG
260 && $this->reader->nodeType === XMLReader::ELEMENT
262 $sysLang = $this->reader->getAttribute(
'systemLanguage' );
263 if ( $sysLang !==
null && $sysLang !==
'' ) {
265 $langList = explode(
',', $sysLang );
266 foreach ( $langList as $langItem ) {
267 $langItem = trim( $langItem );
268 if ( LanguageCode::isWellFormedLanguageTag( $langItem ) ) {
269 $this->languages[$langItem] = self::LANG_FULL_MATCH;
277 $dash = strpos( $langItem,
'-' );
280 $itemPrefix = substr( $langItem, 0, $dash );
281 if ( LanguageCode::isWellFormedLanguageTag( $itemPrefix ) ) {
282 $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
287 switch ( $this->reader->localName ) {
289 $styleContents = $this->reader->readString();
291 str_contains( $styleContents,
'animated' ) ||
292 str_contains( $styleContents,
'@keyframes' )
294 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
295 $this->metadata[
'animated'] =
true;
305 case 'animateMotion':
307 case 'animateTransform':
308 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
309 $this->metadata[
'animated'] =
true;
313 $keepReading = $this->reader->read();
317 private function debug( $data ) {
318 if ( $this->mDebug ) {
328 private function handleSVGAttribs() {
329 $defaultWidth = self::DEFAULT_WIDTH;
330 $defaultHeight = self::DEFAULT_HEIGHT;
335 if ( $this->reader->getAttribute(
'viewBox' ) ) {
337 $viewBox = preg_split(
'/\s*[\s,]\s*/', trim( $this->reader->getAttribute(
'viewBox' ) ??
'' ) );
338 if ( count( $viewBox ) === 4 ) {
339 $viewWidth = self::scaleSVGUnit( $viewBox[2] );
340 $viewHeight = self::scaleSVGUnit( $viewBox[3] );
341 if ( $viewWidth > 0 && $viewHeight > 0 ) {
342 $aspect = $viewWidth / $viewHeight;
343 $defaultHeight = $defaultWidth / $aspect;
347 if ( $this->reader->getAttribute(
'width' ) ) {
348 $width = self::scaleSVGUnit( $this->reader->getAttribute(
'width' ) ??
'', $defaultWidth );
349 $this->metadata[
'originalWidth'] = $this->reader->getAttribute(
'width' );
351 if ( $this->reader->getAttribute(
'height' ) ) {
352 $height = self::scaleSVGUnit( $this->reader->getAttribute(
'height' ) ??
'', $defaultHeight );
353 $this->metadata[
'originalHeight'] = $this->reader->getAttribute(
'height' );
356 if ( !isset( $width ) && !isset( $height ) ) {
357 $width = $defaultWidth;
358 $height = $width / $aspect;
359 } elseif ( isset( $width ) && !isset( $height ) ) {
360 $height = $width / $aspect;
361 } elseif ( isset( $height ) && !isset( $width ) ) {
362 $width = $height * $aspect;
365 if ( $width > 0 && $height > 0 ) {
366 $this->metadata[
'width'] = (int)round( $width );
367 $this->metadata[
'height'] = (int)round( $height );
382 static $unitLength = [
399 '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*' .
400 '(rem|em|ex|px|pt|pc|cm|mm|in|ch|q|%)\s*$/i',
406 if ( $unit ===
'%' ) {
407 return $length * 0.01 * $viewportSize;
410 return $length * $unitLength[$unit];
414 return (
float)$length;