diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index f4e929e7d8..5a742e507d 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -168,6 +168,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options Use the `withoutEnlargement` prop to change this. +#### Custom file name per size + +Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image. +This function receives the original file name, the resize name, the extension, height and width as arguments. + +```ts +{ + name: 'thumbnail', + width: 400, + height: 300, + generateImageName: ({ height, sizeName, extension, width }) => { + return `custom-${sizeName}-${height}-${width}.${extension}` + }, +} +``` + ### Crop and Focal Point Selector This feature is only available for image file types. diff --git a/packages/payload/src/uploads/imageResizer.ts b/packages/payload/src/uploads/imageResizer.ts index 89a5aba7f0..5e09f2ee7f 100644 --- a/packages/payload/src/uploads/imageResizer.ts +++ b/packages/payload/src/uploads/imageResizer.ts @@ -431,15 +431,26 @@ export default async function resizeAndTransformImageSizes({ const mimeInfo = await fromBuffer(bufferData) - const imageNameWithDimensions = createImageName({ - extension: mimeInfo?.ext || sanitizedImage.ext, - height: extractHeightFromImage({ - ...originalImageMeta, - height: bufferInfo.height, - }), - outputImageName: sanitizedImage.name, - width: bufferInfo.width, - }) + const imageNameWithDimensions = imageResizeConfig.generateImageName + ? imageResizeConfig.generateImageName({ + extension: mimeInfo?.ext || sanitizedImage.ext, + height: extractHeightFromImage({ + ...originalImageMeta, + height: bufferInfo.height, + }), + originalName: sanitizedImage.name, + sizeName: imageResizeConfig.name, + width: bufferInfo.width, + }) + : createImageName({ + extension: mimeInfo?.ext || sanitizedImage.ext, + height: extractHeightFromImage({ + ...originalImageMeta, + height: bufferInfo.height, + }), + outputImageName: sanitizedImage.name, + width: bufferInfo.width, + }) const imagePath = `${staticPath}/${imageNameWithDimensions}` diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index cd35526620..fed5f35602 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -51,12 +51,24 @@ export type ImageUploadFormatOptions = { */ export type ImageUploadTrimOptions = Parameters[0] +export type GenerateImageName = (args: { + extension: string + height: number + originalName: string + sizeName: string + width: number +}) => string + export type ImageSize = Omit & { /** * @deprecated prefer position */ crop?: string // comes from sharp package formatOptions?: ImageUploadFormatOptions + /** + * Generate a custom name for the file of this image size. + */ + generateImageName?: GenerateImageName name: string trimOptions?: ImageUploadTrimOptions /** diff --git a/test/uploads/config.ts b/test/uploads/config.ts index 92a9c8eafd..4b5c1ee818 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -11,6 +11,7 @@ import { animatedTypeMedia, audioSlug, cropOnlySlug, + customFileNameMediaSlug, enlargeSlug, focalOnlySlug, globalWithMedia, @@ -203,6 +204,23 @@ export default buildConfigWithDefaults({ }, }, }, + { + slug: customFileNameMediaSlug, + fields: [], + upload: { + imageSizes: [ + { + name: 'custom', + height: 500, + width: 500, + generateImageName: ({ extension, height, width, sizeName }) => + `${sizeName}-${width}x${height}.${extension}`, + }, + ], + mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'], + staticDir: `./${customFileNameMediaSlug}`, + }, + }, { slug: cropOnlySlug, fields: [], diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index 2b6b15981d..caefa8b90e 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -24,6 +24,7 @@ import { withMetadataSlug, withOnlyJPEGMetadataSlug, withoutMetadataSlug, + customFileNameMediaSlug, } from './shared' const { beforeAll, describe } = test @@ -40,6 +41,7 @@ let withMetadataURL: AdminUrlUtil let withoutMetadataURL: AdminUrlUtil let withOnlyJPEGMetadataURL: AdminUrlUtil let relationPreviewURL: AdminUrlUtil +let customFileNameURL: AdminUrlUtil describe('uploads', () => { let page: Page @@ -62,6 +64,7 @@ describe('uploads', () => { withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug) withOnlyJPEGMetadataURL = new AdminUrlUtil(serverURL, withOnlyJPEGMetadataSlug) relationPreviewURL = new AdminUrlUtil(serverURL, relationPreviewSlug) + customFileNameURL = new AdminUrlUtil(serverURL, customFileNameMediaSlug) const context = await browser.newContext() page = await context.newPage() @@ -427,6 +430,25 @@ describe('uploads', () => { expect(webpMediaDoc.sizes.sizeThree.filesize).toEqual(211638) }) + test('should have custom file name for image size', async () => { + await page.goto(customFileNameURL.create) + await page.setInputFiles('input[type="file"]', path.resolve(__dirname, './image.png')) + + await expect(page.locator('.file-field__upload .thumbnail img')).toBeVisible() + + await saveDocAndAssert(page) + + await expect(page.locator('.file-details img')).toBeVisible() + + await page.locator('.file-field__previewSizes').click() + + const renamedImageSizeFile = page + .locator('.preview-sizes__list .preview-sizes__sizeOption') + .nth(1) + + await expect(renamedImageSizeFile).toContainText('custom-500x500.png') + }) + describe('image manipulation', () => { test('should crop image correctly', async () => { const positions = { diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index 2af7e65013..64e77b3eb5 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -15,3 +15,4 @@ export const animatedTypeMedia = 'animated-type-media' export const withMetadataSlug = 'with-meta-data' export const withoutMetadataSlug = 'without-meta-data' export const withOnlyJPEGMetadataSlug = 'with-only-jpeg-meta-data' +export const customFileNameMediaSlug = 'custom-file-name-media'