initial
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Image_CDN;
|
||||
|
||||
use Automattic\Jetpack\Image_CDN\Image_CDN_Setup;
|
||||
use Automattic\Jetpack_Boost\Contracts\Changes_Output_On_Activation;
|
||||
use Automattic\Jetpack_Boost\Contracts\Feature;
|
||||
use Automattic\Jetpack_Boost\Contracts\Needs_Website_To_Be_Public;
|
||||
use Automattic\Jetpack_Boost\Contracts\Optimization;
|
||||
|
||||
class Image_CDN implements Feature, Changes_Output_On_Activation, Needs_Website_To_Be_Public, Optimization {
|
||||
|
||||
public function setup() {
|
||||
Image_CDN_Setup::load();
|
||||
}
|
||||
|
||||
public static function get_slug() {
|
||||
return 'image_cdn';
|
||||
}
|
||||
|
||||
public static function is_available() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Image_CDN;
|
||||
|
||||
use Automattic\Jetpack_Boost\Contracts\Changes_Output_On_Activation;
|
||||
use Automattic\Jetpack_Boost\Contracts\Sub_Feature;
|
||||
use Automattic\Jetpack_Boost\Lib\Premium_Features;
|
||||
|
||||
class Liar implements Sub_Feature, Changes_Output_On_Activation {
|
||||
|
||||
public function setup() {
|
||||
add_action( 'wp_footer', array( $this, 'inject_image_cdn_liar_script' ) );
|
||||
}
|
||||
|
||||
public static function get_slug() {
|
||||
return 'image_cdn_liar';
|
||||
}
|
||||
|
||||
public static function is_available() {
|
||||
return Premium_Features::has_feature( Premium_Features::IMAGE_CDN_LIAR );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the image-cdn-liar.js script as an inline script in the footer.
|
||||
*/
|
||||
public function inject_image_cdn_liar_script() {
|
||||
$file = __DIR__ . '/dist/inline-liar.js';
|
||||
if ( file_exists( $file ) ) {
|
||||
// Include the JavaScript directly inline.
|
||||
// phpcs:ignore
|
||||
$data = file_get_contents( $file );
|
||||
// There's no meaningful way to escape JavaScript in this context.
|
||||
// phpcs:ignore
|
||||
echo wp_get_inline_script_tag( $data, array( 'async' => true ) );
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_parent_features(): array {
|
||||
return array(
|
||||
Image_CDN::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\Jetpack_Boost\Modules\Optimizations\Image_CDN;
|
||||
|
||||
use Automattic\Jetpack\Schema\Schema;
|
||||
use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Contracts\Changes_Output_After_Activation;
|
||||
use Automattic\Jetpack_Boost\Contracts\Has_Data_Sync;
|
||||
use Automattic\Jetpack_Boost\Contracts\Is_Always_On;
|
||||
use Automattic\Jetpack_Boost\Contracts\Sub_Feature;
|
||||
use Automattic\Jetpack_Boost\Lib\Premium_Features;
|
||||
|
||||
class Quality_Settings implements Sub_Feature, Is_Always_On, Has_Data_Sync, Changes_Output_After_Activation {
|
||||
|
||||
public function setup() {
|
||||
add_filter( 'jetpack_photon_pre_args', array( $this, 'add_quality_args' ), 10, 2 );
|
||||
}
|
||||
|
||||
public static function get_slug() {
|
||||
return 'image_cdn_quality';
|
||||
}
|
||||
|
||||
public static function is_available() {
|
||||
return Premium_Features::has_feature( Premium_Features::IMAGE_CDN_QUALITY );
|
||||
}
|
||||
|
||||
public function register_data_sync( Data_Sync $instance ) {
|
||||
$image_cdn_quality_schema = Schema::as_assoc_array(
|
||||
array(
|
||||
'jpg' => Schema::as_assoc_array(
|
||||
array(
|
||||
'quality' => Schema::as_number(),
|
||||
'lossless' => Schema::as_boolean(),
|
||||
)
|
||||
),
|
||||
'png' => Schema::as_assoc_array(
|
||||
array(
|
||||
'quality' => Schema::as_number(),
|
||||
'lossless' => Schema::as_boolean(),
|
||||
)
|
||||
),
|
||||
'webp' => Schema::as_assoc_array(
|
||||
array(
|
||||
'quality' => Schema::as_number(),
|
||||
'lossless' => Schema::as_boolean(),
|
||||
)
|
||||
),
|
||||
)
|
||||
)->fallback(
|
||||
array(
|
||||
'jpg' => array(
|
||||
'quality' => 89,
|
||||
'lossless' => false,
|
||||
),
|
||||
'png' => array(
|
||||
'quality' => 80,
|
||||
'lossless' => false,
|
||||
),
|
||||
'webp' => array(
|
||||
'quality' => 80,
|
||||
'lossless' => false,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$instance->register( 'image_cdn_quality', $image_cdn_quality_schema );
|
||||
}
|
||||
|
||||
public static function get_change_output_action_names() {
|
||||
return array( 'update_option_' . JETPACK_BOOST_DATASYNC_NAMESPACE . '_image_cdn_quality' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add quality arg to existing photon args.
|
||||
*
|
||||
* @param array $args - Existing photon args.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function add_quality_args( $args, $image_url ) {
|
||||
$quality = $this->get_quality_for_image( $image_url );
|
||||
|
||||
if ( $quality !== null ) {
|
||||
$args['quality'] = $quality;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quality for an image based on the extension.
|
||||
*/
|
||||
private function get_quality_for_image( $image_url ) {
|
||||
static $quality_cache = array();
|
||||
|
||||
// Extract the file extension from the URL
|
||||
$file_extension = strtolower( pathinfo( $image_url, PATHINFO_EXTENSION ) );
|
||||
|
||||
static $extension_to_type = array(
|
||||
'jpg' => 'jpg',
|
||||
'jpeg' => 'jpg',
|
||||
'webp' => 'webp',
|
||||
'png' => 'png',
|
||||
);
|
||||
|
||||
if ( ! isset( $extension_to_type[ $file_extension ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $extension_to_type[ $file_extension ];
|
||||
|
||||
if ( ! isset( $quality_cache[ $type ] ) ) {
|
||||
$quality_cache[ $type ] = $this->get_quality_for_type( $type );
|
||||
}
|
||||
|
||||
return $quality_cache[ $type ];
|
||||
}
|
||||
|
||||
private function get_quality_for_type( $image_type ) {
|
||||
$quality_settings = jetpack_boost_ds_get( 'image_cdn_quality' );
|
||||
|
||||
if ( ! isset( $quality_settings[ $image_type ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Passing 100 to photon will result in a lossless image
|
||||
return $quality_settings[ $image_type ]['lossless'] ? 100 : $quality_settings[ $image_type ]['quality'];
|
||||
}
|
||||
|
||||
public static function get_parent_features(): array {
|
||||
return array(
|
||||
Image_CDN::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('wp-polyfill'), 'version' => '31700564e390fbbc6384');
|
||||
+1
@@ -0,0 +1 @@
|
||||
(()=>{"use strict";var t={164(t,e,r){function n(){return window.devicePixelRatio||1}function i(t){const e=new URL(t).searchParams.get("resize");return e?function(t){const[e,r]=t.split(",").map(Number);return isNaN(e)||isNaN(r)?null:{width:e,height:r}}(e):null}function s(t,e){if(e<=0)return!1;const r=e-t;if(r<0)return!1;if(r<50)return!0;const n=t/e;return n>.9&&n<=1}function o(t){if(!(t.getAttribute("width")&&t.getAttribute("height")&&t.srcset&&t.src&&t.src.includes(".wp.com")))return;const e=function(t){const e=n(),r=t.width/t.height,i=10*Math.ceil(t.width*e/10);return{width:i,height:Math.ceil(i/r)}}(t.getBoundingClientRect()),r=t.srcset.split(","),o=function(t,e){for(const r of t){const[t,n]=r.trim().split(" ");if(!n?.trim().endsWith("w"))continue;const o=i(t);if(o&&s(e,o.width))return{url:new URL(t),...o}}}([`${t.src} 0w`,...r],e.width);if(o)o.url.searchParams.set("_jb","closest"),r.push(`${o.url} ${window.innerWidth*n()}w`),t.srcset=r.join(","),t.sizes="auto";else{const i=function(t,e){const r=new URL(t);return r.searchParams.set("resize",`${e.width},${e.height}`),r}(t.src,e);i.searchParams.set("_jb","custom"),r.push(`${i} ${window.innerWidth*n()}w`),t.srcset=r.join(","),t.sizes="auto"}}r.d(e,{Er:()=>o})}},e={};function r(n){var i=e[n];if(void 0!==i)return i.exports;var s=e[n]={exports:{}};return t[n](s,s.exports,r),s.exports}r.d=(t,e)=>{for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var n=r(164);document.querySelectorAll("img[loading=lazy]").forEach(n.Er)})();
|
||||
@@ -0,0 +1,6 @@
|
||||
import { dynamicSrcset } from './srcset';
|
||||
|
||||
( function () {
|
||||
const lazyImages = document.querySelectorAll< HTMLImageElement >( 'img[loading=lazy]' );
|
||||
lazyImages.forEach( dynamicSrcset );
|
||||
} )();
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import {
|
||||
calculateTargetSize,
|
||||
dynamicSrcset,
|
||||
findClosestImageSize,
|
||||
getImageSizeFromUrl,
|
||||
isSizeReusable,
|
||||
parseImageSize,
|
||||
} from './srcset';
|
||||
|
||||
function createImageSize( resize: string, mq: string ) {
|
||||
const url = new URL( 'https://i0.wp.com/example.com/image.jpg' );
|
||||
const [ width, height ] = resize.split( ',' ).map( item => parseInt( item, 10 ) );
|
||||
const size = calculateTargetSize( { width, height } );
|
||||
url.searchParams.set( 'resize', `${ size.width },${ size.height }` );
|
||||
return `${ url } ${ mq }`;
|
||||
}
|
||||
|
||||
function setBoundingRect( img: HTMLImageElement, width: number, height: number ) {
|
||||
Object.defineProperty( img, 'getBoundingClientRect', {
|
||||
value: () => ( {
|
||||
width,
|
||||
height,
|
||||
top: 0,
|
||||
right: width,
|
||||
bottom: height,
|
||||
left: 0,
|
||||
} ),
|
||||
writable: true,
|
||||
} );
|
||||
}
|
||||
|
||||
describe( 'parse image resize param', () => {
|
||||
it( 'should parse valid resize param', () => {
|
||||
expect( parseImageSize( '100,200' ) ).toEqual( { width: 100, height: 200 } );
|
||||
} );
|
||||
|
||||
it( 'should return null for invalid resize param', () => {
|
||||
expect( parseImageSize( 'invalid' ) ).toBeNull();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getImageSizeFromUrl', () => {
|
||||
it( 'should extract image size from URL', () => {
|
||||
const url = 'https://i0.wp.com/example.com/image.jpg?resize=100,200';
|
||||
expect( getImageSizeFromUrl( url ) ).toEqual( { width: 100, height: 200 } );
|
||||
} );
|
||||
|
||||
it( 'should return null if resize param is missing', () => {
|
||||
const url = 'https://i0.wp.com/example.com/image.jpg';
|
||||
expect( getImageSizeFromUrl( url ) ).toBeNull();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'calculateTargetSize', () => {
|
||||
it( 'should calculate target size based on bounding rect and dpr', () => {
|
||||
const rect: DOMRect = {
|
||||
width: 500,
|
||||
height: 250,
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
toJSON: () => ( {} ),
|
||||
};
|
||||
window.devicePixelRatio = 2;
|
||||
expect( calculateTargetSize( rect ) ).toEqual( { width: 1000, height: 500 } );
|
||||
} );
|
||||
|
||||
it( 'should round up the target size to the nearest 10', () => {
|
||||
const ratio = 600 / 200;
|
||||
const rect: DOMRect = {
|
||||
width: 600 - 9,
|
||||
height: ( 600 - 9 ) / ratio, // = 197px
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
toJSON: () => ( {} ),
|
||||
};
|
||||
window.devicePixelRatio = 1;
|
||||
expect( calculateTargetSize( rect ) ).toEqual( {
|
||||
width: 600,
|
||||
height: Math.ceil( 600 / ratio ),
|
||||
} );
|
||||
|
||||
window.devicePixelRatio = 2;
|
||||
expect( calculateTargetSize( rect ) ).toEqual( {
|
||||
width: 1190,
|
||||
height: Math.ceil( 1190 / ratio ),
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isSizeReusable', () => {
|
||||
it( 'should return true if the target width is close to the width', () => {
|
||||
expect( isSizeReusable( 100, 100 ) ).toBe( true );
|
||||
expect( isSizeReusable( 90, 100 ) ).toBe( true );
|
||||
expect( isSizeReusable( 51, 100 ) ).toBe( true );
|
||||
expect( isSizeReusable( 901, 1000 ) ).toBe( true );
|
||||
expect( isSizeReusable( 50, 100 ) ).toBe( false );
|
||||
expect( isSizeReusable( 101, 100 ) ).toBe( false );
|
||||
expect( isSizeReusable( 150, 100 ) ).toBe( false );
|
||||
expect( isSizeReusable( 900, 1000 ) ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'findClosestImageSize', () => {
|
||||
beforeEach( () => {
|
||||
window.devicePixelRatio = 1;
|
||||
} );
|
||||
|
||||
const srcset = [
|
||||
createImageSize( '100,50', '200w' ),
|
||||
createImageSize( '400,250', '800w' ),
|
||||
createImageSize( '1400,700', '2400w' ),
|
||||
];
|
||||
|
||||
it( 'should return undefined if the urls are invalid', () => {
|
||||
expect( findClosestImageSize( [ 'foo.com', 'bar.com' ], 500 ) ).toBeUndefined();
|
||||
} );
|
||||
|
||||
it( "should find the closest image that's larger than the target width", () => {
|
||||
expect( findClosestImageSize( srcset, 51 ) ).toEqual( {
|
||||
url: new URL( srcset[ 0 ].split( ' ' )[ 0 ] ),
|
||||
width: 100,
|
||||
height: 50,
|
||||
} );
|
||||
|
||||
expect( findClosestImageSize( srcset, 1300 ) ).toEqual( {
|
||||
url: new URL( srcset[ 2 ].split( ' ' )[ 0 ] ),
|
||||
width: 1400,
|
||||
height: 700,
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should find the closest image even if the srcset is unordered', () => {
|
||||
const unorderedSrcset = [
|
||||
createImageSize( '1400,700', '1400w' ),
|
||||
createImageSize( '100,50', '100w' ),
|
||||
createImageSize( '400,250', '400w' ),
|
||||
];
|
||||
expect( findClosestImageSize( unorderedSrcset, 1300 ) ).toEqual( {
|
||||
url: new URL( unorderedSrcset[ 0 ].split( ' ' )[ 0 ] ),
|
||||
width: 1400,
|
||||
height: 700,
|
||||
} );
|
||||
|
||||
expect( findClosestImageSize( unorderedSrcset, 51 ) ).toEqual( {
|
||||
url: new URL( unorderedSrcset[ 1 ].split( ' ' )[ 0 ] ),
|
||||
width: 100,
|
||||
height: 50,
|
||||
} );
|
||||
|
||||
expect( findClosestImageSize( unorderedSrcset, 351 ) ).toEqual( {
|
||||
url: new URL( unorderedSrcset[ 2 ].split( ' ' )[ 0 ] ),
|
||||
width: 400,
|
||||
height: 250,
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "shouldn't find closest image if the closest image isn't close enough in size", () => {
|
||||
expect( findClosestImageSize( srcset, 50 ) ).toBeUndefined();
|
||||
expect( findClosestImageSize( srcset, 110 ) ).toBeUndefined();
|
||||
expect( findClosestImageSize( srcset, 1100 ) ).toBeUndefined();
|
||||
expect( findClosestImageSize( srcset, 1200 ) ).toBeUndefined();
|
||||
} );
|
||||
} );
|
||||
|
||||
function untrackedDynamicSrcset( img: HTMLImageElement ) {
|
||||
dynamicSrcset( img );
|
||||
img.srcset = img.srcset.replaceAll( /&_jb=\w+/gim, '' );
|
||||
}
|
||||
|
||||
describe( 'dynamicSrcset', () => {
|
||||
let img: HTMLImageElement;
|
||||
|
||||
it( '[manual validation] should create a new srcset entry for the target size', () => {
|
||||
window.innerWidth = 9999;
|
||||
window.devicePixelRatio = 1;
|
||||
const manualValidationImg = document.createElement( 'img' );
|
||||
|
||||
manualValidationImg.setAttribute( 'src', 'https://i0.wp.com/example.com/image.jpg' );
|
||||
manualValidationImg.setAttribute(
|
||||
'srcset',
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=100%2C200 1000w, https://i0.wp.com/example.com/image.jpg?resize=400%2C200 400w, https://i0.wp.com/example.com/image.jpg?resize=1400%2C700 1400w`
|
||||
);
|
||||
manualValidationImg.setAttribute( 'width', '800' );
|
||||
manualValidationImg.setAttribute( 'height', '400' );
|
||||
setBoundingRect( manualValidationImg, 800, 400 );
|
||||
|
||||
untrackedDynamicSrcset( manualValidationImg );
|
||||
expect( manualValidationImg.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=800%2C400 9999w`
|
||||
);
|
||||
|
||||
window.devicePixelRatio = 2;
|
||||
untrackedDynamicSrcset( manualValidationImg );
|
||||
expect( manualValidationImg.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=1600%2C800 19998w`
|
||||
);
|
||||
} );
|
||||
|
||||
const testDevicePixelRatios = [ 1, 1.5 ];
|
||||
|
||||
testDevicePixelRatios.forEach( devicePixelRatio => {
|
||||
const w = ( value: number ) => `${ value * devicePixelRatio }w`;
|
||||
describe( `with devicePixelRatio ${ devicePixelRatio }`, () => {
|
||||
beforeEach( () => {
|
||||
window.devicePixelRatio = devicePixelRatio;
|
||||
window.innerWidth = 5000;
|
||||
img = document.createElement( 'img' );
|
||||
img.src = 'https://i0.wp.com/example.com/image.jpg?resize=4444%2C2222';
|
||||
const srcset = [
|
||||
createImageSize( '100,50', '100w' ),
|
||||
createImageSize( '400,250', '400w' ),
|
||||
createImageSize( '1400,700', '1400w' ),
|
||||
];
|
||||
|
||||
img.srcset = srcset.join( ',' );
|
||||
img.setAttribute( 'width', '1000' );
|
||||
img.setAttribute( 'height', '500' );
|
||||
|
||||
// Mocking the bounding rect of the image
|
||||
setBoundingRect( img, 1000, 500 );
|
||||
} );
|
||||
|
||||
it( 'srcset should include all original image sizes', () => {
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain( createImageSize( '100,50', '100w' ) );
|
||||
expect( img.srcset ).toContain( createImageSize( '400,250', '400w' ) );
|
||||
expect( img.srcset ).toContain( createImageSize( '1400,700', '1400w' ) );
|
||||
} );
|
||||
|
||||
it( 'should create a new srcset entry for the target size', () => {
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain( createImageSize( '1000,500', w( window.innerWidth ) ) );
|
||||
} );
|
||||
|
||||
it( 'should reuse existing srcset entry if the target size is close enough', () => {
|
||||
setBoundingRect( img, 396, 248 );
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain( createImageSize( '400,250', w( window.innerWidth ) ) );
|
||||
} );
|
||||
|
||||
it( "shouldn't upscale the image when reusing an srcset entry", () => {
|
||||
setBoundingRect( img, 420, 260 );
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain( createImageSize( '420,260', w( window.innerWidth ) ) );
|
||||
} );
|
||||
|
||||
it( 'should not update attributes if conditions are not met', () => {
|
||||
const image = document.createElement( 'img' );
|
||||
image.src = 'https://i0.wp.com/example.com/image.jpg';
|
||||
|
||||
untrackedDynamicSrcset( image );
|
||||
|
||||
expect( image.srcset ).toBe( '' );
|
||||
expect( image.sizes ).toBe( '' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should reuse existing src image if the target size is close enough', () => {
|
||||
window.innerWidth = 5000;
|
||||
window.devicePixelRatio = 1;
|
||||
setBoundingRect( img, 4400, 2200 );
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=4444%2C2222 5000w`
|
||||
);
|
||||
window.devicePixelRatio = 1.5;
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=6600%2C3300 7500w`
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should reuse existing src image after accounting for DPR > 1', () => {
|
||||
window.innerWidth = 5000;
|
||||
|
||||
window.devicePixelRatio = 1;
|
||||
// Fixed DPR 1 sizes as they'd appear in DOM
|
||||
const srcset = [
|
||||
createImageSize( '100,50', '100w' ),
|
||||
createImageSize( '400,250', '400w' ),
|
||||
createImageSize( '1400,700', '1400w' ),
|
||||
];
|
||||
|
||||
// Pretend this is a dense display rendering a small image
|
||||
window.devicePixelRatio = 3;
|
||||
img.srcset = srcset.join( ',' );
|
||||
setBoundingRect( img, 460, 230 );
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=1400%2C700 15000w`
|
||||
);
|
||||
|
||||
setBoundingRect( img, 500, 250 );
|
||||
untrackedDynamicSrcset( img );
|
||||
expect( img.srcset ).toContain(
|
||||
`https://i0.wp.com/example.com/image.jpg?resize=1500%2C750 15000w`
|
||||
);
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,113 @@
|
||||
type Dimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ImageMeta = {
|
||||
url: URL;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function getDpr() {
|
||||
return window.devicePixelRatio || 1;
|
||||
}
|
||||
|
||||
export function parseImageSize( resizeParam: string ): Dimensions | null {
|
||||
const [ width, height ] = resizeParam.split( ',' ).map( Number );
|
||||
if ( isNaN( width ) || isNaN( height ) ) {
|
||||
return null;
|
||||
}
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
export function getImageSizeFromUrl( url: string ): Dimensions | null {
|
||||
const resizeParam = new URL( url ).searchParams.get( 'resize' );
|
||||
if ( ! resizeParam ) {
|
||||
return null;
|
||||
}
|
||||
return parseImageSize( resizeParam );
|
||||
}
|
||||
|
||||
export function calculateTargetSize( dimensions: Dimensions ): Dimensions {
|
||||
const dpr = getDpr();
|
||||
const ratio = dimensions.width / dimensions.height;
|
||||
const targetWidth = Math.ceil( ( dimensions.width * dpr ) / 10 ) * 10;
|
||||
const targetHeight = Math.ceil( targetWidth / ratio );
|
||||
return {
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
export function isSizeReusable( desiredWidth: number, existingWidth: number ) {
|
||||
if ( existingWidth <= 0 ) {
|
||||
return false;
|
||||
}
|
||||
const diff = existingWidth - desiredWidth;
|
||||
if ( diff < 0 ) {
|
||||
return false;
|
||||
}
|
||||
if ( diff < 50 ) {
|
||||
return true;
|
||||
}
|
||||
const ratio = desiredWidth / existingWidth;
|
||||
return ratio > 0.9 && ratio <= 1;
|
||||
}
|
||||
|
||||
export function findClosestImageSize( urls: string[], targetWidth: number ): ImageMeta | undefined {
|
||||
for ( const src of urls ) {
|
||||
const [ url, widthStr ] = src.trim().split( ' ' );
|
||||
if ( ! widthStr?.trim().endsWith( 'w' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const imageSize = getImageSizeFromUrl( url );
|
||||
if ( ! imageSize ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isSizeReusable( targetWidth, imageSize.width ) ) {
|
||||
return { url: new URL( url ), ...imageSize };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resizeImage( imageUrl: string, targetSize: Dimensions ): URL {
|
||||
const newUrl = new URL( imageUrl );
|
||||
newUrl.searchParams.set( 'resize', `${ targetSize.width },${ targetSize.height }` );
|
||||
return newUrl;
|
||||
}
|
||||
|
||||
export function dynamicSrcset( img: HTMLImageElement ) {
|
||||
if (
|
||||
! img.getAttribute( 'width' ) ||
|
||||
! img.getAttribute( 'height' ) ||
|
||||
! img.srcset ||
|
||||
! img.src ||
|
||||
! img.src.includes( '.wp.com' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = img.getBoundingClientRect();
|
||||
const targetSize = calculateTargetSize( rect );
|
||||
|
||||
const srcset = img.srcset.split( ',' );
|
||||
const closestImage = findClosestImageSize( [ `${ img.src } 0w`, ...srcset ], targetSize.width );
|
||||
|
||||
if ( closestImage ) {
|
||||
closestImage.url.searchParams.set( '_jb', 'closest' );
|
||||
srcset.push( `${ closestImage.url } ${ window.innerWidth * getDpr() }w` );
|
||||
img.srcset = srcset.join( ',' );
|
||||
img.sizes = 'auto';
|
||||
} else {
|
||||
const newUrl = resizeImage( img.src, targetSize );
|
||||
newUrl.searchParams.set( '_jb', 'custom' );
|
||||
srcset.push( `${ newUrl } ${ window.innerWidth * getDpr() }w` );
|
||||
img.srcset = srcset.join( ',' );
|
||||
img.sizes = 'auto';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user