Skip to content

Commit

Permalink
Merge branch 'sharp'
Browse files Browse the repository at this point in the history
  • Loading branch information
liamfiddler committed Jul 29, 2020
2 parents 0ce03da + d296f22 commit 814b85c
Show file tree
Hide file tree
Showing 14 changed files with 906 additions and 5,450 deletions.
226 changes: 104 additions & 122 deletions .eleventy.js
Original file line number Diff line number Diff line change
@@ -1,125 +1,129 @@
const fs = require('fs');
const url = require('url');
const querystring = require('querystring');
const path = require('path');
const { JSDOM } = require('jsdom');
const Jimp = require('jimp');

const supportedExtensions = ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'tiff'];

const transformImgPath = (src) => {
if (src.startsWith('/') && !src.startsWith('//')) {
return `.${src}`;
}

return src;
};

const sharp = require('sharp');
const fetch = require('node-fetch');
const cache = require('./cache');
const {
transformImgPath,
logMessage,
initScript,
checkConfig,
} = require('./helpers');

// List of file extensions this plugin can handle (basically just what sharp supports)
const supportedExtensions = [
'jpg',
'jpeg',
'gif',
'png',
'webp',
'svg',
'tiff',
];

// The default values for the plugin
const defaultLazyImagesConfig = {
maxPlaceholderWidth: 12,
maxPlaceholderHeight: 12,
placeholderQuality: 60,
maxPlaceholderWidth: 25,
maxPlaceholderHeight: 25,
imgSelector: 'img',
transformImgPath,
className: ['lazyload'],
cacheFile: '.lazyimages.json',
appendInitScript: true,
scriptSrc: 'https://cdn.jsdelivr.net/npm/lazysizes@5/lazysizes.min.js',
preferNativeLazyLoad: true,
preferNativeLazyLoad: false,
};

// A global to store the current config (saves us passing it around functions)
let lazyImagesConfig = defaultLazyImagesConfig;
let lazyImagesCache = {};

