feat: add support for sharp resize options (#2844)

* feat(ImageResize): add support for resize options

* fix(ImageUpload): reuse name for accidental duplicate

* fix(ImageResize): e2e tests for added media size

* chore: simplify fileExists method

* fix: typo

* feat(ImageResize): update name to be more transparent

* fix: use fileExists in file removal

* improve names, comments and clarity of needsResize function

* fix: jsDoc params

* fix: incorrect needsResize condition and add failing test case

* chore: improve comment

* fix: merge conflict error

---------

Co-authored-by: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com>
This commit is contained in:
Josef Bredreck
2023-08-01 15:20:50 +02:00
committed by GitHub
parent 5ef20e3440
commit 144bb81721
9 changed files with 509 additions and 170 deletions

View File

@@ -13,6 +13,10 @@ export const relationSlug = 'relation';
export const audioSlug = 'audio';
export const enlargeSlug = 'enlarge';
export const reduceSlug = 'reduce';
export const adminThumbnailSlug = 'admin-thumbnail';
const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js');
@@ -92,7 +96,14 @@ export default buildConfigWithDefaults({
upload: {
staticURL: '/media',
staticDir: './media',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml', 'audio/mpeg'],
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
resizeOptions: {
width: 1280,
height: 720,
@@ -128,6 +139,11 @@ export default buildConfigWithDefaults({
height: undefined,
formatOptions: { format: 'jpg', options: { quality: 90 } },
},
{
name: 'accidentalSameSize',
width: 320,
height: 80,
},
{
name: 'tablet',
width: 640,
@@ -149,6 +165,99 @@ export default buildConfigWithDefaults({
},
fields: [],
},
{
slug: enlargeSlug,
upload: {
staticURL: '/enlarge',
staticDir: './media/enlarge',
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
withoutEnlargement: false,
},
{
name: 'resizedLarger',
width: 640,
height: 480,
withoutEnlargement: false,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
},
{
name: 'widthLowerHeightLarger',
width: 300,
height: 300,
fit: 'contain',
},
],
},
fields: [],
},
{
slug: reduceSlug,
upload: {
staticURL: '/reduce',
staticDir: './media/reduce',
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
withoutReduction: true,
fit: 'contain',
},
{
name: 'resizedLarger',
width: 640,
height: 480,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
// Why fit `contain` should also be set to https://github.com/lovell/sharp/issues/3595
withoutReduction: true,
fit: 'contain',
},
],
},
fields: [],
},
{
slug: 'media-trim',
upload: {
@@ -205,7 +314,7 @@ export default buildConfigWithDefaults({
],
onInit: async (payload) => {
const uploadsDir = path.resolve(__dirname, './media');
removeFiles(uploadsDir);
removeFiles(path.normalize(uploadsDir));
await payload.create({
collection: 'users',

View File

@@ -107,13 +107,16 @@ describe('uploads', () => {
await expect(maintainedImageSizeWithNewFormatMeta).toContainText('image/jpeg');
await expect(maintainedImageSizeWithNewFormatMeta).toContainText('1600x1600');
const tabletMeta = page.locator('.file-details__sizes .file-meta').nth(4);
const sameSizeMeta = page.locator('.file-details__sizes .file-meta').nth(4);
await expect(sameSizeMeta).toContainText('320x80');
const tabletMeta = page.locator('.file-details__sizes .file-meta').nth(5);
await expect(tabletMeta).toContainText('640x480');
const mobileMeta = page.locator('.file-details__sizes .file-meta').nth(5);
const mobileMeta = page.locator('.file-details__sizes .file-meta').nth(6);
await expect(mobileMeta).toContainText('320x240');
const iconMeta = page.locator('.file-details__sizes .file-meta').nth(6);
const iconMeta = page.locator('.file-details__sizes .file-meta').nth(7);
await expect(iconMeta).toContainText('16x16');
});

View File

@@ -1,20 +1,20 @@
import FormData from 'form-data';
import fs from 'fs';
import path from 'path';
import FormData from 'form-data';
import { promisify } from 'util';
import { initPayloadTest } from '../helpers/configHelpers';
import { RESTClient } from '../helpers/rest';
import configPromise, { mediaSlug, relationSlug } from './config';
import payload from '../../src';
import getFileByPath from '../../src/uploads/getFileByPath';
import { initPayloadTest } from '../helpers/configHelpers';
import { RESTClient } from '../helpers/rest';
import configPromise, { enlargeSlug, mediaSlug, reduceSlug, relationSlug } from './config';
const stat = promisify(fs.stat);
require('isomorphic-fetch');
let client;
describe('Collections - Uploads', () => {
let client: RESTClient;
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
const config = await configPromise;
@@ -32,27 +32,31 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
const { sizes } = doc;
const expectedPath = path.join(__dirname, './media');
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.maintainedAspectRatio.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.tablet.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.mobile.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.icon.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true);
expect(
await fileExists(path.join(expectedPath, sizes.maintainedAspectRatio.filename)),
).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.tablet.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.mobile.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.icon.filename))).toBe(true);
// Check api response
expect(doc.mimeType).toEqual('image/png');
expect(doc.sizes.maintainedAspectRatio.url).toContain('/media/image');
expect(doc.sizes.maintainedAspectRatio.url).toContain('.png');
expect(doc.sizes.maintainedAspectRatio.width).toEqual(1024);
expect(doc.sizes.maintainedAspectRatio.height).toEqual(1024);
expect(doc.sizes).toHaveProperty('tablet');
expect(doc.sizes).toHaveProperty('mobile');
expect(doc.sizes).toHaveProperty('icon');
expect(sizes.maintainedAspectRatio.url).toContain('/media/image');
expect(sizes.maintainedAspectRatio.url).toContain('.png');
expect(sizes.maintainedAspectRatio.width).toEqual(1024);
expect(sizes.maintainedAspectRatio.height).toEqual(1024);
expect(sizes).toHaveProperty('tablet');
expect(sizes).toHaveProperty('mobile');
expect(sizes).toHaveProperty('icon');
});
it('creates from form data given an svg', async () => {
@@ -63,7 +67,6 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
@@ -87,15 +90,16 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
const expectedPath = path.join(__dirname, './media');
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', 'small-640x480.png'))).toBe(false);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.icon.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, 'small-640x480.png'))).toBe(false);
expect(await fileExists(path.join(expectedPath, doc.sizes.icon.filename))).toBe(true);
// Check api response
expect(doc.sizes.tablet.filename).toBeNull();
@@ -110,14 +114,15 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
const expectedPath = path.join(__dirname, './media');
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.tablet.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, doc.sizes.tablet.filename))).toBe(true);
// Check api response
expect(doc.filename).toContain('.png');
@@ -138,7 +143,6 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
@@ -149,6 +153,119 @@ describe('Collections - Uploads', () => {
// Check api response
expect(doc.filename).toBeDefined();
});
it('should enlarge images if resize options `withoutEnlargement` is set to false', async () => {
const small = await getFileByPath(path.resolve(__dirname, './small.png'));
const result = await payload.create({
collection: enlargeSlug,
data: {},
file: small,
});
expect(result).toBeTruthy();
const { sizes } = result;
const expectedPath = path.join(__dirname, './media/enlarge');
// Check for files
expect(await fileExists(path.join(expectedPath, small.name))).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.resizedLarger.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.resizedSmaller.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, sizes.accidentalSameSize.filename))).toBe(
true,
);
expect(await fileExists(path.join(expectedPath, sizes.sameSizeWithNewFormat.filename))).toBe(
true,
);
// Check api response
expect(sizes.sameSizeWithNewFormat.mimeType).toBe('image/jpeg');
expect(sizes.sameSizeWithNewFormat.filename).toBe('small-320x80.jpg');
expect(sizes.resizedLarger.mimeType).toBe('image/png');
expect(sizes.resizedLarger.filename).toBe('small-640x480.png');
expect(sizes.resizedSmaller.mimeType).toBe('image/png');
expect(sizes.resizedSmaller.filename).toBe('small-180x50.png');
expect(sizes.accidentalSameSize.mimeType).toBe('image/png');
expect(sizes.accidentalSameSize.filename).toBe('small-320x80.png');
await payload.delete({
collection: enlargeSlug,
id: result.id,
});
});
// This test makes sure that the image resizing is not prevented if only one dimension is larger (due to payload preventing enlargement by default)
it('should resize images if one desired dimension is smaller and the other is larger', async () => {
const small = await getFileByPath(path.resolve(__dirname, './small.png'));
const result = await payload.create({
collection: enlargeSlug,
data: {},
file: small,
});
expect(result).toBeTruthy();
const { sizes } = result;
const expectedPath = path.join(__dirname, './media/enlarge');
// Check for files
expect(await fileExists(path.join(expectedPath, sizes.widthLowerHeightLarger.filename))).toBe(
true,
);
// Check api response
expect(sizes.widthLowerHeightLarger.mimeType).toBe('image/png');
expect(sizes.widthLowerHeightLarger.filename).toBe('small-300x300.png');
await payload.delete({
collection: enlargeSlug,
id: result.id,
});
});
it('should not reduce images if resize options `withoutReduction` is set to true', async () => {
const formData = new FormData();
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')));
const small = await getFileByPath(path.resolve(__dirname, './small.png'));
const result = await payload.create({
collection: reduceSlug,
data: {},
file: small,
});
expect(result).toBeTruthy();
const { sizes } = result;
const expectedPath = path.join(__dirname, './media/reduce');
// Check for files
expect(await fileExists(path.join(expectedPath, small.name))).toBe(true);
expect(await fileExists(path.join(expectedPath, 'small-640x480.png'))).toBe(false);
expect(await fileExists(path.join(expectedPath, 'small-180x50.png'))).toBe(false);
expect(await fileExists(path.join(expectedPath, sizes.accidentalSameSize.filename))).toBe(
true,
);
expect(await fileExists(path.join(expectedPath, sizes.sameSizeWithNewFormat.filename))).toBe(
true,
);
// Check api response
expect(sizes.sameSizeWithNewFormat.mimeType).toBe('image/jpeg');
expect(sizes.sameSizeWithNewFormat.filename).toBe('small-320x80.jpg');
expect(sizes.resizedLarger.mimeType).toBeNull();
expect(sizes.resizedLarger.filename).toBeNull();
expect(sizes.accidentalSameSize.mimeType).toBe('image/png');
expect(sizes.resizedSmaller.filename).toBe('small-320x80.png');
expect(sizes.accidentalSameSize.mimeType).toBe('image/png');
expect(sizes.accidentalSameSize.filename).toBe('small-320x80.png');
});
});
it('update', async () => {
@@ -168,17 +285,17 @@ describe('Collections - Uploads', () => {
const { status } = await client.update({
id: mediaDoc.id,
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(200);
const expectedPath = path.join(__dirname, './media');
// Check that previously existing files were removed
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', mediaDoc.sizes.icon.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(true);
});
it('update - update many', async () => {
@@ -201,17 +318,17 @@ describe('Collections - Uploads', () => {
where: {
id: { equals: mediaDoc.id },
},
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(200);
const expectedPath = path.join(__dirname, './media');
// Check that previously existing files were removed
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', mediaDoc.sizes.icon.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(true);
});
it('should remove existing media on re-upload', async () => {
@@ -226,8 +343,10 @@ describe('Collections - Uploads', () => {
file,
});
const expectedPath = path.join(__dirname, './media');
// Check that the temp file was created
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true);
// Replace the temp file with a new one
const newFilePath = path.resolve(__dirname, './temp-renamed.png');
@@ -242,8 +361,8 @@ describe('Collections - Uploads', () => {
});
// Check that the replacement file was created and the old one was removed
expect(await fileExists(path.join(__dirname, './media', updatedMediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(false);
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false);
});
it('should remove existing media on re-upload - update many', async () => {
@@ -258,8 +377,10 @@ describe('Collections - Uploads', () => {
file,
});
const expectedPath = path.join(__dirname, './media');
// Check that the temp file was created
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(true);
// Replace the temp file with a new one
const newFilePath = path.resolve(__dirname, './temp-renamed.png');
@@ -276,8 +397,8 @@ describe('Collections - Uploads', () => {
});
// Check that the replacement file was created and the old one was removed
expect(await fileExists(path.join(__dirname, './media', updatedMediaDoc.docs[0].filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', mediaDoc.filename))).toBe(false);
expect(await fileExists(path.join(expectedPath, updatedMediaDoc.docs[0].filename))).toBe(true);
expect(await fileExists(path.join(expectedPath, mediaDoc.filename))).toBe(false);
});
it('should remove extra sizes on update', async () => {
@@ -394,10 +515,10 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
const { status } = await client.delete(doc.id, {
id: doc.id,
auth: true,
});
@@ -414,7 +535,6 @@ describe('Collections - Uploads', () => {
file: true,
data: formData,
auth: true,
headers: {},
});
const { errors } = await client.deleteMany({