'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePageModule.php',
'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
+ 'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
+ 'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php',
'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php',
'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
public static function newFromContext( ResourceLoaderContext $context ) {
$cache = new self();
- if ( $context->getOnly() === 'styles' ) {
+ if ( $context->getImage() ) {
+ $cache->mType = 'image';
+ } elseif ( $context->getOnly() === 'styles' ) {
$cache->mType = 'css';
} else {
$cache->mType = 'js';
// Get all query values
$queryVals = $context->getRequest()->getValues();
foreach ( $queryVals as $query => $val ) {
- if ( $query === 'modules' || $query === 'version' || $query === '*' ) {
+ if ( in_array( $query, array( 'modules', 'image', 'variant', 'version', '*' ) ) ) {
+ // Use file cache regardless of the value of this parameter
continue; // note: &* added as IE fix
} elseif ( $query === 'skin' && $val === $wgDefaultSkin ) {
continue;
continue;
} elseif ( $query === 'debug' && $val === 'false' ) {
continue;
+ } elseif ( $query === 'format' && $val === 'rasterized' ) {
+ continue;
}
return false;
// Generate a response
$response = $this->makeModuleResponse( $context, $modules, $missing );
- // Prepend comments indicating exceptions
- $response = $errors . $response;
-
// Capture any PHP warnings from the output buffer and append them to the
- // response in a comment if we're in debug mode.
+ // error list if we're in debug mode.
if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
- $response = self::makeComment( $warnings ) . $response;
+ $errors .= self::makeComment( $warnings );
+ $this->hasErrors = true;
+ }
+
+ if ( $context->getImageObj() && !$response ) {
+ $errors .= self::makeComment( "Image generation failed." );
$this->hasErrors = true;
}
+ if ( $this->hasErrors ) {
+ if ( $context->getImageObj() ) {
+ // Bail, we can't show both the error messages and the response when it's not CSS or JS.
+ // sendResponseHeaders() will handle this sensibly.
+ $response = $errors;
+ } else {
+ // Prepend comments indicating exceptions
+ $response = $errors . $response;
+ }
+ }
+
// Save response to file cache unless there are errors
- if ( isset( $fileCache ) && !$errors && !count( $missing ) ) {
- // Cache single modules...and other requests if there are enough hits
+ if ( isset( $fileCache ) && !$this->hasErrors && !count( $missing ) ) {
+ // Cache single modules and images...and other requests if there are enough hits
if ( ResourceFileCache::useFileCache( $context ) ) {
if ( $fileCache->isCacheWorthy() ) {
$fileCache->saveText( $response );
$maxage = $rlMaxage['versioned']['client'];
$smaxage = $rlMaxage['versioned']['server'];
}
- if ( $context->getOnly() === 'styles' ) {
+ if ( $context->getImageObj() ) {
+ if ( $errors ) {
+ header( 'Content-Type: text/plain; charset=utf-8' );
+ } else {
+ $context->getImageObj()->sendResponseHeaders( $context );
+ }
+ } elseif ( $context->getOnly() === 'styles' ) {
header( 'Content-Type: text/css; charset=utf-8' );
header( 'Access-Control-Allow-Origin: *' );
} else {
wfProfileIn( __METHOD__ );
+ $image = $context->getImageObj();
+ if ( $image ) {
+ $data = $image->getImageData( $context );
+ wfProfileOut( __METHOD__ );
+ return $data;
+ }
+
// Pre-fetch blobs
if ( $context->shouldIncludeMessages() ) {
try {
protected $version;
protected $hash;
protected $raw;
+ protected $image;
+ protected $variant;
+ protected $format;
protected $userObj;
+ protected $imageObj;
/* Methods */
$this->only = $request->getVal( 'only' );
$this->version = $request->getVal( 'version' );
$this->raw = $request->getFuzzyBool( 'raw' );
+ // Image requests
+ $this->image = $request->getVal( 'image' );
+ $this->variant = $request->getVal( 'variant' );
+ $this->format = $request->getVal( 'format' );
$skinnames = Skin::getSkinNames();
// If no skin is specified, or we don't recognize the skin, use the default skin
return $this->raw;
}
+ /**
+ * @return string|null
+ */
+ public function getImage() {
+ return $this->image;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVariant() {
+ return $this->variant;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getFormat() {
+ return $this->format;
+ }
+
+ /**
+ * If this is a request for an image, get the ResourceLoaderImage object.
+ *
+ * @since 1.25
+ * @return ResourceLoaderImage|bool false if a valid object cannot be created
+ */
+ public function getImageObj() {
+ if ( $this->imageObj === null ) {
+ $this->imageObj = false;
+
+ if ( !$this->image ) {
+ return $this->imageObj;
+ }
+
+ $modules = $this->getModules();
+ if ( count( $modules ) !== 1 ) {
+ return $this->imageObj;
+ }
+
+ $module = $this->getResourceLoader()->getModule( $modules[0] );
+ if ( !$module || !$module instanceof ResourceLoaderImageModule ) {
+ return $this->imageObj;
+ }
+
+ $image = $module->getImage( $this->image );
+ if ( !$image ) {
+ return $this->imageObj;
+ }
+
+ $this->imageObj = $image;
+ }
+
+ return $this->imageObj;
+ }
+
/**
* @return bool
*/
if ( !isset( $this->hash ) ) {
$this->hash = implode( '|', array(
$this->getLanguage(), $this->getDirection(), $this->getSkin(), $this->getUser(),
+ $this->getImage(), $this->getVariant(), $this->getFormat(),
$this->getDebug(), $this->getOnly(), $this->getVersion()
) );
}
--- /dev/null
+<?php
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImage {
+
+ /**
+ * Map of allowed file extensions to their MIME types.
+ * @var array
+ */
+ protected static $fileTypes = array(
+ 'svg' => 'image/svg+xml',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'jpg' => 'image/jpg',
+ );
+
+ /**
+ * @param string $name Image name
+ * @param string $module Module name
+ * @param string|array $descriptor Path to image file, or array structure containing paths
+ * @param string $basePath Directory to which paths in descriptor refer
+ * @param array $variants
+ * @throws MWException
+ */
+ public function __construct( $name, $module, $descriptor, $basePath, $variants ) {
+ $this->name = $name;
+ $this->module = $module;
+ $this->descriptor = $descriptor;
+ $this->basePath = $basePath;
+ $this->variants = $variants;
+
+ // Ensure that all files have common extension.
+ $extensions = array();
+ $descriptor = (array)$descriptor;
+ array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
+ $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+ } );
+ $extensions = array_unique( $extensions );
+ if ( count( $extensions ) !== 1 ) {
+ throw new MWException( 'Image type for various images differs.' );
+ }
+ $ext = $extensions[0];
+ if ( !isset( self::$fileTypes[$ext] ) ) {
+ throw new MWException( 'Invalid image type; svg, png, gif or jpg required.' );
+ }
+ $this->extension = $ext;
+ }
+
+ /**
+ * Get name of this image.
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Get name of the module this image belongs to.
+ *
+ * @return string
+ */
+ public function getModule() {
+ return $this->module;
+ }
+
+ /**
+ * Get the list of variants this image can be converted to.
+ *
+ * @return string[]
+ */
+ public function getVariants() {
+ return array_keys( $this->variants );
+ }
+
+ /**
+ * Get the path to image file for given context.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @return string
+ */
+ protected function getPath( ResourceLoaderContext $context ) {
+ $desc = $this->descriptor;
+ if ( is_string( $desc ) ) {
+ return $this->basePath . '/' . $desc;
+ } elseif ( isset( $desc['lang'][ $context->getLanguage() ] ) ) {
+ return $this->basePath . '/' . $desc['lang'][ $context->getLanguage() ];
+ } elseif ( isset( $desc[ $context->getDirection() ] ) ) {
+ return $this->basePath . '/' . $desc[ $context->getDirection() ];
+ } else {
+ return $this->basePath . '/' . $desc['default'];
+ }
+ }
+
+ /**
+ * Get the extension of the image.
+ *
+ * @param string $format Format to get the extension for, 'original' or 'rasterized'
+ * @return string Extension without leading dot, e.g. 'png'
+ */
+ public function getExtension( $format = 'original' ) {
+ if ( $format === 'rasterized' && $this->extension === 'svg' ) {
+ return 'png';
+ } else {
+ return $this->extension;
+ }
+ }
+
+ /**
+ * Get the MIME type of the image.
+ *
+ * @param string $format Format to get the MIME type for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getMimeType( $format = 'original' ) {
+ $ext = $this->getExtension( $format );
+ return self::$fileTypes[$ext];
+ }
+
+ /**
+ * Get the load.php URL that will produce this image.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @param string $script URL to load.php
+ * @param string|null $variant Variant to get the URL for
+ * @param string $format Format to get the URL for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getUrl( ResourceLoaderContext $context, $script, $variant, $format ) {
+ $query = array(
+ 'modules' => $this->getModule(),
+ 'image' => $this->getName(),
+ 'variant' => $variant,
+ 'format' => $format,
+ 'lang' => $context->getLanguage(),
+ 'version' => $context->getVersion(),
+ );
+
+ return wfExpandUrl( wfAppendQuery( $script, $query ), PROTO_RELATIVE );
+ }
+
+ /**
+ * Get the data: URI that will produce this image.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @param string|null $variant Variant to get the URI for
+ * @param string $format Format to get the URI for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getDataUri( ResourceLoaderContext $context, $variant, $format ) {
+ $type = $this->getMimeType( $format );
+ $contents = $this->getImageData( $context, $variant, $format );
+ return CSSMin::encodeStringAsDataURI( $contents, $type );
+ }
+
+ /**
+ * Get actual image data for this image. This can be saved to a file or sent to the browser to
+ * produce the converted image.
+ *
+ * Call getExtension() or getMimeType() with the same $format argument to learn what file type the
+ * returned data uses.
+ *
+ * @param ResourceLoaderContext $context Image context, or any context of $variant and $format
+ * given.
+ * @param string|null $variant Variant to get the data for. Optional, if given, overrides the data
+ * from $context.
+ * @param string $format Format to get the data for, 'original' or 'rasterized'. Optional, if
+ * given, overrides the data from $context.
+ * @return string|false Possibly binary image data, or false on failure
+ */
+ public function getImageData( ResourceLoaderContext $context, $variant = false, $format = false ) {
+ if ( $variant === false ) {
+ $variant = $context->getVariant();
+ }
+ if ( $format === false ) {
+ $format = $context->getFormat();
+ }
+
+ if ( $this->getExtension() !== 'svg' ) {
+ return file_get_contents( $this->getPath( $context ) );
+ }
+
+ if ( $variant && isset( $this->variants[$variant] ) ) {
+ $data = $this->variantize( $this->variants[$variant], $context );
+ } else {
+ $data = file_get_contents( $this->getPath( $context ) );
+ }
+
+ if ( $format === 'rasterized' ) {
+ $data = $this->rasterize( $data );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Send response headers (using the header() function) that are necessary to correctly serve the
+ * image data for this image, as returned by getImageData().
+ *
+ * Note that the headers are independent of the language or image variant.
+ *
+ * @param ResourceLoaderContext $context Image context
+ */
+ public function sendResponseHeaders( ResourceLoaderContext $context ) {
+ $format = $context->getFormat();
+ $mime = $this->getMimeType( $format );
+ $filename = $this->getName() . '.' . $this->getExtension( $format );
+
+ header( 'Content-Type: ' . $mime );
+ header( 'Content-Disposition: ' .
+ FileBackend::makeContentDisposition( 'inline', $filename ) );
+ }
+
+ /**
+ * Convert this image, which is assumed to be SVG, to given variant.
+ *
+ * @param array $variantConf Array with a 'color' key, its value will be used as fill color
+ * @param ResourceLoaderContext $context Image context
+ * @return string New SVG file data
+ */
+ protected function variantize( $variantConf, ResourceLoaderContext $context ) {
+ $dom = new DomDocument;
+ $dom->load( $this->getPath( $context ) );
+ $root = $dom->documentElement;
+ $wrapper = $dom->createElement( 'g' );
+ while ( $root->firstChild ) {
+ $wrapper->appendChild( $root->firstChild );
+ }
+ $root->appendChild( $wrapper );
+ $wrapper->setAttribute( 'fill', $variantConf['color'] );
+ return $dom->saveXml();
+ }
+
+ /**
+ * Massage the SVG image data for converters which doesn't understand some path data syntax.
+ *
+ * This is necessary for rsvg and ImageMagick when compiled with rsvg support.
+ * Upstream bug is https://bugzilla.gnome.org/show_bug.cgi?id=620923, fixed 2014-11-10, so
+ * this will be needed for a while. (T76852)
+ *
+ * @param string $svg SVG image data
+ * @return string Massaged SVG image data
+ */
+ protected function massageSvgPathdata( $svg ) {
+ $dom = new DomDocument;
+ $dom->loadXml( $svg );
+ foreach ( $dom->getElementsByTagName( 'path' ) as $node ) {
+ $pathData = $node->getAttribute( 'd' );
+ // Make sure there is at least one space between numbers, and that leading zero is not omitted.
+ // rsvg has issues with syntax like "M-1-2" and "M.445.483" and especially "M-.445-.483".
+ $pathData = preg_replace( '/(-?)(\d*\.\d+|\d+)/', ' ${1}0$2 ', $pathData );
+ // Strip unnecessary leading zeroes for prettiness, not strictly necessary
+ $pathData = preg_replace( '/([ -])0(\d)/', '$1$2', $pathData );
+ $node->setAttribute( 'd', $pathData );
+ }
+ return $dom->saveXml();
+ }
+
+ /**
+ * Convert passed image data, which is assumed to be SVG, to PNG.
+ *
+ * @param string $svg SVG image data
+ * @return string|bool PNG image data, or false on failure
+ */
+ protected function rasterize( $svg ) {
+ // This code should be factored out to a separate method on SvgHandler, or perhaps a separate
+ // class, with a separate set of configuration settings.
+ //
+ // This is a distinct use case from regular SVG rasterization:
+ // * we can skip many sanity and security checks (as the images come from a trusted source,
+ // rather than from the user)
+ // * we need to provide extra options to some converters to achieve acceptable quality for very
+ // small images, which might cause performance issues in the general case
+ // * we need to directly pass image data to the converter instead of a file path
+ //
+ // See https://phabricator.wikimedia.org/T76473#801446 for examples of what happens with the
+ // default settings.
+ //
+ // For now, we special-case rsvg (used in WMF production) and do a messy workaround for other
+ // converters.
+
+ global $wgSVGConverter, $wgSVGConverterPath;
+
+ $svg = $this->massageSvgPathdata( $svg );
+
+ if ( $wgSVGConverter === 'rsvg' ) {
+ $command = 'rsvg-convert'; // Should be just 'rsvg'? T76476
+ if ( $wgSVGConverterPath ) {
+ $command = wfEscapeShellArg( "$wgSVGConverterPath/" ) . $command;
+ }
+
+ $process = proc_open(
+ $command,
+ array( 0 => array( 'pipe', 'r' ), 1 => array( 'pipe', 'w' ) ),
+ $pipes
+ );
+
+ if ( is_resource( $process ) ) {
+ fwrite( $pipes[0], $svg );
+ fclose( $pipes[0] );
+ $png = stream_get_contents( $pipes[1] );
+ fclose( $pipes[1] );
+ proc_close( $process );
+
+ return $png ?: false;
+ }
+ return false;
+
+ } else {
+ // Write input to and read output from a temporary file
+ $tempFilenameSvg = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+ $tempFilenamePng = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+
+ file_put_contents( $tempFilenameSvg, $svg );
+
+ $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg );
+ if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) {
+ return false;
+ }
+
+ $handler = new SvgHandler;
+ $handler->rasterize( $tempFilenameSvg, $tempFilenamePng, $metadata['width'], $metadata['height'] );
+
+ $png = file_get_contents( $tempFilenamePng );
+
+ unlink( $tempFilenameSvg );
+ unlink( $tempFilenamePng );
+
+ return $png ?: false;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Resource loader module for generated and embedded images.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ */
+
+/**
+ * Resource loader module for generated and embedded images.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImageModule extends ResourceLoaderModule {
+
+ /**
+ * Local base path, see __construct()
+ * @var string
+ */
+ protected $localBasePath = '';
+
+ protected $origin = self::ORIGIN_CORE_SITEWIDE;
+
+ protected $images = array();
+ protected $variants = array();
+ protected $prefix = array();
+
+ /**
+ * Constructs a new module from an options array.
+ *
+ * @param array $options List of options; if not given or empty, an empty module will be
+ * constructed
+ * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
+ * to $IP
+ *
+ * Below is a description for the $options array:
+ * @par Construction options:
+ * @code
+ * array(
+ * // Base path to prepend to all local paths in $options. Defaults to $IP
+ * 'localBasePath' => [base path],
+ * // CSS class prefix to use in all style rules
+ * 'prefix' => [CSS class prefix],
+ * // List of variants that may be used for the image files
+ * 'variants' => array(
+ * // ([image type] is a string, used in generated CSS class names and to match variants to images)
+ * [image type] => array(
+ * [variant name] => array(
+ * 'color' => [color string, e.g. '#ffff00'],
+ * 'global' => [boolean, if true, this variant is available for all images of this type],
+ * ),
+ * )
+ * ),
+ * // List of image files and their options
+ * 'images' => array(
+ * [image type] => array(
+ * [file path string],
+ * [file path string] => array(
+ * 'name' => [image name string, defaults to file name],
+ * 'variants' => [array of variant name strings, variants available for this image],
+ * ),
+ * )
+ * ),
+ * )
+ * @endcode
+ * @throws MWException
+ */
+ public function __construct( $options = array(), $localBasePath = null ) {
+ $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
+
+ if ( !isset( $options['prefix'] ) || !$options['prefix'] ) {
+ throw new MWException(
+ "Required 'prefix' option not given or empty."
+ );
+ }
+
+ foreach ( $options as $member => $option ) {
+ switch ( $member ) {
+ case 'images':
+ if ( !is_array( $option ) ) {
+ throw new MWException(
+ "Invalid collated file path list error. '$option' given, array expected."
+ );
+ }
+ foreach ( $option as $key => $value ) {
+ if ( !is_string( $key ) ) {
+ throw new MWException(
+ "Invalid collated file path list key error. '$key' given, string expected."
+ );
+ }
+ $this->{$member}[$key] = (array)$value;
+ }
+ break;
+
+ case 'variants':
+ if ( !is_array( $option ) ) {
+ throw new MWException(
+ "Invalid variant list error. '$option' given, array expected."
+ );
+ }
+ $this->{$member} = $option;
+ break;
+
+ case 'prefix':
+ $this->{$member} = (string)$option;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get CSS class prefix used by this module.
+ * @return string
+ */
+ public function getPrefix() {
+ return $this->prefix;
+ }
+
+ /**
+ * Get a ResourceLoaderImage object for given image.
+ * @param string $name Image name
+ * @return ResourceLoaderImage|null
+ */
+ public function getImage( $name ) {
+ $images = $this->getImages();
+ return isset( $images[$name] ) ? $images[$name] : null;
+ }
+
+ /**
+ * Get ResourceLoaderImage objects for all images.
+ * @return ResourceLoaderImage[] Array keyed by image name
+ */
+ public function getImages() {
+ if ( !isset( $this->imageObjects ) ) {
+ $this->imageObjects = array();
+
+ foreach ( $this->images as $type => $list ) {
+ foreach ( $list as $name => $options ) {
+ $imageDesc = is_string( $options ) ? $options : $options['image'];
+
+ $allowedVariants = array_merge(
+ isset( $options['variants'] ) ? $options['variants'] : array(),
+ $this->getGlobalVariants( $type )
+ );
+ $variantConfig = array_intersect_key(
+ $this->variants[$type],
+ array_fill_keys( $allowedVariants, true )
+ );
+
+ $image = new ResourceLoaderImage( $name, $this->getName(), $imageDesc, $this->localBasePath, $variantConfig );
+ $this->imageObjects[ $image->getName() ] = $image;
+ }
+ }
+ }
+
+ return $this->imageObjects;
+ }
+
+ /**
+ * Get list of variants in this module that are 'global' for given type of images, i.e., available
+ * for every image of given type regardless of image options.
+ * @param string $type Image type
+ * @return string[]
+ */
+ public function getGlobalVariants( $type ) {
+ if ( !isset( $this->globalVariants[$type] ) ) {
+ $this->globalVariants[$type] = array();
+
+ foreach ( $this->variants[$type] as $name => $config ) {
+ if ( isset( $config['global'] ) && $config['global'] ) {
+ $this->globalVariants[$type][] = $name;
+ }
+ }
+ }
+
+ return $this->globalVariants[$type];
+ }
+
+ /**
+ * Get the type of given image.
+ * @param string $imageName Image name
+ * @return string
+ */
+ public function getImageType( $imageName ) {
+ foreach ( $this->images as $type => $list ) {
+ foreach ( $list as $key => $value ) {
+ $file = is_int( $key ) ? $value : $key;
+ $options = is_array( $value ) ? $value : array();
+ $name = isset( $options['name'] ) ? $options['name'] : pathinfo( $file, PATHINFO_FILENAME );
+ if ( $name === $imageName ) {
+ return $type;
+ }
+ }
+ }
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ // Build CSS rules
+ $rules = array();
+ $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
+ $prefix = $this->getPrefix();
+
+ foreach ( $this->getImages() as $name => $image ) {
+ $type = $this->getImageType( $name );
+
+ $declarations = $this->getCssDeclarations(
+ $image->getDataUri( $context, null, 'original' ),
+ $image->getUrl( $context, $script, null, 'rasterized' )
+ );
+ $declarations = implode( "\n\t", $declarations );
+ $rules[] = ".$prefix-$type-$name {\n\t$declarations\n}";
+
+ // TODO: Get variant configurations from $context->getSkin()
+ foreach ( $image->getVariants() as $variant ) {
+ $declarations = $this->getCssDeclarations(
+ $image->getDataUri( $context, $variant, 'original' ),
+ $image->getUrl( $context, $script, $variant, 'rasterized' )
+ );
+ $declarations = implode( "\n\t", $declarations );
+ $rules[] = ".$prefix-$type-$name-$variant {\n\t$declarations\n}";
+ }
+ }
+
+ $style = implode( "\n", $rules );
+ if ( $this->getFlip( $context ) ) {
+ $style = CSSJanus::transform( $style, true, false );
+ }
+ return array( 'all' => $style );
+ }
+
+ /**
+ * @param string $primary Primary URI
+ * @param string $fallback Fallback URI
+ * @return string[] CSS declarations to use given URIs as background-image
+ */
+ protected function getCssDeclarations( $primary, $fallback ) {
+ // SVG support using a transparent gradient to guarantee cross-browser
+ // compatibility (browsers able to understand gradient syntax support also SVG).
+ // http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+ return array(
+ "background-image: url($fallback);",
+ "background-image: -webkit-linear-gradient(transparent, transparent), url($primary);",
+ "background-image: linear-gradient(transparent, transparent), url($primary);",
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * Extract a local base path from module definition information.
+ *
+ * @param array $options Module definition
+ * @param string $localBasePath Path to use if not provided in module definition. Defaults
+ * to $IP
+ * @return string Local base path
+ */
+ public static function extractLocalBasePath( $options, $localBasePath = null ) {
+ global $IP;
+
+ if ( $localBasePath === null ) {
+ $localBasePath = $IP;
+ }
+
+ if ( array_key_exists( 'localBasePath', $options ) ) {
+ $localBasePath = (string)$options['localBasePath'];
+ }
+
+ return $localBasePath;
+ }
+}