/
handle-image-sizes.js
149 lines (123 loc) · 5.5 KB
/
handle-image-sizes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const _ = require('lodash');
const path = require('path');
const {NoContentError} = require('@tryghost/errors');
const imageTransform = require('@tryghost/image-transform');
const storage = require('../../../server/adapters/storage');
const activeTheme = require('../../services/theme-engine/active');
const config = require('../../../shared/config');
const {imageSize} = require('../../../server/lib/image');
const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//;
const FORMAT_PATH_REGEX = /^\/format\/([^./]+)\//;
const TRAILING_SLASH_REGEX = /\/+$/;
module.exports = function handleImageSizes(req, res, next) {
// In admin we need to read images and calculate the average color (blocked by CORS otherwise)
res.setHeader('Access-Control-Allow-Origin', '*');
if (!SIZE_PATH_REGEX.test(req.url)) {
return next();
}
if (TRAILING_SLASH_REGEX.test(req.url)) {
return next();
}
const requestedDimension = req.url.match(SIZE_PATH_REGEX)[1];
// Note that we don't use sizeImageDir because we need to keep the trailing slash
let imagePath = req.url.replace(`/size/${requestedDimension}`, '');
// Check if we want to format the image
let format = null;
const matchedFormat = imagePath.match(FORMAT_PATH_REGEX);
if (matchedFormat) {
format = matchedFormat[1];
// Note that we don't use matchedFormat[0] because we need to keep the trailing slash
imagePath = imagePath.replace(`/format/${format}`, '');
}
const redirectToOriginal = () => {
// We need to keep the first slash here
let url = req.originalUrl
.replace(`/size/${requestedDimension}`, '');
if (format) {
url = url.replace(`/format/${format}`, '');
}
return res.redirect(url);
};
const requestUrlFileExtension = path.parse(req.url).ext;
// CASE: no file extension was given
if (requestUrlFileExtension === '') {
return next();
}
const contentImageSizes = config.get('imageOptimization:contentImageSizes');
const internalImageSizes = config.get('imageOptimization:internalImageSizes');
const themeImageSizes = activeTheme.get().config('image_sizes');
const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
// build a new object with keys that match the strings used in size paths like "w640h480"
const imageDimensions = {};
Object.keys(imageSizes).forEach((size) => {
const {width, height} = imageSizes[size];
const dimension = (width ? 'w' + width : '') + (height ? 'h' + height : '');
// if there are duplicate size names the first encountered wins
if (!imageDimensions[dimension]) {
imageDimensions[dimension] = imageSizes[size];
}
});
const imageDimensionConfig = imageDimensions[requestedDimension];
// CASE: unknown dimension
if (!imageDimensionConfig || (!imageDimensionConfig.width && !imageDimensionConfig.height)) {
return redirectToOriginal();
}
// CASE: image transform is not capable of transforming some files (e.g. .ico)
if (!imageTransform.canTransformFileExtension(requestUrlFileExtension)) {
return redirectToOriginal();
}
if (format) {
// CASE: When formatting, we need to check if the imageTransform package supports this specific format
if (!imageTransform.canTransformToFormat(format)) {
// transform not supported
return redirectToOriginal();
}
}
// CASE: when transforming is supported, we need to check if it is desired
// (e.g. it is not desired to resize SVGs when not formatting them to a different type)
if (!format && !imageTransform.shouldResizeFileExtension(requestUrlFileExtension)) {
return redirectToOriginal();
}
const storageInstance = storage.getStorage('images');
// CASE: unsupported storage adapter
if (typeof storageInstance.saveRaw !== 'function') {
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) => {
if (exists) {
return;
}
return imageSize.getOriginalImagePath(imagePath)
.then((storagePath) => {
return storageInstance.read({path: storagePath});
})
.then((originalImageBuffer) => {
if (originalImageBuffer.length <= 0) {
throw new NoContentError();
}
return imageTransform.resizeFromBuffer(originalImageBuffer, {withoutEnlargement: requestUrlFileExtension !== '.svg', ...imageDimensionConfig, format});
})
.then((resizedImageBuffer) => {
return storageInstance.saveRaw(resizedImageBuffer, req.url);
});
}).then(() => {
if (format) {
// File extension won't match the new format, so we need to update the Content-Type header manually here
// Express JS still uses an out of date mime package, which doesn't support avif
res.type(format === 'avif' ? 'image/avif' : format);
}
next();
}).catch(function (err) {
if (err.code === 'SHARP_INSTALLATION' || err.code === 'IMAGE_PROCESSING' || err.errorType === 'NoContentError') {
return redirectToOriginal();
}
if (err.errorType === 'NotFoundError') {
return next();
}
next(err);
});
};