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}`
  },
}
```
This commit is contained in:
Paul
2024-08-12 12:25:20 -06:00
committed by GitHub
parent 78dd6a2d5b
commit 56aded8507
8 changed files with 128 additions and 9 deletions

View File

@@ -169,6 +169,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
image size. Use the `withoutEnlargement` prop to change this.
</Banner>
#### 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.

View File

@@ -59,6 +59,14 @@ export const createClientCollectionConfig = ({
delete sanitized.upload.adminThumbnail
delete sanitized.upload.externalFileHeaderFilter
delete sanitized.upload.withMetadata
if ('imageSizes' in sanitized.upload && sanitized.upload.imageSizes.length) {
sanitized.upload.imageSizes = sanitized.upload.imageSizes.map((size) => {
const sanitizedSize = { ...size }
if ('generateImageName' in sanitizedSize) delete sanitizedSize.generateImageName
return sanitizedSize
})
}
}
if ('auth' in sanitized && typeof sanitized.auth === 'object') {

View File

@@ -408,15 +408,26 @@ export async function resizeAndTransformImageSizes({
const mimeInfo = await fileTypeFromBuffer(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}`

View File

@@ -49,12 +49,24 @@ export type ImageUploadFormatOptions = {
*/
export type ImageUploadTrimOptions = Parameters<Sharp['trim']>[0]
export type GenerateImageName = (args: {
extension: string
height: number
originalName: string
sizeName: string
width: number
}) => string
export type ImageSize = {
/**
* @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
/**

View File

@@ -13,6 +13,7 @@ import { Uploads2 } from './collections/Upload2/index.js'
import {
animatedTypeMedia,
audioSlug,
customFileNameMediaSlug,
enlargeSlug,
focalNoSizesSlug,
mediaSlug,
@@ -505,6 +506,23 @@ export default buildConfigWithDefaults({
trimOptions: 0,
},
},
{
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: path.resolve(dirname, `./${customFileNameMediaSlug}`),
},
},
{
slug: unstoredMediaSlug,
fields: [],

View File

@@ -24,6 +24,7 @@ import {
adminThumbnailSizeSlug,
animatedTypeMedia,
audioSlug,
customFileNameMediaSlug,
focalOnlySlug,
mediaSlug,
relationPreviewSlug,
@@ -51,6 +52,7 @@ let withMetadataURL: AdminUrlUtil
let withoutMetadataURL: AdminUrlUtil
let withOnlyJPEGMetadataURL: AdminUrlUtil
let relationPreviewURL: AdminUrlUtil
let customFileNameURL: AdminUrlUtil
describe('uploads', () => {
let page: Page
@@ -74,6 +76,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()
@@ -243,6 +246,25 @@ describe('uploads', () => {
await expect(page.locator('.file-details img')).toBeVisible()
})
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')
})
test('should show draft uploads in the relation list', async () => {
await page.goto(relationURL.list)
// from the list edit the first document

View File

@@ -27,6 +27,7 @@ export interface Config {
enlarge: Enlarge;
reduce: Reduce;
'media-trim': MediaTrim;
'custom-file-name-media': CustomFileNameMedia;
'unstored-media': UnstoredMedia;
'externally-served-media': ExternallyServedMedia;
'uploads-1': Uploads1;
@@ -56,6 +57,7 @@ export interface Config {
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
@@ -67,6 +69,7 @@ export interface UserAuthOperations {
};
unlock: {
email: string;
password: string;
};
}
/**
@@ -754,6 +757,34 @@ export interface MediaTrim {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-file-name-media".
*/
export interface CustomFileNameMedia {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
custom?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "unstored-media".

View File

@@ -18,3 +18,4 @@ export const customUploadFieldSlug = 'custom-upload-field'
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'