mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
✨ Added format option to img-url
helper (#14962)
fixes https://github.com/TryGhost/Ghost/issues/14323 - Fixed support for resizing images from Unsplash using the `img-url` helper (previously the size property was ignored for images from Unsplash) - Added support for `avif` file formats (supported by sharp out of the box) - Added support for setting the format of images, with a new `format` option: E.g. to convert an image to webp (only works in combination with size for now, except for Unsplash where you can use it without size): ``` {{img_url @site.cover_image size="s" format="webp"}} ``` This can help improve the performance of a theme, by serving assets in `<picture>` elements with webp and fallback image formats. Usage example: ```html <picture> <source srcset="{{img_url feature_image size="s" format="avif"}} 300w, {{img_url feature_image size="m" format="avif"}} 600w, {{img_url feature_image size="l" format="avif"}} 1000w, {{img_url feature_image size="xl" format="avif"}} 2000w" sizes="(min-width: 1400px) 1400px, 92vw" type="image/avif" > <source srcset="{{img_url feature_image size="s" format="webp"}} 300w, {{img_url feature_image size="m" format="webp"}} 600w, {{img_url feature_image size="l" format="webp"}} 1000w, {{img_url feature_image size="xl" format="webp"}} 2000w" sizes="(min-width: 1400px) 1400px, 92vw" type="image/webp" > <img srcset="{{img_url feature_image size="s"}} 300w, {{img_url feature_image size="m"}} 600w, {{img_url feature_image size="l"}} 1000w, {{img_url feature_image size="xl"}} 2000w" sizes="(min-width: 1400px) 1400px, 92vw" src="{{img_url feature_image size="xl"}}" alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}" > </picture> ```
This commit is contained in:
parent
312e2330a1
commit
b7f3892be0
6 changed files with 539 additions and 29 deletions
|
@ -12,6 +12,7 @@ const url = require('url');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const logging = require('@tryghost/logging');
|
const logging = require('@tryghost/logging');
|
||||||
const tpl = require('@tryghost/tpl');
|
const tpl = require('@tryghost/tpl');
|
||||||
|
const imageTransform = require('@tryghost/image-transform');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
attrIsRequired: 'Attribute is required e.g. {{img_url feature_image}}'
|
attrIsRequired: 'Attribute is required e.g. {{img_url feature_image}}'
|
||||||
|
@ -41,15 +42,26 @@ module.exports = function imgUrl(requestedImageUrl, options) {
|
||||||
|
|
||||||
// CASE: if you pass an external image, there is nothing we want to do to it!
|
// CASE: if you pass an external image, there is nothing we want to do to it!
|
||||||
const isInternalImage = detectInternalImage(requestedImageUrl);
|
const isInternalImage = detectInternalImage(requestedImageUrl);
|
||||||
|
const sizeOptions = getImageSizeOptions(options);
|
||||||
|
|
||||||
if (!isInternalImage) {
|
if (!isInternalImage) {
|
||||||
|
// Detect Unsplash width and format
|
||||||
|
const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
|
||||||
|
if (isUnsplashImage) {
|
||||||
|
try {
|
||||||
|
return getUnsplashImage(requestedImageUrl, sizeOptions);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore errors and just return the original URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return requestedImageUrl;
|
return requestedImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {requestedSize, imageSizes} = getImageSizeOptions(options);
|
|
||||||
const absoluteUrlRequested = getAbsoluteOption(options);
|
const absoluteUrlRequested = getAbsoluteOption(options);
|
||||||
|
|
||||||
function applyImageSizes(image) {
|
function applyImageSizes(image) {
|
||||||
return getImageWithSize(image, requestedSize, imageSizes);
|
return getImageWithSize(image, sizeOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageUrl(image) {
|
function getImageUrl(image) {
|
||||||
|
@ -79,10 +91,12 @@ function getAbsoluteOption(options) {
|
||||||
function getImageSizeOptions(options) {
|
function getImageSizeOptions(options) {
|
||||||
const requestedSize = options && options.hash && options.hash.size;
|
const requestedSize = options && options.hash && options.hash.size;
|
||||||
const imageSizes = options && options.data && options.data.config && options.data.config.image_sizes;
|
const imageSizes = options && options.data && options.data.config && options.data.config.image_sizes;
|
||||||
|
const requestedFormat = options && options.hash && options.hash.format;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestedSize,
|
requestedSize,
|
||||||
imageSizes
|
imageSizes,
|
||||||
|
requestedFormat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,12 +113,58 @@ function detectInternalImage(requestedImageUrl) {
|
||||||
return isAbsoluteInternalImage || isRelativeInternalImage;
|
return isAbsoluteInternalImage || isRelativeInternalImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageWithSize(imagePath, requestedSize, imageSizes) {
|
function getUnsplashImage(imagePath, sizeOptions) {
|
||||||
|
const parsedUrl = new URL(imagePath);
|
||||||
|
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||||
|
|
||||||
|
if (requestedFormat) {
|
||||||
|
const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
|
||||||
|
if (supportedFormats.includes(requestedFormat)) {
|
||||||
|
parsedUrl.searchParams.set('fm', requestedFormat);
|
||||||
|
} else if (requestedFormat === 'jpeg') {
|
||||||
|
// Map to alias
|
||||||
|
parsedUrl.searchParams.set('fm', 'jpg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageSizes || !imageSizes[requestedSize]) {
|
||||||
|
return parsedUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const {width, height} = imageSizes[requestedSize];
|
||||||
|
|
||||||
|
if (!width && !height) {
|
||||||
|
return parsedUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUrl.searchParams.delete('w');
|
||||||
|
parsedUrl.searchParams.delete('h');
|
||||||
|
|
||||||
|
if (width) {
|
||||||
|
parsedUrl.searchParams.set('w', width);
|
||||||
|
}
|
||||||
|
if (height) {
|
||||||
|
parsedUrl.searchParams.set('h', height);
|
||||||
|
}
|
||||||
|
return parsedUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} imagePath
|
||||||
|
* @param {Object} sizeOptions
|
||||||
|
* @param {string} sizeOptions.requestedSize
|
||||||
|
* @param {Object[]} sizeOptions.imageSizes
|
||||||
|
* @param {string} [sizeOptions.requestedFormat]
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getImageWithSize(imagePath, sizeOptions) {
|
||||||
const hasLeadingSlash = imagePath[0] === '/';
|
const hasLeadingSlash = imagePath[0] === '/';
|
||||||
|
|
||||||
if (hasLeadingSlash) {
|
if (hasLeadingSlash) {
|
||||||
return '/' + getImageWithSize(imagePath.slice(1), requestedSize, imageSizes);
|
return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
|
||||||
}
|
}
|
||||||
|
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||||
|
|
||||||
if (!requestedSize) {
|
if (!requestedSize) {
|
||||||
return imagePath;
|
return imagePath;
|
||||||
|
@ -123,8 +183,9 @@ function getImageWithSize(imagePath, requestedSize, imageSizes) {
|
||||||
const [imgBlogUrl, imageName] = imagePath.split(STATIC_IMAGE_URL_PREFIX);
|
const [imgBlogUrl, imageName] = imagePath.split(STATIC_IMAGE_URL_PREFIX);
|
||||||
|
|
||||||
const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
|
const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
|
||||||
|
const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
|
||||||
|
|
||||||
return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, imageName].join('');
|
return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefixIfPresent(prefix, string) {
|
function prefixIfPresent(prefix, string) {
|
||||||
|
|
|
@ -58,11 +58,6 @@ module.exports = function (req, res, next) {
|
||||||
const themeImageSizes = activeTheme.get().config('image_sizes');
|
const themeImageSizes = activeTheme.get().config('image_sizes');
|
||||||
const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
|
const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
|
||||||
|
|
||||||
// CASE: no image_sizes config (NOTE - unlikely to be reachable now we have content sizes)
|
|
||||||
if (!imageSizes) {
|
|
||||||
return redirectToOriginal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// build a new object with keys that match the strings used in size paths like "w640h480"
|
// build a new object with keys that match the strings used in size paths like "w640h480"
|
||||||
const imageDimensions = {};
|
const imageDimensions = {};
|
||||||
Object.keys(imageSizes).forEach((size) => {
|
Object.keys(imageSizes).forEach((size) => {
|
||||||
|
@ -106,16 +101,16 @@ module.exports = function (req, res, next) {
|
||||||
return redirectToOriginal();
|
return redirectToOriginal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exit early if sharp isn't installed to avoid extra file reads
|
||||||
|
if (!imageTransform.canTransformFiles()) {
|
||||||
|
return redirectToOriginal();
|
||||||
|
}
|
||||||
|
|
||||||
storageInstance.exists(req.url).then((exists) => {
|
storageInstance.exists(req.url).then((exists) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exit early if sharp isn't installed to avoid extra file reads
|
|
||||||
if (!imageTransform.canTransformFiles()) {
|
|
||||||
return redirectToOriginal();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {dir, name, ext} = path.parse(imagePath);
|
const {dir, name, ext} = path.parse(imagePath);
|
||||||
const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
|
const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
|
||||||
|
|
||||||
|
@ -148,7 +143,8 @@ module.exports = function (req, res, next) {
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (format) {
|
if (format) {
|
||||||
// File extension won't match the new format, so we need to update the Content-Type header manually here
|
// File extension won't match the new format, so we need to update the Content-Type header manually here
|
||||||
res.type(format);
|
// Express JS still uses an out of date mime package, which doesn't support avif
|
||||||
|
res.type(format === 'avif' ? 'image/avif' : format);
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
"@tryghost/errors": "1.2.14",
|
"@tryghost/errors": "1.2.14",
|
||||||
"@tryghost/express-dynamic-redirects": "0.0.0",
|
"@tryghost/express-dynamic-redirects": "0.0.0",
|
||||||
"@tryghost/helpers": "1.1.71",
|
"@tryghost/helpers": "1.1.71",
|
||||||
"@tryghost/image-transform": "1.1.0",
|
"@tryghost/image-transform": "1.2.0",
|
||||||
"@tryghost/job-manager": "0.0.0",
|
"@tryghost/job-manager": "0.0.0",
|
||||||
"@tryghost/kg-card-factory": "3.1.3",
|
"@tryghost/kg-card-factory": "3.1.3",
|
||||||
"@tryghost/kg-default-atoms": "3.1.2",
|
"@tryghost/kg-default-atoms": "3.1.2",
|
||||||
|
|
|
@ -135,6 +135,7 @@ describe('{{img_url}} helper', function () {
|
||||||
should.exist(rendered);
|
should.exist(rendered);
|
||||||
rendered.should.equal('/content/images/size/w400/my-coole-img.jpg');
|
rendered.should.equal('/content/images/size/w400/my-coole-img.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should output the correct url for protocol relative urls', function () {
|
it('should output the correct url for protocol relative urls', function () {
|
||||||
const rendered = img_url('//website.com/whatever/my-coole-img.jpg', {
|
const rendered = img_url('//website.com/whatever/my-coole-img.jpg', {
|
||||||
hash: {
|
hash: {
|
||||||
|
@ -153,6 +154,7 @@ describe('{{img_url}} helper', function () {
|
||||||
should.exist(rendered);
|
should.exist(rendered);
|
||||||
rendered.should.equal('//website.com/whatever/my-coole-img.jpg');
|
rendered.should.equal('//website.com/whatever/my-coole-img.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should output the correct url for relative paths', function () {
|
it('should output the correct url for relative paths', function () {
|
||||||
const rendered = img_url('/content/images/my-coole-img.jpg', {
|
const rendered = img_url('/content/images/my-coole-img.jpg', {
|
||||||
hash: {
|
hash: {
|
||||||
|
@ -190,5 +192,286 @@ describe('{{img_url}} helper', function () {
|
||||||
should.exist(rendered);
|
should.exist(rendered);
|
||||||
rendered.should.equal('content/images/size/w400/my-coole-img.jpg');
|
rendered.should.equal('content/images/size/w400/my-coole-img.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores invalid size options', function () {
|
||||||
|
const rendered = img_url('/content/images/author-image-relative-url.png', {hash: {size: 'invalid-size'}});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('/content/images/author-image-relative-url.png');
|
||||||
|
logWarnStub.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores misconfigured sizes', function () {
|
||||||
|
const rendered = img_url('/content/images/author-image-relative-url.png', {
|
||||||
|
hash: {
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {typo: 600}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('/content/images/author-image-relative-url.png');
|
||||||
|
logWarnStub.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores format if size is missing', function () {
|
||||||
|
const rendered = img_url('/content/images/author-image-relative-url.png', {
|
||||||
|
hash: {
|
||||||
|
format: 'webp'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
w600: {width: 600}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('/content/images/author-image-relative-url.png');
|
||||||
|
logWarnStub.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds format and size options', function () {
|
||||||
|
const rendered = img_url('/content/images/author-image-relative-url.png', {
|
||||||
|
hash: {
|
||||||
|
size: 'w600',
|
||||||
|
format: 'webp'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
w600: {width: 600}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('/content/images/size/w600/format/webp/author-image-relative-url.png');
|
||||||
|
logWarnStub.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid formats', function () {
|
||||||
|
const rendered = img_url('/content/images/author-image-relative-url.png', {
|
||||||
|
hash: {
|
||||||
|
size: 'w600',
|
||||||
|
format: 'invalid'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
w600: {width: 600}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('/content/images/size/w600/author-image-relative-url.png');
|
||||||
|
logWarnStub.called.should.be.false();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Unsplash', function () {
|
||||||
|
before(function () {
|
||||||
|
configUtils.set({url: 'http://localhost:65535/'});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
configUtils.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works without size option', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change the output width', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=400');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change the output height', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80', {
|
||||||
|
hash: {
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
height: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&h=400');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid image size configurations', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80', {
|
||||||
|
hash: {
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
typo: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid urls', function () {
|
||||||
|
const invalid = ':https://images.unsplash.com/test';
|
||||||
|
const rendered = img_url(invalid, {
|
||||||
|
hash: {
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal(invalid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid sizes', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
size: 'invalid'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change the output format', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
format: 'webp'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=webp&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores invalid formats', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
format: 'invalid'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transforms jpeg to jpg', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=auto&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
format: 'jpeg'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change the output format and size', function () {
|
||||||
|
const rendered = img_url('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=2000', {
|
||||||
|
hash: {
|
||||||
|
format: 'webp',
|
||||||
|
size: 'medium'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
config: {
|
||||||
|
image_sizes: {
|
||||||
|
medium: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should.exist(rendered);
|
||||||
|
rendered.should.equal('https://images.unsplash.com/photo-1657816793628-191deb91e20f?crop=entropy&cs=tinysrgb&fit=max&fm=webp&ixid=MnwxMTc3M3wwfDF8YWxsfDJ8fHx8fHwyfHwxNjU3ODkzNjU5&ixlib=rb-1.2.1&q=80&w=400');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,14 @@ const storage = require('../../../../../core/server/adapters/storage');
|
||||||
const activeTheme = require('../../../../../core/frontend/services/theme-engine/active');
|
const activeTheme = require('../../../../../core/frontend/services/theme-engine/active');
|
||||||
const handleImageSizes = require('../../../../../core/frontend/web/middleware/handle-image-sizes.js');
|
const handleImageSizes = require('../../../../../core/frontend/web/middleware/handle-image-sizes.js');
|
||||||
const imageTransform = require('@tryghost/image-transform');
|
const imageTransform = require('@tryghost/image-transform');
|
||||||
|
const config = require('../../../../../core/shared/config');
|
||||||
|
|
||||||
// @TODO make these tests lovely and non specific to implementation
|
// @TODO make these tests lovely and non specific to implementation
|
||||||
describe('handleImageSizes middleware', function () {
|
describe('handleImageSizes middleware', function () {
|
||||||
|
this.afterEach(function () {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
it('calls next immediately if the url does not match /size/something/', function (done) {
|
it('calls next immediately if the url does not match /size/something/', function (done) {
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
url: '/size/something'
|
url: '/size/something'
|
||||||
|
@ -33,6 +38,32 @@ describe('handleImageSizes middleware', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls next immediately if the file extension is missing', function (done) {
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/something/file'
|
||||||
|
};
|
||||||
|
// CASE: second thing middleware does is try to match to a regex
|
||||||
|
fakeReq.url.match = function () {
|
||||||
|
throw new Error('Should have exited immediately');
|
||||||
|
};
|
||||||
|
handleImageSizes(fakeReq, {}, function next() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls next immediately if the file has a trailing slash', function (done) {
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/something/file.jpg/'
|
||||||
|
};
|
||||||
|
// CASE: second thing middleware does is try to match to a regex
|
||||||
|
fakeReq.url.match = function () {
|
||||||
|
throw new Error('Should have exited immediately');
|
||||||
|
};
|
||||||
|
handleImageSizes(fakeReq, {}, function next() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('calls next immediately if the url does not match /size/something/', function (done) {
|
it('calls next immediately if the url does not match /size/something/', function (done) {
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
url: '/size//'
|
url: '/size//'
|
||||||
|
@ -74,7 +105,18 @@ describe('handleImageSizes middleware', function () {
|
||||||
return {
|
return {
|
||||||
l: {
|
l: {
|
||||||
width: 1000
|
width: 1000
|
||||||
}
|
},
|
||||||
|
m: {
|
||||||
|
width: 1000,
|
||||||
|
height: 200
|
||||||
|
},
|
||||||
|
n: {
|
||||||
|
height: 1000
|
||||||
|
},
|
||||||
|
h100: {
|
||||||
|
height: 100
|
||||||
|
},
|
||||||
|
missing: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,10 +127,6 @@ describe('handleImageSizes middleware', function () {
|
||||||
resizeFromBufferStub = sinon.stub(imageTransform, 'resizeFromBuffer').resolves(Buffer.from([]));
|
resizeFromBufferStub = sinon.stub(imageTransform, 'resizeFromBuffer').resolves(Buffer.from([]));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.afterEach(function () {
|
|
||||||
sinon.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects for invalid format extension', function (done) {
|
it('redirects for invalid format extension', function (done) {
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
url: '/size/w1000/format/test/image.jpg',
|
url: '/size/w1000/format/test/image.jpg',
|
||||||
|
@ -112,6 +150,52 @@ describe('handleImageSizes middleware', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redirects for invalid sizes', function (done) {
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/w123/image.jpg',
|
||||||
|
originalUrl: '/blog/content/images/size/w123/image.jpg'
|
||||||
|
};
|
||||||
|
const fakeRes = {
|
||||||
|
redirect(url) {
|
||||||
|
try {
|
||||||
|
url.should.equal('/blog/content/images/image.jpg');
|
||||||
|
} catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleImageSizes(fakeReq, fakeRes, function next(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
done(new Error('Should not have called next'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects for invalid configured size', function (done) {
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/missing/image.jpg',
|
||||||
|
originalUrl: '/blog/content/images/size/missing/image.jpg'
|
||||||
|
};
|
||||||
|
const fakeRes = {
|
||||||
|
redirect(url) {
|
||||||
|
try {
|
||||||
|
url.should.equal('/blog/content/images/image.jpg');
|
||||||
|
} catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleImageSizes(fakeReq, fakeRes, function next(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
done(new Error('Should not have called next'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('returns original URL if file is empty', function (done) {
|
it('returns original URL if file is empty', function (done) {
|
||||||
dummyStorage.exists = async function (path) {
|
dummyStorage.exists = async function (path) {
|
||||||
if (path === '/blank_o.png') {
|
if (path === '/blank_o.png') {
|
||||||
|
@ -148,6 +232,58 @@ describe('handleImageSizes middleware', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns original URL if unsupported storage adapter', function (done) {
|
||||||
|
dummyStorage.saveRaw = undefined;
|
||||||
|
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/w1000/blank.png',
|
||||||
|
originalUrl: '/blog/content/images/size/w1000/blank.png'
|
||||||
|
};
|
||||||
|
const fakeRes = {
|
||||||
|
redirect(url) {
|
||||||
|
try {
|
||||||
|
url.should.equal('/blog/content/images/blank.png');
|
||||||
|
} catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageSizes(fakeReq, fakeRes, function next(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
done(new Error('Should not have called next'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects if sharp is not installed', function (done) {
|
||||||
|
sinon.stub(imageTransform, 'canTransformFiles').returns(false);
|
||||||
|
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/w1000/blank.png',
|
||||||
|
originalUrl: '/blog/content/images/size/w1000/blank.png'
|
||||||
|
};
|
||||||
|
const fakeRes = {
|
||||||
|
redirect(url) {
|
||||||
|
try {
|
||||||
|
url.should.equal('/blog/content/images/blank.png');
|
||||||
|
} catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageSizes(fakeReq, fakeRes, function next(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
done(new Error('Should not have called next'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('continues if file exists', function (done) {
|
it('continues if file exists', function (done) {
|
||||||
dummyStorage.exists = async function (path) {
|
dummyStorage.exists = async function (path) {
|
||||||
if (path === '/size/w1000/blank.png') {
|
if (path === '/size/w1000/blank.png') {
|
||||||
|
@ -182,8 +318,8 @@ describe('handleImageSizes middleware', function () {
|
||||||
const spy = sinon.spy(dummyStorage, 'read');
|
const spy = sinon.spy(dummyStorage, 'read');
|
||||||
|
|
||||||
const fakeReq = {
|
const fakeReq = {
|
||||||
url: '/size/w1000/blank.png',
|
url: '/size/h100/blank.png',
|
||||||
originalUrl: '/size/w1000/blank.png'
|
originalUrl: '/size/h100/blank.png'
|
||||||
};
|
};
|
||||||
const fakeRes = {
|
const fakeRes = {
|
||||||
redirect(url) {
|
redirect(url) {
|
||||||
|
@ -415,6 +551,40 @@ describe('handleImageSizes middleware', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can format PNG to AVIF', function (done) {
|
||||||
|
dummyStorage.exists = async function (path) {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
dummyStorage.read = async function (path) {
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeReq = {
|
||||||
|
url: '/size/w1000/format/avif/blank.png',
|
||||||
|
originalUrl: '/size/w1000/format/avif/blank.png'
|
||||||
|
};
|
||||||
|
const fakeRes = {
|
||||||
|
redirect(url) {
|
||||||
|
done(new Error('Should not have called redirect'));
|
||||||
|
},
|
||||||
|
type: function () {}
|
||||||
|
};
|
||||||
|
const typeStub = sinon.spy(fakeRes, 'type');
|
||||||
|
|
||||||
|
handleImageSizes(fakeReq, fakeRes, function next(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resizeFromBufferStub.calledOnceWithExactly(buffer, {withoutEnlargement: true, width: 1000, format: 'avif'}).should.be.true();
|
||||||
|
typeStub.calledOnceWithExactly('image/avif').should.be.true();
|
||||||
|
} catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('can format GIF to WEBP', function (done) {
|
it('can format GIF to WEBP', function (done) {
|
||||||
dummyStorage.exists = async function (path) {
|
dummyStorage.exists = async function (path) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1703,10 +1703,10 @@
|
||||||
"@tryghost/errors" "^1.2.14"
|
"@tryghost/errors" "^1.2.14"
|
||||||
"@tryghost/request" "^0.1.28"
|
"@tryghost/request" "^0.1.28"
|
||||||
|
|
||||||
"@tryghost/image-transform@1.1.0":
|
"@tryghost/image-transform@1.2.0":
|
||||||
version "1.1.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tryghost/image-transform/-/image-transform-1.1.0.tgz#23f1abc7eca781cd65e6c99d1bfc463a744b40c1"
|
resolved "https://registry.yarnpkg.com/@tryghost/image-transform/-/image-transform-1.2.0.tgz#fe00ac76c412ce9bfa29030f2869f118e753c1e2"
|
||||||
integrity sha512-8gSTIqPOnEBSOMc1s9xR43l8U0z7gorPVbBQWMQ6ZXM3hFAT8UubLdvqpO3OBDbsi115DRxVtH4RkkZVMHBfIQ==
|
integrity sha512-Y2KTbhUttdXp2xvA3hKfjiRV3WcbEkmeI+ipYxrdAdp8jwj97BeF//5lTakq2zb+0L0HE9EaYsHgbdvW3YTHhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tryghost/errors" "^1.2.1"
|
"@tryghost/errors" "^1.2.1"
|
||||||
bluebird "^3.7.2"
|
bluebird "^3.7.2"
|
||||||
|
|
Loading…
Add table
Reference in a new issue