24 private const DEFAULT_WIDTH = 512;
25 private const DEFAULT_HEIGHT = 512;
26 private const NS_SVG =
'http://www.w3.org/2000/svg';
34 private $mDebug =
false;
37 private $metadata = [];
39 private $languages = [];
41 private $languagePrefixes = [];
49 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
50 ->get( MainConfigNames::SVGMetadataCutoff );
51 $this->reader =
new XMLReader();
55 if ( $size ===
false ) {
59 if ( $size > $svgMetadataCutoff ) {
60 $this->debug(
"SVG is $size bytes, which is bigger than {$svgMetadataCutoff}. Truncating." );
61 $contents = file_get_contents(
$source,
false,
null, 0, $svgMetadataCutoff );
62 if ( $contents ===
false ) {
65 $status = $this->reader->XML( $contents,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
67 $status = $this->reader->open(
$source,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
82 $oldDisable = @libxml_disable_entity_loader(
true );
83 $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES,
true );
85 $this->metadata[
'width'] = self::DEFAULT_WIDTH;
86 $this->metadata[
'height'] = self::DEFAULT_HEIGHT;
91 $this->metadata[
'originalWidth'] =
'100%';
92 $this->metadata[
'originalHeight'] =
'100%';
97 AtEase::suppressWarnings();
103 libxml_disable_entity_loader( $oldDisable );
104 AtEase::restoreWarnings();
112 return $this->metadata;
121 $keepReading = $this->reader->read();
124 while ( $keepReading && $this->reader->nodeType !== XMLReader::ELEMENT ) {
125 $keepReading = $this->reader->read();
128 if ( $this->reader->localName !==
'svg' || $this->reader->namespaceURI !== self::NS_SVG ) {
130 $this->reader->localName .
" in NS " . $this->reader->namespaceURI );
132 $this->debug(
'<svg> tag is correct.' );
133 $this->handleSVGAttribs();
135 $exitDepth = $this->reader->depth;
136 $keepReading = $this->reader->read();
137 while ( $keepReading ) {
138 $tag = $this->reader->localName;
139 $type = $this->reader->nodeType;
140 $isSVG = ( $this->reader->namespaceURI === self::NS_SVG );
142 $this->debug(
"$tag" );
144 if ( $isSVG && $tag ===
'svg' && $type === XMLReader::END_ELEMENT
145 && $this->reader->depth <= $exitDepth
150 if ( $isSVG && $tag ===
'title' ) {
151 $this->readField( $tag,
'title' );
152 } elseif ( $isSVG && $tag ===
'desc' ) {
153 $this->readField( $tag,
'description' );
154 } elseif ( $isSVG && $tag ===
'metadata' && $type === XMLReader::ELEMENT ) {
155 $this->readXml(
'metadata' );
156 } elseif ( $isSVG && $tag ===
'script' ) {
160 $this->metadata[
'animated'] =
true;
161 } elseif ( $tag !==
'#text' ) {
162 $this->debug(
"Unhandled top-level XML tag $tag" );
165 $this->animateFilterAndLang( $tag );
169 $keepReading = $this->reader->next();
172 $this->reader->close();
174 $this->metadata[
'translations'] = $this->languages + $this->languagePrefixes;
185 private function readField( $name, $metafield =
null ) {
186 $this->debug(
"Read field $metafield" );
187 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
190 $keepReading = $this->reader->read();
191 while ( $keepReading ) {
192 if ( $this->reader->localName === $name
193 && $this->reader->namespaceURI === self::NS_SVG
194 && $this->reader->nodeType === XMLReader::END_ELEMENT
199 if ( $this->reader->nodeType === XMLReader::TEXT ) {
200 $this->metadata[$metafield] = trim( $this->reader->value );
202 $keepReading = $this->reader->read();
211 private function readXml( $metafield =
null ) {
212 $this->debug(
"Read top level metadata" );
213 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
217 $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
219 $this->reader->next();
228 private function animateFilterAndLang( $name ) {
229 $this->debug(
"animate filter for tag $name" );
230 if ( $this->reader->nodeType !== XMLReader::ELEMENT ) {
233 if ( $this->reader->isEmptyElement ) {
236 $exitDepth = $this->reader->depth;
237 $keepReading = $this->reader->read();
238 while ( $keepReading ) {
239 if ( $this->reader->localName === $name && $this->reader->depth <= $exitDepth
240 && $this->reader->nodeType === XMLReader::END_ELEMENT
245 if ( $this->reader->namespaceURI === self::NS_SVG
246 && $this->reader->nodeType === XMLReader::ELEMENT
248 $sysLang = $this->reader->getAttribute(
'systemLanguage' );
249 if ( $sysLang !==
null && $sysLang !==
'' ) {
251 $langList = explode(
',', $sysLang );
252 foreach ( $langList as $langItem ) {
253 $langItem = trim( $langItem );
254 if ( LanguageCode::isWellFormedLanguageTag( $langItem ) ) {
255 $this->languages[$langItem] = self::LANG_FULL_MATCH;
263 $dash = strpos( $langItem,
'-' );
266 $itemPrefix = substr( $langItem, 0, $dash );
267 if ( LanguageCode::isWellFormedLanguageTag( $itemPrefix ) ) {
268 $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
273 switch ( $this->reader->localName ) {
275 $styleContents = $this->reader->readString();
277 str_contains( $styleContents,
'animated' ) ||
278 str_contains( $styleContents,
'@keyframes' )
280 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
281 $this->metadata[
'animated'] =
true;
291 case 'animateMotion':
293 case 'animateTransform':
294 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
295 $this->metadata[
'animated'] =
true;
299 $keepReading = $this->reader->read();
303 private function debug(
string $data ) {
304 if ( $this->mDebug ) {
314 private function handleSVGAttribs() {
315 $defaultWidth = self::DEFAULT_WIDTH;
316 $defaultHeight = self::DEFAULT_HEIGHT;
321 if ( $this->reader->getAttribute(
'viewBox' ) ) {
323 $viewBox = preg_split(
'/\s*[\s,]\s*/', trim( $this->reader->getAttribute(
'viewBox' ) ??
'' ) );
324 if ( count( $viewBox ) === 4 ) {
325 $viewWidth = self::scaleSVGUnit( $viewBox[2] );
326 $viewHeight = self::scaleSVGUnit( $viewBox[3] );
327 if ( $viewWidth > 0 && $viewHeight > 0 ) {
328 $aspect = $viewWidth / $viewHeight;
329 $defaultHeight = $defaultWidth / $aspect;
333 if ( $this->reader->getAttribute(
'width' ) ) {
334 $width = self::scaleSVGUnit( $this->reader->getAttribute(
'width' ) ??
'', $defaultWidth );
335 $this->metadata[
'originalWidth'] = $this->reader->getAttribute(
'width' );
337 if ( $this->reader->getAttribute(
'height' ) ) {
338 $height = self::scaleSVGUnit( $this->reader->getAttribute(
'height' ) ??
'', $defaultHeight );
339 $this->metadata[
'originalHeight'] = $this->reader->getAttribute(
'height' );
342 if ( $width ===
null && $height ===
null ) {
343 $width = $defaultWidth;
344 $height = $width / $aspect;
345 } elseif ( $width !==
null && $height ===
null ) {
346 $height = $width / $aspect;
347 } elseif ( $height !==
null && $width ===
null ) {
348 $width = $height * $aspect;
351 if ( $width > 0 && $height > 0 ) {
352 $this->metadata[
'width'] = (int)round( $width );
353 $this->metadata[
'height'] = (int)round( $height );
368 static $unitLength = [
385 '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*' .
386 '(rem|em|ex|px|pt|pc|cm|mm|in|ch|q|%)\s*$/i',
392 if ( $unit ===
'%' ) {
393 return $length * 0.01 * $viewportSize;
396 return $length * $unitLength[$unit];
400 return (
float)$length;