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:
@@ -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.
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
|
||||
@@ -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
|
||||
/**
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user