109 lines
3.4 KiB
React
109 lines
3.4 KiB
React
|
// @flow
|
||
|
import React from 'react';
|
||
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||
|
|
||
|
function scaleToDevicePixelRatio(value: number, window: any) {
|
||
|
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||
|
return Math.ceil(value * devicePixelRatio);
|
||
|
}
|
||
|
|
||
|
type Props = {
|
||
|
src: string,
|
||
|
objectFit?: string,
|
||
|
};
|
||
|
|
||
|
function OptimizedImage(props: Props) {
|
||
|
const { objectFit, src, ...imgProps } = props;
|
||
|
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||
|
const ref = React.useRef<any>();
|
||
|
|
||
|
function getOptimizedImgUrl(url, width, height) {
|
||
|
let optimizedUrl = url;
|
||
|
if (url && !url.startsWith('/public/')) {
|
||
|
optimizedUrl = url.trim().replace(/^http:\/\//i, 'https://');
|
||
|
|
||
|
// @if TARGET='web'
|
||
|
if (!optimizedUrl.endsWith('.gif')) {
|
||
|
optimizedUrl = getThumbnailCdnUrl({ thumbnail: optimizedUrl, width, height, quality: 85 });
|
||
|
}
|
||
|
// @endif
|
||
|
}
|
||
|
return optimizedUrl;
|
||
|
}
|
||
|
|
||
|
function getOptimumSize(elem) {
|
||
|
if (!elem || !elem.parentElement || !elem.parentElement.clientWidth || !elem.parentElement.clientHeight) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
let width = elem.parentElement.clientWidth;
|
||
|
let height = elem.parentElement.clientHeight;
|
||
|
|
||
|
width = scaleToDevicePixelRatio(width, window);
|
||
|
height = scaleToDevicePixelRatio(height, window);
|
||
|
|
||
|
// Round to next 100px for better caching
|
||
|
width = Math.ceil(width / 100) * 100;
|
||
|
height = Math.ceil(height / 100) * 100;
|
||
|
|
||
|
// Reminder: CDN expects integers.
|
||
|
return { width, height };
|
||
|
}
|
||
|
|
||
|
function adjustOptimizationIfNeeded(elem, objectFit, origSrc) {
|
||
|
if (objectFit === 'cover' && elem) {
|
||
|
const containerSize = getOptimumSize(elem);
|
||
|
if (containerSize) {
|
||
|
// $FlowFixMe
|
||
|
if (elem.naturalWidth < containerSize.width) {
|
||
|
// For 'cover', we don't want to stretch the image. We started off by
|
||
|
// filling up the container height, but the width still has a gap for
|
||
|
// this instance (usually due to aspect ratio mismatch).
|
||
|
// If the original image is much larger, we can request for a larger
|
||
|
// image so that "objectFit=cover" will center it without stretching and
|
||
|
// making it blur. The double fetch might seem wasteful, but on
|
||
|
// average the total transferred bytes is still less than the original.
|
||
|
const probablyMaxedOut = elem.naturalHeight < containerSize.height;
|
||
|
if (!probablyMaxedOut) {
|
||
|
const newOptimizedSrc = getOptimizedImgUrl(origSrc, containerSize.width, 0);
|
||
|
if (newOptimizedSrc && newOptimizedSrc !== optimizedSrc) {
|
||
|
setOptimizedSrc(newOptimizedSrc);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
React.useEffect(() => {
|
||
|
const containerSize = getOptimumSize(ref.current);
|
||
|
if (containerSize) {
|
||
|
const width = 0; // The CDN will fill the zeroed attribute per image's aspect ratio.
|
||
|
const height = containerSize.height;
|
||
|
|
||
|
const newOptimizedSrc = getOptimizedImgUrl(src, width, height);
|
||
|
|
||
|
if (newOptimizedSrc !== optimizedSrc) {
|
||
|
setOptimizedSrc(newOptimizedSrc);
|
||
|
}
|
||
|
} else {
|
||
|
setOptimizedSrc(src);
|
||
|
}
|
||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
|
||
|
if (!src) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<img
|
||
|
ref={ref}
|
||
|
{...imgProps}
|
||
|
src={optimizedSrc}
|
||
|
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
|
||
|
/>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default OptimizedImage;
|