| /** |
| * @module thumbnail |
| * @private |
| */ |
| |
| import constants from '../constants'; |
| |
| export const SIZES = { |
| portraitImage: { |
| h: 250, // Exact height |
| w: 203 // Max width |
| }, |
| landscapeImage: { |
| h: 200, // Max height |
| w: 320 // Exact Width |
| } |
| }; |
| |
| /** |
| * @typedef {Object} ext.popups.Thumbnail |
| * @property {jQuery} el |
| * @property {boolean} isTall Whether or not the thumbnail is portrait |
| * @property {number} width |
| * @property {number} height |
| * @property {boolean} isNarrow whether the thumbnail is portrait and also |
| * thinner than the default portrait thumbnail width |
| * (as defined in SIZES.portraitImage.w) |
| * @property {number} offset in pixels between the thumbnail width and the |
| * standard portrait thumbnail width (as defined in SIZES.portraitImage.w) |
| */ |
| |
| /** |
| * Creates a thumbnail from the representation of a thumbnail returned by the |
| * PageImages MediaWiki API query module. |
| * |
| * If there's no thumbnail, the thumbnail is too small, or the thumbnail's URL |
| * contains characters that could be used to perform an |
| * [XSS attack via CSS](https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)), |
| * then `null` is returned. |
| * |
| * Extracted from `mw.popups.renderer.article.createThumbnail`. |
| * |
| * @param {Object} rawThumbnail |
| * @param {boolean} useCSSClipPath |
| * @return {ext.popups.Thumbnail|null} |
| */ |
| export function createThumbnail( rawThumbnail, useCSSClipPath ) { |
| const devicePixelRatio = constants.BRACKETED_DEVICE_PIXEL_RATIO; |
| |
| if ( !rawThumbnail ) { |
| return null; |
| } |
| |
| const thumbWidth = rawThumbnail.width / devicePixelRatio; |
| const thumbHeight = rawThumbnail.height / devicePixelRatio; |
| // For images less than 320 wide, try to display a 250 high vertical slice instead |
| const tall = rawThumbnail.height > rawThumbnail.width || thumbWidth < SIZES.landscapeImage.w; |
| |
| if ( |
| // Image too small for portrait display |
| ( tall && thumbHeight < SIZES.portraitImage.h && |
| rawThumbnail.height < SIZES.portraitImage.h ) || |
| // These characters in URL that could inject CSS and thus JS |
| ( |
| rawThumbnail.source.indexOf( '\\' ) > -1 || |
| rawThumbnail.source.indexOf( '\'' ) > -1 || |
| rawThumbnail.source.indexOf( '"' ) > -1 |
| ) |
| ) { |
| return null; |
| } |
| |
| const aspectRatio = thumbWidth / thumbHeight; |
| const isSquare = aspectRatio > 0.7 && aspectRatio < 1.3; |
| |
| let x, y, width, height; |
| if ( tall ) { |
| x = ( thumbWidth > SIZES.portraitImage.w ) ? |
| ( ( thumbWidth - SIZES.portraitImage.w ) / -2 ) : |
| ( SIZES.portraitImage.w - thumbWidth ); |
| y = ( thumbHeight > SIZES.portraitImage.h ) ? |
| ( ( thumbHeight - SIZES.portraitImage.h ) / -2 ) : 0; |
| width = SIZES.portraitImage.w; |
| height = SIZES.portraitImage.h; |
| |
| // Special handling for thin tall images |
| // https://phabricator.wikimedia.org/T192928#4312088 |
| if ( thumbWidth < width ) { |
| x = 0; |
| width = thumbWidth; |
| } |
| } else { |
| x = 0; |
| y = ( thumbHeight > SIZES.landscapeImage.h ) ? |
| ( ( thumbHeight - SIZES.landscapeImage.h ) / -2 ) : 0; |
| width = SIZES.landscapeImage.w; |
| height = ( thumbHeight > SIZES.landscapeImage.h ) ? |
| SIZES.landscapeImage.h : thumbHeight; |
| } |
| |
| const isNarrow = tall && thumbWidth < SIZES.portraitImage.w; |
| const el = useCSSClipPath ? createThumbnailImg( rawThumbnail.source ) : createThumbnailSVG( |
| tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall', |
| rawThumbnail.source, |
| x, |
| y, |
| thumbWidth, |
| thumbHeight, |
| width, |
| height |
| ); |
| |
| return { |
| el, |
| isTall: tall || isSquare, |
| isNarrow, |
| offset: isNarrow ? SIZES.portraitImage.w - thumbWidth : 0, |
| width: thumbWidth, |
| height: thumbHeight |
| }; |
| } |
| |
| function createThumbnailImg( url ) { |
| const img = document.createElement( 'img' ); |
| img.className = 'mwe-popups-thumbnail'; |
| img.src = url; |
| return img; |
| } |
| |
| /** |
| * Sets multiple attributes on a node. |
| * |
| * @param {HTMLElement} node |
| * @param {Record<string, string>} attrs |
| */ |
| const addAttributes = ( node, attrs ) => { |
| Object.keys( attrs ).forEach( ( key ) => { |
| node.setAttribute( key, attrs[ key ] ); |
| } ); |
| }; |
| |
| /** |
| * Creates the SVG image element that represents the thumbnail. |
| * |
| * This function is distinct from `createThumbnail` as it abstracts away some |
| * browser issues that are uncovered when manipulating elements across |
| * namespaces. |
| * |
| * @param {string} className |
| * @param {string} url |
| * @param {number} x |
| * @param {number} y |
| * @param {number} thumbnailWidth |
| * @param {number} thumbnailHeight |
| * @param {number} width |
| * @param {number} height |
| * @return {HTMLElement} |
| */ |
| |
| export function createThumbnailSVG( |
| className, url, x, y, thumbnailWidth, thumbnailHeight, width, height |
| ) { |
| const nsSvg = 'http://www.w3.org/2000/svg', |
| nsXlink = 'http://www.w3.org/1999/xlink'; |
| |
| // We want to visually separate the image from the summary |
| // Given we use an SVG mask, we cannot rely on border to do this |
| // and instead must insert a polyline element to visually separate |
| const line = document.createElementNS( nsSvg, 'polyline' ); |
| const isTall = className.indexOf( 'not-tall' ) === -1; |
| const points = isTall ? [ 0, 0, 0, height ] : |
| [ 0, height - 1, width, height - 1 ]; |
| |
| line.setAttribute( 'stroke', 'rgba(0,0,0,0.1)' ); |
| line.setAttribute( 'points', points.join( ' ' ) ); |
| line.setAttribute( 'stroke-width', 1 ); |
| |
| const thumbnailSVGImage = document.createElementNS( nsSvg, 'image' ); |
| thumbnailSVGImage.setAttributeNS( nsXlink, 'href', url ); |
| // The following classes are used here: |
| // * mwe-popups-is-not-tall |
| // * mwe-popups-is-tall |
| thumbnailSVGImage.classList.add( className ); |
| addAttributes( |
| thumbnailSVGImage, |
| { |
| x, |
| y, |
| width: thumbnailWidth, |
| height: thumbnailHeight |
| } |
| ); |
| |
| const thumbnail = document.createElementNS( nsSvg, 'svg' ); |
| addAttributes( |
| thumbnail, { |
| xmlns: nsSvg, |
| width, |
| height |
| } |
| ); |
| thumbnail.appendChild( thumbnailSVGImage ); |
| thumbnail.appendChild( line ); |
| return thumbnail; |
| } |