Skip to content

Commit

Permalink
Initial stab at responsive images for screen densities.
Browse files Browse the repository at this point in the history
* adds $wgResponsiveImages setting, defaulting to true, to enable the feature
* adds 'srcset' attribute with 1.5x and 2x URLs to image links and image thumbs
* adds jquery.hidpi plugin to check pixel density and implement partial 'srcset' polyfill
** $.devicePixelRatio() returns window.devicePixelRatio, with compat fallback for IE 10
** $().hidpi() performs a 'srcset' polyfill for browsers with no native 'srcset' support
* adds mediawiki.hidpi RL script to trigger hidpi loads after main images load

Note that this is a work in progress. There will be places where this doesn't yet work which output their imgs differently. If moving from a low to high-DPI screen on a MacBook Pro Retina display, you won't see images load until you reload.

Confirmed basic images and thumbs in wikitext appear to work in Safari 6, Chrome 21, Firefox 18 nightly on MacBook Pro Retina display, and IE 10 in Windows 8 at 150% zoom, 200% zoom, and 140% and 180%-ratio Metro tablet sizes.

Internally this is still a bit of a hack; Linker::makeImageLink and Linker::makeThumbLink explicitly ask for 1.5x and 2x scaled versions and insert their URLs, if different, into the original thumbnail object which (in default handler) outputs the srcset. This means that a number of places that handle images differently won't see the higher-resolution versions, such as <gallery> and the large thumbnail on the File: description page.

At some point we may wish to redo some of how the MediaHandler stuff works so that requesting a single thumbnail automatically produces the extra sizes in all circumstances. We might also consider outputting a 'srcset' or multiple src sizes in 'imageinfo' API requests, which would make ApiForeignRepo/InstantCommons more efficient. (Currently it has to make three requests for each image to get the three sizes.)

Change-Id: Id80ebd07a1a9f401a2c2bfeb21aae987e5aa863b
  • Loading branch information
bvibber committed Oct 11, 2012
1 parent 19745cb commit 966cda2
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 31 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES-1.21
Expand Up @@ -19,6 +19,7 @@ production.
* (bug 34876) jquery.makeCollapsible has been improved in performance.
* Added ContentHandler facility to allow extensions to support other content than wikitext.
See docs/contenthandler.txt for details.
* $wgResponsiveImages is added to support images on high-DPI mobile and desktop displays.