const logMessage = (message) => {
console.log(`LazyImages - ${message}`);
};
// Reads the image object from the source file
const readImage = async (imageSrc) => {
let image;

const loadCache = () => {
const { cacheFile } = lazyImagesConfig;

if (!cacheFile) {
return;
if (imageSrc.startsWith('http') || imageSrc.startsWith('//')) {
const res = await fetch(imageSrc);
const buffer = await res.buffer();
image = await sharp(buffer);
return image;
}

try {
if (fs.existsSync(cacheFile)) {
const cachedData = fs.readFileSync(cacheFile, 'utf8');
lazyImagesCache = JSON.parse(cachedData);
image = await sharp(imageSrc);
await image.metadata(); // just to confirm it can be read
} catch (firstError) {
try {
// We couldn't read the file at the input path, but maybe it's
// in './src', developers love to put things in './src'
image = await sharp(`./src/${imageSrc}`);
await image.metadata();
} catch (secondError) {
throw firstError;
}
} catch (e) {
console.error('LazyImages: cacheFile', e);
}
};

const readCache = (imageSrc) => {
if (imageSrc in lazyImagesCache) {
return lazyImagesCache[imageSrc];
}

return undefined;
};

const updateCache = (imageSrc, imageData) => {
const { cacheFile } = lazyImagesConfig;
lazyImagesCache[imageSrc] = imageData;

if (cacheFile) {
const cacheData = JSON.stringify(lazyImagesCache);

fs.writeFile(cacheFile, cacheData, (err) => {
if (err) {
console.error('LazyImages: cacheFile', e);
}
});
}
return image;
};

// Gets the image width+height+LQIP from the cache, or generates them if not found
const getImageData = async (imageSrc) => {
const {
maxPlaceholderWidth,
maxPlaceholderHeight,
placeholderQuality,
cacheFile,
} = lazyImagesConfig;

let imageData = readCache(imageSrc);
let imageData = cache.read(imageSrc);

if (imageData) {
return imageData;
}

logMessage(`started processing ${imageSrc}`);

const image = await Jimp.read(imageSrc);
const width = image.bitmap.width;
const height = image.bitmap.height;
const image = await readImage(imageSrc);
const metadata = await image.metadata();
const width = metadata.width;
const height = metadata.height;

const resized = image
.scaleToFit(maxPlaceholderWidth, maxPlaceholderHeight)
.quality(placeholderQuality);
const lqip = await image
.resize({
width: maxPlaceholderWidth,
height: maxPlaceholderHeight,
fit: sharp.fit.inside,
})
.blur()
.toBuffer();

const encoded = await resized.getBase64Async(Jimp.AUTO);
const encodedLqip = lqip.toString('base64');

imageData = {
width,
height,
src: encoded,
src: `data:image/png;base64,${encodedLqip}`,
};

logMessage(`finished processing ${imageSrc}`);
updateCache(imageSrc, imageData);
cache.update(cacheFile, imageSrc, imageData);
return imageData;
};

const processImage = async (imgElem) => {
const { transformImgPath, className } = lazyImagesConfig;
// Adds the attributes to the image element
const processImage = async (imgElem, options) => {
const {
transformImgPath,
className,
preferNativeLazyLoad,
} = lazyImagesConfig;

if (/^data:/.test(imgElem.src)) {
logMessage(`skipping "data:" src`);
if (imgElem.src.startsWith('data:')) {
logMessage('skipping image with data URI');
return;
}

const imgPath = transformImgPath(imgElem.src);
const imgPath = transformImgPath(imgElem.src, options);
const parsedUrl = url.parse(imgPath);
let fileExt = path.extname(parsedUrl.pathname).substr(1);

Expand All @@ -128,7 +132,10 @@ const processImage = async (imgElem) => {
fileExt = querystring.parse(parsedUrl.query).format;
}

imgElem.setAttribute('loading', 'lazy');
if (preferNativeLazyLoad) {
imgElem.setAttribute('loading', 'lazy');
}

imgElem.setAttribute('data-src', imgElem.src);

const classNameArr = Array.isArray(className) ? className : [className];
Expand All @@ -147,56 +154,32 @@ const processImage = async (imgElem) => {

try {
const image = await getImageData(imgPath);
const imageWidth = imgElem.getAttribute('width')

if (!imageWidth) {
imgElem.setAttribute('width', image.width);
}
imgElem.setAttribute('src', image.src);

if (!imgElem.hasAttribute('height')) {
// If 'width' attribute was set on the image, we should set appropriate height,
// to keep correct aspect ratio
if (imageWidth) {
imgElem.setAttribute('height', image.height * imageWidth / image.width);
} else {
imgElem.setAttribute('height', image.height);
}
// Don't set width/height for vector images
if (fileExt === 'svg') {
return;
}

imgElem.setAttribute('src', image.src);
} catch (e) {
console.error('LazyImages', imgPath, e);
}
};
const widthAttr = imgElem.getAttribute('width');
const heightAttr = imgElem.getAttribute('height');

// Have to use lowest common denominator JS language features here
// because we don't know what the target browser support is
const initLazyImages = function (selector, src, preferNativeLazyLoad) {
if (preferNativeLazyLoad && 'loading' in HTMLImageElement.prototype) {
var images = document.querySelectorAll(selector);
var numImages = images.length;

if (numImages > 0) {
for (var i = 0; i < numImages; i++) {
if ('dataset' in images[i] && 'src' in images[i].dataset) {
images[i].src = images[i].dataset.src;
}

if ('srcset' in images[i].dataset) {
images[i].srcset = images[i].dataset.srcset;
}
}
if (!widthAttr && !heightAttr) {
imgElem.setAttribute('width', image.width);
imgElem.setAttribute('height', image.height);
} else if (widthAttr && !heightAttr) {
const ratioHeight = (image.height * widthAttr) / image.width;
imgElem.setAttribute('height', Math.round(ratioHeight));
} else if (heightAttr && !widthAttr) {
const ratioWidth = (image.width * heightAttr) / image.height;
imgElem.setAttribute('width', Math.round(ratioWidth));
}

return;
} catch (e) {
logMessage(`${e.message}: ${imgPath}`);
}

var script = document.createElement('script');
script.async = true;
script.src = src;
document.body.appendChild(script);
};

// Scans the output HTML for images, processes them, & appends the init script
const transformMarkup = async (rawContent, outputPath) => {
const {
imgSelector,
Expand All @@ -212,22 +195,20 @@ const transformMarkup = async (rawContent, outputPath) => {

if (images.length > 0) {
logMessage(`found ${images.length} images in ${outputPath}`);
await Promise.all(images.map(processImage));
await Promise.all(images.map((image) => processImage(image, { outputPath })));
logMessage(`processed ${images.length} images in ${outputPath}`);

if (appendInitScript) {
dom.window.document.body.insertAdjacentHTML(
'beforeend',
`<script>
(${initLazyImages.toString()})(
(${initScript.toString()})(
'${imgSelector}',
'${scriptSrc}',
${!!preferNativeLazyLoad}
);
</script>`
);
} else if (scriptSrc !== defaultLazyImagesConfig.scriptSrc) {
console.warn('LazyImages - scriptSrc config is ignored because appendInitScript=false');
}

content = dom.serialize();
Expand All @@ -237,16 +218,17 @@ const transformMarkup = async (rawContent, outputPath) => {
return content;
};

// Export as 11ty plugin
module.exports = {
initArguments: {},
configFunction: (eleventyConfig, pluginOptions = {}) => {
lazyImagesConfig = Object.assign(
{},
defaultLazyImagesConfig,
pluginOptions
);
lazyImagesConfig = {
...defaultLazyImagesConfig,
...pluginOptions,
};

loadCache();
checkConfig(lazyImagesConfig, defaultLazyImagesConfig);
cache.load(lazyImagesConfig.cacheFile);
eleventyConfig.addTransform('lazyimages', transformMarkup);
},
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ _site

# other junk
.DS_Store
example/**/package-lock.json
example/**/.lazyimages.json
44 changes: 44 additions & 0 deletions cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const fs = require('fs');

// A global to store the cache data in memory
let lazyImagesCache = {};

// Loads the cache data into memory
exports.load = (cacheFile) => {
if (!cacheFile) {
return;
}

try {
if (fs.existsSync(cacheFile)) {
const cachedData = fs.readFileSync(cacheFile, 'utf8');
lazyImagesCache = JSON.parse(cachedData);
}
} catch (e) {
console.error('LazyImages - cacheFile', e);
}
};

// Reads the cached data for an image
exports.read = (imageSrc) => {
if (imageSrc in lazyImagesCache) {
return lazyImagesCache[imageSrc];
}

return undefined;
};

// Updates image data in the cache
exports.update = (cacheFile, imageSrc, imageData) => {
lazyImagesCache[imageSrc] = imageData;

if (cacheFile) {
const cacheData = JSON.stringify(lazyImagesCache);

fs.writeFile(cacheFile, cacheData, (err) => {
if (err) {
console.error('LazyImages - cacheFile', e);
}
});
}
};
1 change: 0 additions & 1 deletion example/basic/.lazyimages.json

This file was deleted.

2 changes: 1 addition & 1 deletion example/basic/_includes/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
margin: 0 auto;
padding: 1rem;
}
img {
img:not(.unstyled) {
display: block;
width: 100%;
max-width: 100%;
Expand Down
Loading

0 comments on commit 814b85c

Please sign in to comment.