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:
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user