-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added creation of custom image sizes on request #10184
Changes from all commits
26d2417
5f3e346
20c78da
52fd709
274fc7e
2b23205
80d5fb8
79f2371
9d3cdce
ddbc78d
6adbb2d
7e791ed
515e23f
e9d67e7
c2ee4ea
9186c3e
3a4e3f0
ad6d41a
10884ee
008a387
58b3d8a
46bddcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,4 +61,27 @@ const process = (options = {}) => { | |
}); | ||
}; | ||
|
||
const resizeImage = (originalBuffer, {width, height} = {}) => { | ||
const sharp = require('sharp'); | ||
return sharp(originalBuffer) | ||
.resize(width, height, { | ||
// CASE: dont make the image bigger than it was | ||
withoutEnlargement: true | ||
}) | ||
// CASE: Automatically remove metadata and rotate based on the orientation. | ||
.rotate() | ||
.toBuffer() | ||
.then((resizedBuffer) => { | ||
return resizedBuffer.length < originalBuffer.length ? resizedBuffer : originalBuffer; | ||
}); | ||
}; | ||
|
||
module.exports.process = process; | ||
module.exports.safeResizeImage = (buffer, options) => { | ||
try { | ||
require('sharp'); | ||
return resizeImage(buffer, options); | ||
} catch (e) { | ||
return Promise.resolve(buffer); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI design hint: libraries should return an error, callers need to decide what to do if an error is thrown/returned. Callers can ignore errors, callers can return errors to the next chain. Whatever... Furthermore: If sharp isn't installed or resizeImage throws an error, we would save the same buffer stream as the resized image? Is that wanted? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The lib returns a promise, which will reject if an error occurs, so the consumer of this is totally capable of catching any errors. resizeImage returns promise, so errors from there will be handled by the library consumer - if sharp doesn't exist then yeah - we will save the resizedImage - i think this is a reasonable fallback for the functionality. @ErisDS what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IMO a library should always return a specific/custom/handled error. Otherwise, multiple/different callers need to handle an error from a third party library or from node. Plus the callers would need to wrap the error in case they want to forward the error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah - I think a specific error is good, I'm not sure how specific we can be on top of sharps errors though. I noticed in this file we use InternalServer errors but that doesn't seem correct as the library has nothing to do with servers it just deals with image buffers and the filesystem 🦐 Do you think we should create some image manipulation errors to use in the module? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There are many definitions of what an internat server error is.
In general: We did not create custom error types for custom situations in the past, because
There is no need to block this PR, but it's important that all of us have a common thinking of how libraries are designed, how error handling works etc :)
This just needs answering 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merging as is - will do a bit of clean up later |
||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const path = require('path'); | ||
const image = require('../../../../lib/image'); | ||
const storage = require('../../../../adapters/storage'); | ||
const activeTheme = require('../../../../services/themes/active'); | ||
|
||
const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//; | ||
This comment was marked as abuse.
Sorry, something went wrong. |
||
|
||
module.exports = function (req, res, next) { | ||
if (!SIZE_PATH_REGEX.test(req.url)) { | ||
return next(); | ||
} | ||
|
||
const [sizeImageDir, requestedDimension] = req.url.match(SIZE_PATH_REGEX); | ||
const redirectToOriginal = () => { | ||
const url = req.originalUrl.replace(`/size/${requestedDimension}`, ''); | ||
return res.redirect(url); | ||
}; | ||
|
||
const imageSizes = activeTheme.get().config('image_sizes'); | ||
// CASE: no image_sizes config | ||
if (!imageSizes) { | ||
return redirectToOriginal(); | ||
} | ||
|
||
const imageDimensions = Object.keys(imageSizes).reduce((dimensions, size) => { | ||
allouis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const {width, height} = imageSizes[size]; | ||
const dimension = (width ? 'w' + width : '') + (height ? 'h' + height : ''); | ||
return Object.assign({ | ||
[dimension]: imageSizes[size] | ||
}, dimensions); | ||
}, {}); | ||
|
||
const imageDimensionConfig = imageDimensions[requestedDimension]; | ||
// CASE: unknown dimension | ||
if (!imageDimensionConfig || (!imageDimensionConfig.width && !imageDimensionConfig.height)) { | ||
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
Sorry, something went wrong.
This comment was marked as abuse.
Sorry, something went wrong. |
||
return redirectToOriginal(); | ||
} | ||
|
||
const storageInstance = storage.getStorage(); | ||
// CASE: unsupported storage adapter but theme is using custom image_sizes | ||
if (typeof storageInstance.saveRaw !== 'function') { | ||
return redirectToOriginal(); | ||
} | ||
|
||
storageInstance.exists(req.url).then((exists) => { | ||
if (exists) { | ||
return; | ||
} | ||
|
||
const originalImagePath = path.relative(sizeImageDir, req.url); | ||
|
||
return storageInstance.read({path: originalImagePath}) | ||
.then((originalImageBuffer) => { | ||
naz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return image.manipulator.safeResizeImage(originalImageBuffer, imageDimensionConfig); | ||
}) | ||
.then((resizedImageBuffer) => { | ||
return storageInstance.saveRaw(resizedImageBuffer, req.url); | ||
}); | ||
}).then(() => { | ||
next(); | ||
}).catch(next); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
module.exports = { | ||
get normalize() { | ||
return require('./normalize'); | ||
}, | ||
get handleImageSizes() { | ||
return require('./handle-image-sizes'); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
const should = require('should'); | ||
const handleImageSizes = require('../../../../../server/web/shared/middlewares/image/handle-image-sizes.js'); | ||
|
||
// @TODO make these tests lovely and non specific to implementation | ||
describe('handleImageSizes middleware', function () { | ||
it('calls next immediately if the url does not match /size/something/', function (done) { | ||
const fakeReq = { | ||
url: '/size/something' | ||
}; | ||
// CASE: second thing middleware does is try to match to a regex | ||
naz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) { | ||
const fakeReq = { | ||
url: '/url/whatever/' | ||
}; | ||
// 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) { | ||
const fakeReq = { | ||
url: '/size//' | ||
}; | ||
// 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(); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this separation of a check logic for sharp being installed, maybe we should unify this handling for both
safeResizeImage
&process
to keep it DRY? 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that idea, but I think can be done in a separate PR so that we can refactor them to share some sharp code too☺️