Skip to content

Commit 56aded8

Browse files
authored
feat: add support for custom image size file names (#7634)
Add support for custom file names in images sizes ```ts { name: 'thumbnail', width: 400, height: 300, generateImageName: ({ height, sizeName, extension, width }) => { return `custom-${sizeName}-${height}-${width}.${extension}` }, } ```
1 parent 78dd6a2 commit 56aded8

File tree

8 files changed

+128
-9
lines changed

8 files changed

+128
-9
lines changed

docs/upload/overview.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
169169
image size. Use the `withoutEnlargement` prop to change this.
170170
</Banner>
171171

172+
#### Custom file name per size
173+
174+
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
175+
This function receives the original file name, the resize name, the extension, height and width as arguments.
176+
177+
```ts
178+
{
179+
name: 'thumbnail',
180+
width: 400,
181+
height: 300,
182+
generateImageName: ({ height, sizeName, extension, width }) => {
183+
return `custom-${sizeName}-${height}-${width}.${extension}`
184+
},
185+
}
186+
```
187+
172188
## Crop and Focal Point Selector
173189

174190
This feature is only available for image file types.

packages/payload/src/collections/config/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ export const createClientCollectionConfig = ({
5959
delete sanitized.upload.adminThumbnail
6060
delete sanitized.upload.externalFileHeaderFilter
6161
delete sanitized.upload.withMetadata
62+
63+
if ('imageSizes' in sanitized.upload && sanitized.upload.imageSizes.length) {
64+
sanitized.upload.imageSizes = sanitized.upload.imageSizes.map((size) => {
65+
const sanitizedSize = { ...size }
66+
if ('generateImageName' in sanitizedSize) delete sanitizedSize.generateImageName
67+
return sanitizedSize
68+
})
69+
}
6270
}
6371

6472
if ('auth' in sanitized && typeof sanitized.auth === 'object') {

packages/payload/src/uploads/imageResizer.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -408,15 +408,26 @@ export async function resizeAndTransformImageSizes({
408408

409409
const mimeInfo = await fileTypeFromBuffer(bufferData)
410410

411-
const imageNameWithDimensions = createImageName({
412-
extension: mimeInfo?.ext || sanitizedImage.ext,
413-
height: extractHeightFromImage({
414-
...originalImageMeta,
415-
height: bufferInfo.height,
416-
}),
417-
outputImageName: sanitizedImage.name,
418-
width: bufferInfo.width,
419-
})
411+
const imageNameWithDimensions = imageResizeConfig.generateImageName
412+
? imageResizeConfig.generateImageName({
413+
extension: mimeInfo?.ext || sanitizedImage.ext,
414+
height: extractHeightFromImage({
415+
...originalImageMeta,
416+
height: bufferInfo.height,
417+
}),
418+
originalName: sanitizedImage.name,
419+
sizeName: imageResizeConfig.name,
420+
width: bufferInfo.width,
421+
})
422+
: createImageName({
423+
extension: mimeInfo?.ext || sanitizedImage.ext,
424+
height: extractHeightFromImage({
425+
...originalImageMeta,
426+
height: bufferInfo.height,
427+
}),
428+
outputImageName: sanitizedImage.name,
429+
width: bufferInfo.width,
430+
})
420431

421432
const imagePath = `${staticPath}/${imageNameWithDimensions}`
422433

packages/payload/src/uploads/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,24 @@ export type ImageUploadFormatOptions = {
4949
*/
5050
export type ImageUploadTrimOptions = Parameters<Sharp['trim']>[0]
5151

52+
export type GenerateImageName = (args: {
53+
extension: string
54+
height: number
55+
originalName: string
56+
sizeName: string
57+
width: number
58+
}) => string
59+
5260
export type ImageSize = {
5361
/**
5462
* @deprecated prefer position
5563
*/
5664
crop?: string // comes from sharp package
5765
formatOptions?: ImageUploadFormatOptions
66+
/**
67+
* Generate a custom name for the file of this image size.
68+
*/
69+
generateImageName?: GenerateImageName
5870
name: string
5971
trimOptions?: ImageUploadTrimOptions
6072
/**

test/uploads/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Uploads2 } from './collections/Upload2/index.js'
1313
import {
1414
animatedTypeMedia,
1515
audioSlug,
16+
customFileNameMediaSlug,
1617
enlargeSlug,
1718
focalNoSizesSlug,
1819
mediaSlug,
@@ -505,6 +506,23 @@ export default buildConfigWithDefaults({
505506
trimOptions: 0,
506507
},
507508
},
509+
{
510+
slug: customFileNameMediaSlug,
511+
fields: [],
512+
upload: {
513+
imageSizes: [
514+
{
515+
name: 'custom',
516+
height: 500,
517+
width: 500,
518+
generateImageName: ({ extension, height, width, sizeName }) =>
519+
`${sizeName}-${width}x${height}.${extension}`,
520+
},
521+
],
522+
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
523+
staticDir: path.resolve(dirname, `./${customFileNameMediaSlug}`),
524+
},
525+
},
508526
{
509527
slug: unstoredMediaSlug,
510528
fields: [],

test/uploads/e2e.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
adminThumbnailSizeSlug,
2525
animatedTypeMedia,
2626
audioSlug,
27+
customFileNameMediaSlug,
2728
focalOnlySlug,
2829
mediaSlug,
2930
relationPreviewSlug,
@@ -51,6 +52,7 @@ let withMetadataURL: AdminUrlUtil
5152
let withoutMetadataURL: AdminUrlUtil
5253
let withOnlyJPEGMetadataURL: AdminUrlUtil
5354
let relationPreviewURL: AdminUrlUtil
55+
let customFileNameURL: AdminUrlUtil
5456

5557
describe('uploads', () => {
5658
let page: Page
@@ -74,6 +76,7 @@ describe('uploads', () => {
7476
withoutMetadataURL = new AdminUrlUtil(serverURL, withoutMetadataSlug)
7577
withOnlyJPEGMetadataURL = new AdminUrlUtil(serverURL, withOnlyJPEGMetadataSlug)
7678
relationPreviewURL = new AdminUrlUtil(serverURL, relationPreviewSlug)
79+
customFileNameURL = new AdminUrlUtil(serverURL, customFileNameMediaSlug)
7780

7881
const context = await browser.newContext()
7982
page = await context.newPage()
@@ -243,6 +246,25 @@ describe('uploads', () => {
243246
await expect(page.locator('.file-details img')).toBeVisible()
244247
})
245248

249+
test('should have custom file name for image size', async () => {
250+
await page.goto(customFileNameURL.create)
251+
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
252+
253+
await expect(page.locator('.file-field__upload .thumbnail img')).toBeVisible()
254+
255+
await saveDocAndAssert(page)
256+
257+
await expect(page.locator('.file-details img')).toBeVisible()
258+
259+
await page.locator('.file-field__previewSizes').click()
260+
261+
const renamedImageSizeFile = page
262+
.locator('.preview-sizes__list .preview-sizes__sizeOption')
263+
.nth(1)
264+
265+
await expect(renamedImageSizeFile).toContainText('custom-500x500.png')
266+
})
267+
246268
test('should show draft uploads in the relation list', async () => {
247269
await page.goto(relationURL.list)
248270
// from the list edit the first document

test/uploads/payload-types.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Config {
2727
enlarge: Enlarge;
2828
reduce: Reduce;
2929
'media-trim': MediaTrim;
30+
'custom-file-name-media': CustomFileNameMedia;
3031
'unstored-media': UnstoredMedia;
3132
'externally-served-media': ExternallyServedMedia;
3233
'uploads-1': Uploads1;
@@ -56,6 +57,7 @@ export interface Config {
5657
export interface UserAuthOperations {
5758
forgotPassword: {
5859
email: string;
60+
password: string;
5961
};
6062
login: {
6163
email: string;
@@ -67,6 +69,7 @@ export interface UserAuthOperations {
6769
};
6870
unlock: {
6971
email: string;
72+
password: string;
7073
};
7174
}
7275
/**
@@ -754,6 +757,34 @@ export interface MediaTrim {
754757
};
755758
};
756759
}
760+
/**
761+
* This interface was referenced by `Config`'s JSON-Schema
762+
* via the `definition` "custom-file-name-media".
763+
*/
764+
export interface CustomFileNameMedia {
765+
id: string;
766+
updatedAt: string;
767+
createdAt: string;
768+
url?: string | null;
769+
thumbnailURL?: string | null;
770+
filename?: string | null;
771+
mimeType?: string | null;
772+
filesize?: number | null;
773+
width?: number | null;
774+
height?: number | null;
775+
focalX?: number | null;
776+
focalY?: number | null;
777+
sizes?: {
778+
custom?: {
779+
url?: string | null;
780+
width?: number | null;
781+
height?: number | null;
782+
mimeType?: string | null;
783+
filesize?: number | null;
784+
filename?: string | null;
785+
};
786+
};
787+
}
757788
/**
758789
* This interface was referenced by `Config`'s JSON-Schema
759790
* via the `definition` "unstored-media".

test/uploads/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export const customUploadFieldSlug = 'custom-upload-field'
1818
export const withMetadataSlug = 'with-meta-data'
1919
export const withoutMetadataSlug = 'without-meta-data'
2020
export const withOnlyJPEGMetadataSlug = 'with-only-jpeg-meta-data'
21+
export const customFileNameMediaSlug = 'custom-file-name-media'

0 commit comments

Comments
 (0)