=== Bug fixes in 1.21 ===
* (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
Expand Down
10 changes: 10 additions & 0 deletions includes/DefaultSettings.php
Expand Up @@ -1088,6 +1088,16 @@
*/
$wgDirectoryMode = 0777;

/**
* Generate and use thumbnails suitable for screens with 1.5 and 2.0 pixel densities.
*
* This means a 320x240 use of an image on the wiki will also generate 480x360 and 640x480
* thumbnails, output via data-src-1-5 and data-src-2-0. Runtime JavaScript switches the
* images in after loading the original low-resolution versions depending on the reported
* window.devicePixelRatio.
*/
$wgResponsiveImages = true;

/**
* @name DJVU settings
* @{
Expand Down
18 changes: 18 additions & 0 deletions includes/Html.php
Expand Up @@ -942,4 +942,22 @@ static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = tru

return $s;
}

/**
* Generate a srcset attribute value from an array mapping pixel densities
* to URLs. Note that srcset supports width and height values as well, which
* are not used here.
*
* @param array $urls
* @return string
*/
static function srcSet( $urls ) {
$candidates = array();
foreach( $urls as $density => $url ) {
// Image candidate syntax per current whatwg live spec, 2012-09-23:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-1.html#attr-img-srcset
$candidates[] = "{$url} {$density}x";
}
return implode( ", ", $candidates );
}
}
37 changes: 37 additions & 0 deletions includes/Linker.php
Expand Up @@ -676,6 +676,7 @@ public static function makeImageLink( /*Parser*/ $parser, Title $title, $file, $
if ( !$thumb ) {
$s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
} else {
self::processResponsiveImages( $file, $thumb, $hp );
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
Expand Down Expand Up @@ -796,6 +797,7 @@ public static function makeThumbLink2( Title $title, $file, $frameParams = array
$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
}
$thumb = false;
$noscale = false;

if ( !$exists ) {
$outerWidth = $hp['width'] + 2;
Expand All @@ -814,6 +816,7 @@ public static function makeThumbLink2( Title $title, $file, $frameParams = array
} elseif ( isset( $fp['framed'] ) ) {
// Use image dimensions, don't scale
$thumb = $file->getUnscaledThumb( $hp );
$noscale = true;
} else {
# Do not present an image bigger than the source, for bitmap-style images
# This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour
Expand Down Expand Up @@ -847,6 +850,9 @@ public static function makeThumbLink2( Title $title, $file, $frameParams = array
$s .= wfMessage( 'thumbnail_error', '' )->escaped();
$zoomIcon = '';
} else {
if ( !$noscale ) {
self::processResponsiveImages( $file, $thumb, $hp );
}
$params = array(
'alt' => $fp['alt'],
'title' => $fp['title'],
Expand All @@ -873,6 +879,37 @@ public static function makeThumbLink2( Title $title, $file, $frameParams = array
return str_replace( "\n", ' ', $s );
}

/**
* Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
* applicable.
*
* @param File $file
* @param MediaOutput $thumb
* @param array $hp image parameters
*/
protected static function processResponsiveImages( $file, $thumb, $hp ) {
global $wgResponsiveImages;
if ( $wgResponsiveImages ) {
$hp15 = $hp;
$hp15['width'] = round( $hp['width'] * 1.5 );
$hp20 = $hp;
$hp20['width'] = $hp['width'] * 2;
if ( isset( $hp['height'] ) ) {
$hp15['height'] = round( $hp['height'] * 1.5 );
$hp20['height'] = $hp['height'] * 2;
}

$thumb15 = $file->transform( $hp15 );
$thumb20 = $file->transform( $hp20 );
if ( $thumb15->url !== $thumb->url ) {
$thumb->responsiveUrls['1.5'] = $thumb15->url;
}
if ( $thumb20->url !== $thumb->url ) {
$thumb->responsiveUrls['2'] = $thumb20->url;
}
}
}

/**
* Make a "broken" link to an image
*
Expand Down
7 changes: 6 additions & 1 deletion includes/OutputPage.php
Expand Up @@ -2462,7 +2462,7 @@ public function headElement( Skin $sk, $includeStyle = true ) {
*/
private function addDefaultModules() {
global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
$wgAjaxWatch;
$wgAjaxWatch, $wgResponsiveImages;

// Add base resources
$this->addModules( array(
Expand Down Expand Up @@ -2503,6 +2503,11 @@ private function addDefaultModules() {
if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) {
$this->addModules( 'mediawiki.action.view.dblClickEdit' );
}

// Support for high-density display images
if ( $wgResponsiveImages ) {
$this->addModules( 'mediawiki.hidpi' );
}
}

/**
Expand Down
14 changes: 13 additions & 1 deletion includes/media/MediaTransformOutput.php
Expand Up @@ -33,6 +33,13 @@ abstract class MediaTransformOutput {
var $file;

var $width, $height, $url, $page, $path;

/**
* @var array Associative array mapping optional supplementary image files
* from pixel density (eg 1.5 or 2) to additional URLs.
*/
public $responsiveUrls = array();

protected $storagePath = false;

/**
Expand Down Expand Up @@ -324,14 +331,19 @@ function toHtml( $options = array() ) {
'alt' => $alt,
'src' => $this->url,
'width' => $this->width,
'height' => $this->height,
'height' => $this->height
);
if ( !empty( $options['valign'] ) ) {
$attribs['style'] = "vertical-align: {$options['valign']}";
}
if ( !empty( $options['img-class'] ) ) {
$attribs['class'] = $options['img-class'];
}

// Additional densities for responsive images, if specified.
if ( !empty( $this->responsiveUrls ) ) {
$attribs['srcset'] = Html::srcSet( $this->responsiveUrls );
}
return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) );
}

Expand Down
9 changes: 9 additions & 0 deletions resources/Resources.php
Expand Up @@ -178,6 +178,9 @@
'jquery.getAttrs' => array(
'scripts' => 'resources/jquery/jquery.getAttrs.js',
),
'jquery.hidpi' => array(
'scripts' => 'resources/jquery/jquery.hidpi.js',
),
'jquery.highlightText' => array(
'scripts' => 'resources/jquery/jquery.highlightText.js',
'dependencies' => 'jquery.mwExtension',
Expand Down Expand Up @@ -621,6 +624,12 @@
'feedback-bugnew',
),
),
'mediawiki.hidpi' => array(
'scripts' => 'resources/mediawiki/mediawiki.hidpi.js',
'dependencies' => array(
'jquery.hidpi',
),
),
'mediawiki.htmlform' => array(
'scripts' => 'resources/mediawiki/mediawiki.htmlform.js',
),
Expand Down
119 changes: 119 additions & 0 deletions resources/jquery/jquery.hidpi.js
@@ -0,0 +1,119 @@
/**
* Responsive images based on 'srcset' and 'window.devicePixelRatio' emulation where needed.
*
* Call $().hidpi() on a document or part of a document to replace image srcs in that section.
*
* $.devicePixelRatio() can be used to supplement window.devicePixelRatio with support on
* some additional browsers.
*/
( function ( $ ) {

/**
* Detect reported or approximate device pixel ratio.
* 1.0 means 1 CSS pixel is 1 hardware pixel
* 2.0 means 1 CSS pixel is 2 hardware pixels
* etc
*
* Uses window.devicePixelRatio if available, or CSS media queries on IE.
*
* @method
* @returns {number} Device pixel ratio
*/
$.devicePixelRatio = function () {
if ( window.devicePixelRatio !== undefined ) {
// Most web browsers:
// * WebKit (Safari, Chrome, Android browser, etc)
// * Opera
// * Firefox 18+
return window.devicePixelRatio;
} else if ( window.msMatchMedia !== undefined ) {
// Windows 8 desktops / tablets, probably Windows Phone 8
//
// IE 10 doesn't report pixel ratio directly, but we can get the
// screen DPI and divide by 96. We'll bracket to [1, 1.5, 2.0] for
// simplicity, but you may get different values depending on zoom
// factor, size of screen and orientation in Metro IE.
if ( window.msMatchMedia( '(min-resolution: 192dpi)' ).matches ) {
return 2;
} else if ( window.msMatchMedia( '(min-resolution: 144dpi)' ).matches ) {
return 1.5;
} else {
return 1;
}
} else {
// Legacy browsers...
// Assume 1 if unknown.
return 1;
}
};

/**
* Implement responsive images based on srcset attributes, if browser has no
* native srcset support.
*
* @method
* @returns {jQuery} This selection
*/
$.fn.hidpi = function () {
var $target = this,
// @todo add support for dpi media query checks on Firefox, IE
devicePixelRatio = $.devicePixelRatio(),
testImage = new Image();

if ( devicePixelRatio > 1 && testImage.srcset === undefined ) {
// No native srcset support.
$target.find( 'img' ).each( function () {
var $img = $( this ),
srcset = $img.attr( 'srcset' ),
match;
if ( typeof srcset === 'string' && srcset !== '' ) {
match = $.matchSrcSet( devicePixelRatio, srcset );
if (match !== null ) {
$img.attr( 'src', match );
}
}
});
}

return $target;
};

/**
* Match a srcset entry for the given device pixel ratio
*
* @param {number} devicePixelRatio
* @param {string} srcset
* @return {mixed} null or the matching src string
*
* Exposed for testing.
*/
$.matchSrcSet = function ( devicePixelRatio, srcset ) {
var candidates,
candidate,
bits,
src,
i,
ratioStr,
ratio,
selectedRatio = 1,
selectedSrc = null;
candidates = srcset.split( / *, */ );
for ( i = 0; i < candidates.length; i++ ) {
candidate = candidates[i];
bits = candidate.split( / +/ );
src = bits[0];
if ( bits.length > 1 && bits[1].charAt( bits[1].length - 1 ) === 'x' ) {
ratioStr = bits[1].substr( 0, bits[1].length - 1 );
ratio = parseFloat( ratioStr );
if ( ratio > devicePixelRatio ) {
// Too big, skip!
} else if ( ratio > selectedRatio ) {
selectedRatio = ratio;
selectedSrc = src;
}
}
}
return selectedSrc;
};

}( jQuery ) );
5 changes: 5 additions & 0 deletions resources/mediawiki/mediawiki.hidpi.js
@@ -0,0 +1,5 @@
$( function() {
// Apply hidpi images on DOM-ready
// Some may have already partly preloaded at low resolution.
$( 'body' ).hidpi();
} );
2 changes: 1 addition & 1 deletion tests/parser/parserTest.inc
Expand Up @@ -671,7 +671,7 @@ class ParserTest {
'wgNoFollowLinks' => true,
'wgNoFollowDomainExceptions' => array(),
'wgThumbnailScriptPath' => false,
'wgUseImageResize' => false,
'wgUseImageResize' => true,
'wgLocaltimezone' => 'UTC',
'wgAllowExternalImages' => true,
'wgUseTidy' => false,
Expand Down

0 comments on commit 966cda2

Please sign in to comment.