diff --git a/extensions/blocks/tiled-gallery/gallery-image/edit.js b/extensions/blocks/tiled-gallery/gallery-image/edit.js index 9147c3fce050d..8f343ded565fb 100644 --- a/extensions/blocks/tiled-gallery/gallery-image/edit.js +++ b/extensions/blocks/tiled-gallery/gallery-image/edit.js @@ -106,6 +106,7 @@ class GalleryImageEdit extends Component { onRemove, origUrl, // setAttributes, + srcSet, url, width, } = this.props; @@ -138,6 +139,7 @@ class GalleryImageEdit extends Component { onKeyDown={ this.onImageKeyDown } ref={ this.img } src={ url } + srcSet={ srcSet } tabIndex="0" /> { isBlobURL( origUrl ) && } diff --git a/extensions/blocks/tiled-gallery/layout/index.js b/extensions/blocks/tiled-gallery/layout/index.js index d796feaad340a..c0bf8546175a1 100644 --- a/extensions/blocks/tiled-gallery/layout/index.js +++ b/extensions/blocks/tiled-gallery/layout/index.js @@ -1,11 +1,8 @@ /** * External dependencies */ -import photon from 'photon'; import { __, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { format as formatUrl, parse as parseUrl } from 'url'; -import { isBlobURL } from '@wordpress/blob'; /** * Internal dependencies @@ -14,34 +11,9 @@ import GalleryImageEdit from '../gallery-image/edit'; import GalleryImageSave from '../gallery-image/save'; import Mosaic from './mosaic'; import Square from './square'; -import { PHOTON_MAX_RESIZE } from '../constants'; +import { isSquareishLayout, photonizedImgProps } from '../utils'; export default class Layout extends Component { - photonize( { height, width, url } ) { - if ( ! url ) { - return; - } - - // Do not Photonize images that are still uploading or from localhost - if ( isBlobURL( url ) || /^https?:\/\/localhost/.test( url ) ) { - return url; - } - - // Drop query args, photon URLs can't handle them - // This should be the "raw" url, we'll add dimensions later - const cleanUrl = url.split( '?', 1 )[ 0 ]; - - const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; - - const { layoutStyle } = this.props; - - if ( isSquareishLayout( layoutStyle ) && width && height ) { - const size = Math.min( PHOTON_MAX_RESIZE, width, height ); - return photonImplementation( cleanUrl, { resize: `${ size },${ size }` } ); - } - return photonImplementation( cleanUrl ); - } - // This is tricky: // - We need to "photonize" to resize the images at appropriate dimensions // - The resize will depend on the image size and the layout in some cases @@ -53,6 +25,7 @@ export default class Layout extends Component { images, isSave, linkTo, + layoutStyle, onRemoveImage, onSelectImage, selectedImage, @@ -67,6 +40,8 @@ export default class Layout extends Component { ); const Image = isSave ? GalleryImageSave : GalleryImageEdit; + const { src, srcSet } = photonizedImgProps( img, { layoutStyle } ); + return ( { ); @@ -111,52 +87,3 @@ export default class Layout extends Component { ); } } - -function isSquareishLayout( layout ) { - return [ 'circle', 'square' ].includes( layout ); -} - -function isWpcomFilesUrl( url ) { - const { host } = parseUrl( url ); - return /\.files\.wordpress\.com$/.test( host ); -} - -/** - * Apply photon arguments to *.files.wordpress.com images - * - * This function largely duplicates the functionlity of the photon.js lib. - * This is necessary because we want to serve images from *.files.wordpress.com so that private - * WordPress.com sites can use this block which depends on a Photon-like image service. - * - * If we pass all images through Photon servers, some images are unreachable. *.files.wordpress.com - * is already photon-like so we can pass it the same parameters for image resizing. - * - * @param {string} url Image url - * @param {Object} opts Options to pass to photon - * - * @return {string} Url string with options applied - */ -function photonWpcomImage( url, opts = {} ) { - // Adhere to the same options API as the photon.js lib - const photonLibMappings = { - width: 'w', - height: 'h', - letterboxing: 'lb', - removeLetterboxing: 'ulb', - }; - - // Discard some param parts - const { auth, hash, port, query, search, ...urlParts } = parseUrl( url ); - - // Build query - // This reduction intentionally mutates the query as it is built internally. - urlParts.query = Object.keys( opts ).reduce( - ( q, key ) => - Object.assign( q, { - [ photonLibMappings.hasOwnProperty( key ) ? photonLibMappings[ key ] : key ]: opts[ key ], - } ), - {} - ); - - return formatUrl( urlParts ); -} diff --git a/extensions/blocks/tiled-gallery/utils/index.js b/extensions/blocks/tiled-gallery/utils/index.js new file mode 100644 index 0000000000000..8f9950010145b --- /dev/null +++ b/extensions/blocks/tiled-gallery/utils/index.js @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import photon from 'photon'; +import { format as formatUrl, parse as parseUrl } from 'url'; +import { isBlobURL } from '@wordpress/blob'; +import { range } from 'lodash'; + +/** + * Internal dependencies + */ +import { PHOTON_MAX_RESIZE } from './constants'; + +export function isSquareishLayout( layout ) { + return [ 'circle', 'square' ].includes( layout ); +} + +/** + * Build src and srcSet properties which can be used on an + * + * @param {Object} img Image + * @param {number} img.height Image height + * @param {string} img.url Image URL + * @param {number} img.width Image width + * + * @param {Object} galleryAtts Gallery attributes relevant for image optimization. + * @param {string} galleryAtts.layoutStyle Gallery layout. 'rectangular', 'circle', etc. + * @param {number} galleryAtts.columns Gallery columns. Not applicable for all layouts. + * + * @return {Object} Returns an object. If possible, the object will include `src` and `srcSet` + * properties {string} for use on an image. + */ +export function photonizedImgProps( img, galleryAtts = {} ) { + if ( ! img.height || ! img.url || ! img.width ) { + return {}; + } + + // Do not Photonize images that are still uploading or from localhost + if ( isBlobURL( img.url ) || /^https?:\/\/localhost/.test( img.url ) ) { + return { src: img.url }; + } + + // Drop query args, photon URLs can't handle them + // This should be the "raw" url, we'll add dimensions later + const url = img.url.split( '?', 1 )[ 0 ]; + const { height, width } = img; + const { layoutStyle } = galleryAtts; + + const photonImplementation = isWpcomFilesUrl( url ) ? photonWpcomImage : photon; + + /** + * Build the `src` + * We don't know what the viewport size will be like. Use full size src. + */ + + let src; + if ( isSquareishLayout( layoutStyle ) && width && height ) { + // Layouts with 1:1 width/height ratio should be made square + const size = Math.min( PHOTON_MAX_RESIZE, width, height ); + src = photonImplementation( url, { + resize: `${ size },${ size }`, + strip: 'all', + } ); + } else { + src = photonImplementation( url, { strip: 'all' } ); + } + + /** + * Build a sensible `srcSet` that will let the browser get an optimized image based on + * viewport width + */ + + const step = 300; + let srcSet; + if ( isSquareishLayout( layoutStyle ) ) { + const minWidth = Math.min( 600, width, height ); + const maxWidth = Math.min( PHOTON_MAX_RESIZE, width, height ); + + srcSet = range( minWidth, maxWidth, step ) + .map( srcsetWidth => { + const srcsetSrc = photonImplementation( url, { + resize: `${ srcsetWidth },${ srcsetWidth }`, + strip: 'all', + } ); + return srcsetSrc ? `${ srcsetSrc } ${ srcsetWidth }w` : null; + } ) + .filter( Boolean ) + .join( ',' ); + } else { + const minWidth = Math.min( 600, width ); + const maxWidth = Math.min( PHOTON_MAX_RESIZE, width ); + + srcSet = range( minWidth, maxWidth, step ) + .map( srcsetWidth => { + const srcsetSrc = photonImplementation( url, { + strip: 'all', + width: srcsetWidth, + } ); + return srcsetSrc ? `${ srcsetSrc } ${ srcsetWidth }w` : null; + } ) + .filter( Boolean ) + .join( ',' ); + } + + return Object.assign( { src }, srcSet && { srcSet } ); +} + +function isWpcomFilesUrl( url ) { + const { host } = parseUrl( url ); + return /\.files\.wordpress\.com$/.test( host ); +} + +/** + * Apply photon arguments to *.files.wordpress.com images + * + * This function largely duplicates the functionlity of the photon.js lib. + * This is necessary because we want to serve images from *.files.wordpress.com so that private + * WordPress.com sites can use this block which depends on a Photon-like image service. + * + * If we pass all images through Photon servers, some images are unreachable. *.files.wordpress.com + * is already photon-like so we can pass it the same parameters for image resizing. + * + * @param {string} url Image url + * @param {Object} opts Options to pass to photon + * + * @return {string} Url string with options applied + */ +function photonWpcomImage( url, opts = {} ) { + // Adhere to the same options API as the photon.js lib + const photonLibMappings = { + width: 'w', + height: 'h', + letterboxing: 'lb', + removeLetterboxing: 'ulb', + }; + + // Discard some param parts + const { auth, hash, port, query, search, ...urlParts } = parseUrl( url ); + + // Build query + // This reduction intentionally mutates the query as it is built internally. + urlParts.query = Object.keys( opts ).reduce( + ( q, key ) => + Object.assign( q, { + [ photonLibMappings.hasOwnProperty( key ) ? photonLibMappings[ key ] : key ]: opts[ key ], + } ), + {} + ); + + return formatUrl( urlParts ); +}