MediaWiki REL1_34
ResourceLoaderImage.php
Go to the documentation of this file.
1<?php
22
30
35 protected static $fileTypes = [
36 'svg' => 'image/svg+xml',
37 'png' => 'image/png',
38 'gif' => 'image/gif',
39 'jpg' => 'image/jpg',
40 ];
41
43 private $name;
45 private $module;
47 private $descriptor;
49 private $basePath;
51 private $variants;
55 private $extension;
56
68 $defaultColor = null
69 ) {
70 $this->name = $name;
71 $this->module = $module;
72 $this->descriptor = $descriptor;
73 $this->basePath = $basePath;
74 $this->variants = $variants;
75 $this->defaultColor = $defaultColor;
76
77 // Expand shorthands:
78 // [ "en,de,fr" => "foo.svg" ]
79 // → [ "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ]
80 if ( is_array( $this->descriptor ) && isset( $this->descriptor['lang'] ) ) {
81 foreach ( array_keys( $this->descriptor['lang'] ) as $langList ) {
82 if ( strpos( $langList, ',' ) !== false ) {
83 $this->descriptor['lang'] += array_fill_keys(
84 explode( ',', $langList ),
85 $this->descriptor['lang'][$langList]
86 );
87 unset( $this->descriptor['lang'][$langList] );
88 }
89 }
90 }
91 // Remove 'deprecated' key
92 if ( is_array( $this->descriptor ) ) {
93 unset( $this->descriptor['deprecated'] );
94 }
95
96 // Ensure that all files have common extension.
97 $extensions = [];
98 $descriptor = is_array( $this->descriptor ) ? $this->descriptor : [ $this->descriptor ];
99 array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
100 $extensions[] = pathinfo( $this->getLocalPath( $path ), PATHINFO_EXTENSION );
101 } );
102 $extensions = array_unique( $extensions );
103 if ( count( $extensions ) !== 1 ) {
104 throw new InvalidArgumentException(
105 "File type for different image files of '$name' not the same in module '$module'"
106 );
107 }
108 $ext = $extensions[0];
109 if ( !isset( self::$fileTypes[$ext] ) ) {
110 throw new InvalidArgumentException(
111 "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg) in module '$module'"
112 );
113 }
114 $this->extension = $ext;
115 }
116
122 public function getName() {
123 return $this->name;
124 }
125
131 public function getModule() {
132 return $this->module;
133 }
134
140 public function getVariants() {
141 return array_keys( $this->variants );
142 }
143
152 $desc = $this->descriptor;
153 if ( !is_array( $desc ) ) {
154 return $this->getLocalPath( $desc );
155 }
156 if ( isset( $desc['lang'] ) ) {
157 $contextLang = $context->getLanguage();
158 if ( isset( $desc['lang'][$contextLang] ) ) {
159 return $this->getLocalPath( $desc['lang'][$contextLang] );
160 }
161 $fallbacks = Language::getFallbacksFor( $contextLang, Language::STRICT_FALLBACKS );
162 foreach ( $fallbacks as $lang ) {
163 if ( isset( $desc['lang'][$lang] ) ) {
164 return $this->getLocalPath( $desc['lang'][$lang] );
165 }
166 }
167 }
168 if ( isset( $desc[$context->getDirection()] ) ) {
169 return $this->getLocalPath( $desc[$context->getDirection()] );
170 }
171 if ( isset( $desc['default'] ) ) {
172 return $this->getLocalPath( $desc['default'] );
173 } else {
174 throw new MWException( 'No matching path found' );
175 }
176 }
177
182 protected function getLocalPath( $path ) {
183 if ( $path instanceof ResourceLoaderFilePath ) {
184 return $path->getLocalPath();
185 }
186
187 return "{$this->basePath}/$path";
188 }
189
196 public function getExtension( $format = 'original' ) {
197 if ( $format === 'rasterized' && $this->extension === 'svg' ) {
198 return 'png';
199 }
200 return $this->extension;
201 }
202
209 public function getMimeType( $format = 'original' ) {
210 $ext = $this->getExtension( $format );
211 return self::$fileTypes[$ext];
212 }
213
223 public function getUrl( ResourceLoaderContext $context, $script, $variant, $format ) {
224 $query = [
225 'modules' => $this->getModule(),
226 'image' => $this->getName(),
227 'variant' => $variant,
228 'format' => $format,
229 ];
230 if ( $this->varyOnLanguage() ) {
231 $query['lang'] = $context->getLanguage();
232 }
233 // The following parameters are at the end to keep the original order of the parameters.
234 $query['skin'] = $context->getSkin();
235 $rl = $context->getResourceLoader();
236 $query['version'] = $rl->makeVersionQuery( $context, [ $this->getModule() ] );
237
238 return wfAppendQuery( $script, $query );
239 }
240
249 public function getDataUri( ResourceLoaderContext $context, $variant, $format ) {
250 $type = $this->getMimeType( $format );
251 $contents = $this->getImageData( $context, $variant, $format );
252 return CSSMin::encodeStringAsDataURI( $contents, $type );
253 }
254
271 public function getImageData( ResourceLoaderContext $context, $variant = false, $format = false ) {
272 if ( $variant === false ) {
273 $variant = $context->getVariant();
274 }
275 if ( $format === false ) {
276 $format = $context->getFormat();
277 }
278
279 $path = $this->getPath( $context );
280 if ( !file_exists( $path ) ) {
281 throw new MWException( "File '$path' does not exist" );
282 }
283
284 if ( $this->getExtension() !== 'svg' ) {
285 return file_get_contents( $path );
286 }
287
288 if ( $variant && isset( $this->variants[$variant] ) ) {
289 $data = $this->variantize( $this->variants[$variant], $context );
290 } else {
292 $data = $defaultColor ?
293 $this->variantize( [ 'color' => $defaultColor ], $context ) :
294 file_get_contents( $path );
295 }
296
297 if ( $format === 'rasterized' ) {
298 $data = $this->rasterize( $data );
299 if ( !$data ) {
300 wfDebugLog( 'ResourceLoaderImage', __METHOD__ . " failed to rasterize for $path" );
301 }
302 }
303
304 return $data;
305 }
306
316 $format = $context->getFormat();
317 $mime = $this->getMimeType( $format );
318 $filename = $this->getName() . '.' . $this->getExtension( $format );
319
320 header( 'Content-Type: ' . $mime );
321 header( 'Content-Disposition: ' .
322 FileBackend::makeContentDisposition( 'inline', $filename ) );
323 }
324
332 protected function variantize( $variantConf, ResourceLoaderContext $context ) {
333 $dom = new DOMDocument;
334 $dom->loadXML( file_get_contents( $this->getPath( $context ) ) );
335 $root = $dom->documentElement;
336 $titleNode = null;
337 $wrapper = $dom->createElementNS( 'http://www.w3.org/2000/svg', 'g' );
338 // Reattach all direct children of the `<svg>` root node to the `<g>` wrapper
339 while ( $root->firstChild ) {
340 $node = $root->firstChild;
341 // @phan-suppress-next-line PhanUndeclaredProperty False positive
342 if ( !$titleNode && $node->nodeType === XML_ELEMENT_NODE && $node->tagName === 'title' ) {
343 // Remember the first encountered `<title>` node
344 $titleNode = $node;
345 }
346 $wrapper->appendChild( $node );
347 }
348 if ( $titleNode ) {
349 // Reattach the `<title>` node to the `<svg>` root node rather than the `<g>` wrapper
350 $root->appendChild( $titleNode );
351 }
352 $root->appendChild( $wrapper );
353 $wrapper->setAttribute( 'fill', $variantConf['color'] );
354 return $dom->saveXML();
355 }
356
367 protected function massageSvgPathdata( $svg ) {
368 $dom = new DOMDocument;
369 $dom->loadXML( $svg );
370 foreach ( $dom->getElementsByTagName( 'path' ) as $node ) {
371 $pathData = $node->getAttribute( 'd' );
372 // Make sure there is at least one space between numbers, and that leading zero is not omitted.
373 // rsvg has issues with syntax like "M-1-2" and "M.445.483" and especially "M-.445-.483".
374 $pathData = preg_replace( '/(-?)(\d*\.\d+|\d+)/', ' ${1}0$2 ', $pathData );
375 // Strip unnecessary leading zeroes for prettiness, not strictly necessary
376 $pathData = preg_replace( '/([ -])0(\d)/', '$1$2', $pathData );
377 $node->setAttribute( 'd', $pathData );
378 }
379 return $dom->saveXML();
380 }
381
388 protected function rasterize( $svg ) {
408
409 $svg = $this->massageSvgPathdata( $svg );
410
411 // Sometimes this might be 'rsvg-secure'. Long as it's rsvg.
412 if ( strpos( $wgSVGConverter, 'rsvg' ) === 0 ) {
413 $command = 'rsvg-convert';
414 if ( $wgSVGConverterPath ) {
415 $command = Shell::escape( "$wgSVGConverterPath/" ) . $command;
416 }
417
418 $process = proc_open(
419 $command,
420 [ 0 => [ 'pipe', 'r' ], 1 => [ 'pipe', 'w' ] ],
421 $pipes
422 );
423
424 if ( is_resource( $process ) ) {
425 fwrite( $pipes[0], $svg );
426 fclose( $pipes[0] );
427 $png = stream_get_contents( $pipes[1] );
428 fclose( $pipes[1] );
429 proc_close( $process );
430
431 return $png ?: false;
432 }
433 return false;
434
435 } else {
436 // Write input to and read output from a temporary file
437 $tempFilenameSvg = tempnam( wfTempDir(), 'ResourceLoaderImage' );
438 $tempFilenamePng = tempnam( wfTempDir(), 'ResourceLoaderImage' );
439
440 file_put_contents( $tempFilenameSvg, $svg );
441
442 $svgReader = new SVGReader( $tempFilenameSvg );
443 $metadata = $svgReader->getMetadata();
444 if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) {
445 unlink( $tempFilenameSvg );
446 return false;
447 }
448
449 $handler = new SvgHandler;
450 $res = $handler->rasterize(
451 $tempFilenameSvg,
452 $tempFilenamePng,
453 $metadata['width'],
454 $metadata['height']
455 );
456 unlink( $tempFilenameSvg );
457
458 $png = null;
459 if ( $res === true ) {
460 $png = file_get_contents( $tempFilenamePng );
461 unlink( $tempFilenamePng );
462 }
463
464 return $png ?: false;
465 }
466 }
467
473 private function varyOnLanguage() {
474 return is_array( $this->descriptor ) && (
475 isset( $this->descriptor['ltr'] ) ||
476 isset( $this->descriptor['rtl'] ) ||
477 isset( $this->descriptor['lang'] ) );
478 }
479}
$wgSVGConverter
Pick a converter defined in $wgSVGConverters.
$wgSVGConverterPath
If not in the executable PATH, specify the SVG converter path.
wfTempDir()
Tries to get the system directory for temporary files.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
$command
Definition cdb.php:65
static makeContentDisposition( $type, $filename='')
Build a Content-Disposition header value per RFC 6266.
MediaWiki exception.
Executes shell commands.
Definition Shell.php:44
Context object that contains information about the state of a specific ResourceLoader web request.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Class encapsulating an image used in a ResourceLoaderImageModule.
rasterize( $svg)
Convert passed image data, which is assumed to be SVG, to PNG.
variantize( $variantConf, ResourceLoaderContext $context)
Convert this image, which is assumed to be SVG, to given variant.
getUrl(ResourceLoaderContext $context, $script, $variant, $format)
Get the load.php URL that will produce this image.
getPath(ResourceLoaderContext $context)
Get the path to image file for given context.
massageSvgPathdata( $svg)
Massage the SVG image data for converters which don't understand some path data syntax.
getMimeType( $format='original')
Get the MIME type of the image.
getModule()
Get name of the module this image belongs to.
getExtension( $format='original')
Get the extension of the image.
__construct( $name, $module, $descriptor, $basePath, $variants, $defaultColor=null)
varyOnLanguage()
Check if the image depends on the language.
getDataUri(ResourceLoaderContext $context, $variant, $format)
Get the data: URI that will produce this image.
sendResponseHeaders(ResourceLoaderContext $context)
Send response headers (using the header() function) that are necessary to correctly serve the image d...
getName()
Get name of this image.
getVariants()
Get the list of variants this image can be converted to.
static array $fileTypes
Map of allowed file extensions to their MIME types.
getImageData(ResourceLoaderContext $context, $variant=false, $format=false)
Get actual image data for this image.
Handler for SVG images.
rasterize( $srcPath, $dstPath, $width, $height, $lang=false)
Transform an SVG file to PNG This function can be called outside of thumbnail contexts.
$context
Definition load.php:45
if(!is_readable( $file)) $ext
Definition router.php:48
if(!isset( $args[0])) $lang