Skip to content

Commit 8ab68cb

Browse files
committed
Implement center cropping in both canvas and sharp.js
1 parent f148be6 commit 8ab68cb

File tree

1 file changed

+127
-1
lines changed

1 file changed

+127
-1
lines changed

src/image_utils.js

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,14 @@ class CustomImage {
194194
* Resize the image to the given dimensions. This method uses the canvas API to perform the resizing.
195195
* @param {number} width - The width of the new image.
196196
* @param {number} height - The height of the new image.
197+
* @param {object} options - Additional options for resizing.
198+
* @param {string} [options.resample] - The resampling method to use. Can be one of `nearest`, `bilinear`, `bicubic`.
197199
* @returns {Promise<CustomImage>} - `this` to support chaining.
198200
*/
199-
async resize(width, height) {
201+
async resize(width, height, {
202+
// TODO: Use `resample`
203+
resample = 'bilinear',
204+
} = {}) {
200205
if (CanvasClass) {
201206
// Store number of channels before resizing
202207
let numChannels = this.channels;
@@ -235,6 +240,127 @@ class CustomImage {
235240

236241
}
237242

243+
async center_crop(crop_width, crop_height) {
244+
// If the image is already the desired size, return it
245+
if (this.width === crop_width && this.height === crop_height) {
246+
return this;
247+
}
248+
249+
// Determine bounds of the image in the new canvas
250+
let width_offset = (this.width - crop_width) / 2;
251+
let height_offset = (this.height - crop_height) / 2;
252+
253+
254+
if (CanvasClass) {
255+
// Store number of channels before resizing
256+
let numChannels = this.channels;
257+
258+
// Create canvas object for this image
259+
let canvas = this.toCanvas();
260+
261+
// Create a new canvas of the desired size. This is needed since if the
262+
// image is too small, we need to pad it with black pixels.
263+
const ctx = new CanvasClass(crop_width, crop_height).getContext('2d');
264+
265+
let sourceX = 0;
266+
let sourceY = 0;
267+
let destX = 0;
268+
let destY = 0;
269+
270+
if (width_offset >= 0) {
271+
sourceX = width_offset;
272+
} else {
273+
destX = -width_offset;
274+
}
275+
276+
if (height_offset >= 0) {
277+
sourceY = height_offset;
278+
} else {
279+
destY = -height_offset;
280+
}
281+
282+
// Draw image to context, cropping in the process
283+
ctx.drawImage(canvas,
284+
sourceX, sourceY, crop_width, crop_height,
285+
destX, destY, crop_width, crop_height
286+
);
287+
288+
// Create image from the resized data
289+
let resizedImage = new CustomImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
290+
291+
// Convert back so that image has the same number of channels as before
292+
return resizedImage.convert(numChannels);
293+
294+
} else {
295+
// Create sharp image from raw data
296+
let img = sharp(this.data, {
297+
raw: {
298+
width: this.width,
299+
height: this.height,
300+
channels: this.channels
301+
}
302+
});
303+
304+
if (width_offset >= 0 && height_offset >= 0) {
305+
// Cropped image lies entirely within the original image
306+
img = img.extract({
307+
left: Math.floor(width_offset),
308+
top: Math.floor(height_offset),
309+
width: crop_width,
310+
height: crop_height,
311+
})
312+
} else if (width_offset <= 0 && height_offset <= 0) {
313+
// Cropped image lies entirely outside the original image,
314+
// so we add padding
315+
let top = Math.floor(-height_offset);
316+
let left = Math.floor(-width_offset);
317+
img = img.extend({
318+
top: top,
319+
left: left,
320+
321+
// Ensures the resulting image has the desired dimensions
322+
right: crop_width - this.width - left,
323+
bottom: crop_height - this.height - top,
324+
});
325+
} else {
326+
// Cropped image lies partially outside the original image.
327+
// We first pad, then crop.
328+
329+
let y_padding = [0, 0];
330+
let y_extract = 0;
331+
if (height_offset < 0) {
332+
y_padding[0] = Math.floor(-height_offset);
333+
y_padding[1] = crop_height - this.height - y_padding[0];
334+
} else {
335+
y_extract = Math.floor(height_offset);
336+
}
337+
338+
let x_padding = [0, 0];
339+
let x_extract = 0;
340+
if (width_offset < 0) {
341+
x_padding[0] = Math.floor(-width_offset);
342+
x_padding[1] = crop_width - this.width - x_padding[0];
343+
} else {
344+
x_extract = Math.floor(width_offset);
345+
}
346+
347+
img = img.extend({
348+
top: y_padding[0],
349+
bottom: y_padding[1],
350+
left: x_padding[0],
351+
right: x_padding[1],
352+
}).extract({
353+
left: x_extract,
354+
top: y_extract,
355+
width: crop_width,
356+
height: crop_height,
357+
})
358+
}
359+
360+
return await loadImageFunction(img);
361+
}
362+
}
363+
238364
toCanvas() {
239365
// Clone, and convert data to RGBA before drawing to canvas.
240366
// This is because the canvas API only supports RGBA

0 commit comments

Comments
 (0)