Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4111931
LazyImageTransform.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
LazyImageTransform.php
View Options
<?php
namespace
MobileFrontend\Transforms
;
use
DOMDocument
;
use
DOMElement
;
use
MobileFrontend\Transforms\Utils\HtmlClassUtils
;
use
MobileFrontend\Transforms\Utils\HtmlStyleUtils
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
class
LazyImageTransform
implements
IMobileTransform
{
/**
* Do not lazy load images smaller than this size (in pixels)
*/
private
const
SMALL_IMAGE_DIMENSION_THRESHOLD_IN_PX
=
50
;
/**
* Do not lazy load images smaller than this size (in relative to x-height of the current font)
*/
private
const
SMALL_IMAGE_DIMENSION_THRESHOLD_IN_EX
=
10
;
/**
* Whether to skip the loading of small images
* @var bool
*/
protected
$skipSmall
;
/**
* @param bool $skipSmallImages whether small images should be excluded from lazy loading
*/
public
function
__construct
(
$skipSmallImages
=
false
)
{
$this
->
skipSmall
=
$skipSmallImages
;
}
/**
* Insert a table of content placeholder into the element
* which will be progressively enhanced via JS
*
* @param DOMElement $node to be transformed
*/
public
function
apply
(
DOMElement
$node
)
{
// Parsoid can have nested sections, so ideally we'd use a child combinator
// here to avoid multiple applications of the lazy image transform to the
// nested sections. For example, `.mw-parser-output > section`
$query
=
'section'
;
// Until section collapsing in MFE is made to work with Parsoid output,
// all of Parsoid's sections are nested in the first mf section.
$parserOutput
=
DOMCompat
::
querySelector
(
$node
,
'.mw-parser-output'
);
$isParsoid
=
$parserOutput
&&
$parserOutput
->
hasAttribute
(
'data-mw-parsoid-version'
);
if
(
$isParsoid
)
{
$query
=
'#mf-section-0 > section'
;
}
$sections
=
DOMCompat
::
querySelectorAll
(
$node
,
$query
);
$sectionNumber
=
0
;
foreach
(
$sections
as
$sectionNumber
=>
$section
)
{
if
(
$sectionNumber
>
0
)
{
$this
->
doRewriteImagesForLazyLoading
(
$section
,
$section
->
ownerDocument
);
}
$sectionNumber
++;
}
}
/**
* @see MobileFormatter#getImageDimensions
*
* @param DOMElement $img
* @param string $dimension Either "width" or "height"
* @return string|null
*/
private
function
getImageDimension
(
DOMElement
$img
,
$dimension
)
{
$style
=
$img
->
getAttribute
(
'style'
);
$numMatches
=
preg_match
(
"/.*?{$dimension} *
\:
*([^;]*)/"
,
$style
,
$matches
);
if
(
!
$numMatches
&&
!
$img
->
hasAttribute
(
$dimension
)
)
{
return
null
;
}
return
$numMatches
?
trim
(
$matches
[
1
]
)
:
$img
->
getAttribute
(
$dimension
)
.
'px'
;
}
/**
* Determine the user perceived width and height of an image element based on `style`, `width`,
* and `height` attributes.
*
* As in the browser, the `style` attribute takes precedence over the `width` and `height`
* attributes. If the image has no `style`, `width` or `height` attributes, then the image is
* dimensionless.
*
* @param DOMElement $img <img> element
* @return array with width and height parameters if dimensions are found
*/
public
function
getImageDimensions
(
DOMElement
$img
)
{
$result
=
[];
foreach
(
[
'width'
,
'height'
]
as
$dimensionName
)
{
$dimension
=
$this
->
getImageDimension
(
$img
,
$dimensionName
);
if
(
$dimension
)
{
$result
[
$dimensionName
]
=
$dimension
;
}
}
return
$result
;
}
/**
* Is image dimension small enough to not lazy load it
*
* @param string $dimension in css format, supports only px|ex units
* @return bool
*/
public
function
isDimensionSmallerThanThreshold
(
$dimension
)
{
$matches
=
null
;
if
(
preg_match
(
'/(
\d
+)(
\.\d
+)?(px|ex)/'
,
$dimension
,
$matches
)
===
0
)
{
return
false
;
}
$size
=
$matches
[
1
];
$unit
=
array_pop
(
$matches
);
switch
(
strtolower
(
$unit
)
)
{
case
'px'
:
return
$size
<=
self
::
SMALL_IMAGE_DIMENSION_THRESHOLD_IN_PX
;
case
'ex'
:
return
$size
<=
self
::
SMALL_IMAGE_DIMENSION_THRESHOLD_IN_EX
;
default
:
return
false
;
}
}
/**
* @param array $dimensions
* @return bool
*/
private
function
skipLazyLoadingForSmallDimensions
(
array
$dimensions
)
{
if
(
array_key_exists
(
'width'
,
$dimensions
)
&&
$this
->
isDimensionSmallerThanThreshold
(
$dimensions
[
'width'
]
)
)
{
return
true
;
}
if
(
array_key_exists
(
'height'
,
$dimensions
)
&&
$this
->
isDimensionSmallerThanThreshold
(
$dimensions
[
'height'
]
)
)
{
return
true
;
}
return
false
;
}
/**
* Enables images to be loaded asynchronously
*
* @param DOMElement|DOMDocument $el Element or document to rewrite images in.
* @param ?DOMDocument $doc Document to create elements in
*/
private
function
doRewriteImagesForLazyLoading
(
$el
,
?
DOMDocument
$doc
)
{
if
(
$doc
===
null
)
{
return
;
}
$lazyLoadSkipSmallImages
=
$this
->
skipSmall
;
foreach
(
DOMCompat
::
querySelectorAll
(
$el
,
'img'
)
as
$img
)
{
$parent
=
$img
->
parentNode
;
$dimensions
=
$this
->
getImageDimensions
(
$img
);
$hasCompleteDimensions
=
isset
(
$dimensions
[
'width'
]
)
&&
isset
(
$dimensions
[
'height'
]
);
if
(
$lazyLoadSkipSmallImages
&&
$this
->
skipLazyLoadingForSmallDimensions
(
$dimensions
)
)
{
continue
;
}
// T133085 - don't transform if we have no idea about dimensions of image
if
(
!
$hasCompleteDimensions
)
{
continue
;
}
// HTML only clients
$noscript
=
$doc
->
createElement
(
'noscript'
);
// To be loaded image placeholder
$imgPlaceholder
=
$doc
->
createElement
(
'span'
);
$this
->
copyClasses
(
$img
,
$imgPlaceholder
,
[
// T199351
'thumbborder'
],
[
'lazy-image-placeholder'
,
]
);
$this
->
copyStyles
(
$img
,
$imgPlaceholder
,
[
// T207929
'vertical-align'
],
[
'width'
=>
$dimensions
[
'width'
],
'height'
=>
$dimensions
[
'height'
],
]
);
foreach
(
[
'src'
,
'alt'
,
'width'
,
'height'
,
'srcset'
,
'class'
,
'usemap'
]
as
$attr
)
{
if
(
$img
->
hasAttribute
(
$attr
)
)
{
$prefix
=
(
$attr
===
'src'
||
$attr
===
'srcset'
)
?
'data-mw-'
:
'data-'
;
$imgPlaceholder
->
setAttribute
(
$prefix
.
$attr
,
$img
->
getAttribute
(
$attr
)
);
}
}
// Assume data saving and remove srcset attribute from the non-js experience
$img
->
removeAttribute
(
'srcset'
);
// T145222: Add a non-breaking space inside placeholders to ensure that they do not report
// themselves as invisible when inline.
$imgPlaceholder
->
appendChild
(
$doc
->
createEntityReference
(
'nbsp'
)
);
// Set the placeholder where the original image was
$parent
->
replaceChild
(
$imgPlaceholder
,
$img
);
// Add the original image to the HTML only markup
$noscript
->
appendChild
(
$img
);
// Insert the HTML only markup before the placeholder
$parent
->
insertBefore
(
$noscript
,
$imgPlaceholder
);
}
}
/**
* Copy allowed styles from one HTMLElement to another, filtering them and
* unconditionally adding several in front of list
*
* @param DOMElement|DOMDocument $from html element styles to be copied from
* @param DOMElement|DOMDocument $to html element styles to be copied to
* @param string[] $enabled list of enabled styles to be copied
* @param array $additional key-value array of styles to be added/overriden unconditionally
* at the beginning of string
*
*/
private
function
copyStyles
(
$from
,
$to
,
array
$enabled
,
array
$additional
)
{
$styles
=
HtmlStyleUtils
::
parseStyleString
(
$from
->
hasAttribute
(
'style'
)
?
$from
->
getAttribute
(
'style'
)
:
''
);
$filteredStyles
=
HtmlStyleUtils
::
filterAllowedStyles
(
$styles
,
$enabled
,
$additional
);
$to
->
setAttribute
(
'style'
,
HtmlStyleUtils
::
formStyleString
(
$filteredStyles
)
);
}
/**
* Copy allowed classes from one HTMLElement to another
* @param DOMElement|DOMDocument $from html element classes to be copied from
* @param DOMElement|DOMDocument $to html element classes to be copied to
* @param string[] $enabled array of enabled classes to be copied
* @param string[] $additional array of classes to be added/overriden unconditionally
* to the beginning
*/
private
function
copyClasses
(
$from
,
$to
,
array
$enabled
,
array
$additional
)
{
$styles
=
HtmlClassUtils
::
parseClassString
(
$from
->
hasAttribute
(
'class'
)
?
$from
->
getAttribute
(
'class'
)
:
''
);
$filteredClasses
=
HtmlClassUtils
::
filterAllowedClasses
(
$styles
,
$enabled
,
$additional
);
$to
->
setAttribute
(
'class'
,
HtmlClassUtils
::
formClassString
(
$filteredClasses
)
);
}
/**
* Fallback for Grade C to load lazy-load image placeholders.
*
* Note: This will add a single repaint for Grade C browsers as
* images enter view but this is intentional and deemed acceptable.
*
* @return string The JavaScript code to load lazy placeholders in Grade C browsers
*/
public
static
function
gradeCImageSupport
()
{
// Notes:
// * Document#getElementsByClassName is supported by IE9+ and #querySelectorAll is
// supported by IE8+. To gain the widest possible browser support we scan for
// noscript tags using #getElementsByTagName and look at the next sibling.
// If the next sibling has the lazy-image-placeholder class then it will be assumed
// to be a placeholder and replace with an img tag.
// * Iterating over the live NodeList from getElementsByTagName() is suboptimal
// but in IE < 9, Array#slice() throws when given a NodeList. It also requires
// the 2nd argument ('end').
$js
=
<<<JAVASCRIPT
(window.NORLQ = window.NORLQ || []).push( function () {
var ns, i, p, img;
ns = document.getElementsByTagName( 'noscript' );
for ( i = 0; i < ns.length; i++ ) {
p = ns[i].nextSibling;
if ( p && p.className && p.className.indexOf( 'lazy-image-placeholder' ) > -1 ) {
img = document.createElement( 'img' );
img.setAttribute( 'src', p.getAttribute( 'data-mw-src' ) );
img.setAttribute( 'width', p.getAttribute( 'data-width' ) );
img.setAttribute( 'height', p.getAttribute( 'data-height' ) );
img.setAttribute( 'alt', p.getAttribute( 'data-alt' ) );
p.parentNode.replaceChild( img, p );
}
}
} );
JAVASCRIPT;
return
$js
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Tue, Aug 18, 22:36 (2 h, 36 m ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
6c/e9/65d864ba04e3d2850d732046dceb
Default Alt Text
LazyImageTransform.php (9 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment