31 use Wikimedia\AtEase\AtEase;
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 $status = $this->reader->XML( $contents,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
78 $status = $this->reader->open(
$source,
null, LIBXML_NOERROR | LIBXML_NOWARNING );
81 throw new MWException(
"Error getting xml of SVG." );
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();
111 }
catch ( Exception $e ) {
116 libxml_disable_entity_loader( $oldDisable );
117 AtEase::restoreWarnings();
125 return $this->metadata;
134 $keepReading = $this->reader->read();
137 while ( $keepReading && $this->reader->nodeType !== XMLReader::ELEMENT ) {
138 $keepReading = $this->reader->read();
141 if ( $this->reader->localName !==
'svg' || $this->reader->namespaceURI !== self::NS_SVG ) {
142 throw new MWException(
"Expected <svg> tag, got " .
143 $this->reader->localName .
" in NS " . $this->reader->namespaceURI );
145 $this->debug(
'<svg> tag is correct.' );
146 $this->handleSVGAttribs();
148 $exitDepth = $this->reader->depth;
149 $keepReading = $this->reader->read();
150 while ( $keepReading ) {
151 $tag = $this->reader->localName;
152 $type = $this->reader->nodeType;
153 $isSVG = ( $this->reader->namespaceURI === self::NS_SVG );
155 $this->debug(
"$tag" );
157 if ( $isSVG && $tag ===
'svg' &&
$type === XMLReader::END_ELEMENT
158 && $this->reader->depth <= $exitDepth
163 if ( $isSVG && $tag ===
'title' ) {
164 $this->readField( $tag,
'title' );
165 } elseif ( $isSVG && $tag ===
'desc' ) {
166 $this->readField( $tag,
'description' );
167 } elseif ( $isSVG && $tag ===
'metadata' &&
$type === XMLReader::ELEMENT ) {
168 $this->readXml(
'metadata' );
169 } elseif ( $isSVG && $tag ===
'script' ) {
173 $this->metadata[
'animated'] =
true;
174 } elseif ( $tag !==
'#text' ) {
175 $this->debug(
"Unhandled top-level XML tag $tag" );
178 $this->animateFilterAndLang( $tag );
182 $keepReading = $this->reader->next();
185 $this->reader->close();
187 $this->metadata[
'translations'] = $this->languages + $this->languagePrefixes;
198 private function readField( $name, $metafield =
null ) {
199 $this->debug(
"Read field $metafield" );
200 if ( !$metafield || $this->reader->nodeType !== XMLReader::ELEMENT ) {
203 $keepReading = $this->reader->read();
204 while ( $keepReading ) {
205 if ( $this->reader->localName === $name
206 && $this->reader->namespaceURI === self::NS_SVG
207 && $this->reader->nodeType === XMLReader::END_ELEMENT
212 if ( $this->reader->nodeType === XMLReader::TEXT ) {
213 $this->metadata[$metafield] = trim( $this->reader->value );
215 $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 );
277 $dash = strpos( $langItem,
'-' );
280 $itemPrefix = substr( $langItem, 0, $dash );
287 switch ( $this->reader->localName ) {
295 case 'animateMotion':
297 case 'animateTransform':
298 $this->debug(
"HOUSTON WE HAVE ANIMATION" );
299 $this->metadata[
'animated'] =
true;
303 $keepReading = $this->reader->read();
307 private function debug( $data ) {
308 if ( $this->mDebug ) {
318 private function handleSVGAttribs() {
319 $defaultWidth = self::DEFAULT_WIDTH;
320 $defaultHeight = self::DEFAULT_HEIGHT;
325 if ( $this->reader->getAttribute(
'viewBox' ) ) {
327 $viewBox = preg_split(
'/\s*[\s,]\s*/', trim( $this->reader->getAttribute(
'viewBox' ) ??
'' ) );
328 if ( count( $viewBox ) === 4 ) {
331 if ( $viewWidth > 0 && $viewHeight > 0 ) {
332 $aspect = $viewWidth / $viewHeight;
333 $defaultHeight = $defaultWidth / $aspect;
337 if ( $this->reader->getAttribute(
'width' ) ) {
338 $width =
self::scaleSVGUnit( $this->reader->getAttribute(
'width' ) ??
'', $defaultWidth );
339 $this->metadata[
'originalWidth'] = $this->reader->getAttribute(
'width' );
341 if ( $this->reader->getAttribute(
'height' ) ) {
342 $height =
self::scaleSVGUnit( $this->reader->getAttribute(
'height' ) ??
'', $defaultHeight );
343 $this->metadata[
'originalHeight'] = $this->reader->getAttribute(
'height' );
346 if ( !isset( $width ) && !isset( $height ) ) {
347 $width = $defaultWidth;
348 $height = $width / $aspect;
349 } elseif ( isset( $width ) && !isset( $height ) ) {
350 $height = $width / $aspect;
351 } elseif ( isset( $height ) && !isset( $width ) ) {
352 $width = $height * $aspect;
355 if ( $width > 0 && $height > 0 ) {
356 $this->metadata[
'width'] = (int)round( $width );
357 $this->metadata[
'height'] = (int)round( $height );
370 static $unitLength = [
383 '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/',
389 if ( $unit ===
'%' ) {
390 return $length * 0.01 * $viewportSize;
393 return $length * $unitLength[$unit];
397 return (
float)$length;
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static isWellFormedLanguageTag(string $code, bool $lenient=false)
Returns true if a language code string is a well-formed language tag according to RFC 5646.
A class containing constants representing the names of configuration variables.
static scaleSVGUnit( $length, $viewportSize=512)
Return a rounded pixel equivalent for a labeled CSS/SVG length.
__construct( $source)
Creates an SVGReader drawing from the source provided.