Skip to content

Instantly share code, notes, and snippets.

@elliott-w
Last active April 24, 2025 21:47
Show Gist options
  • Save elliott-w/19676d9373d79ee20b9195601dc45808 to your computer and use it in GitHub Desktop.
Save elliott-w/19676d9373d79ee20b9195601dc45808 to your computer and use it in GitHub Desktop.
Payload CMS Image Aspect Ratios Pugin
import type {
CollectionBeforeOperationHook,
FileData,
Plugin,
UploadCollectionSlug,
} from 'payload'
type AspectRatios = Record<string, number>
type Collections = Partial<Record<UploadCollectionSlug, AspectRatios>>
interface ImageAspectRatiosPluginArgs {
collections: Collections
enable?: boolean
}
export function imageAspectRatiosPlugin({
collections,
enable = true,
}: ImageAspectRatiosPluginArgs): Plugin {
return (config) => {
if (!enable) {
return config
}
const uploadCollections = collections
const uploadCollectionSlugs = Object.keys(collections)
config.collections = (config.collections || []).map((c) => {
const slug = c.slug as UploadCollectionSlug
if (uploadCollectionSlugs.includes(c.slug) && slug in uploadCollections) {
return {
...c,
hooks: {
...(c.hooks || {}),
beforeOperation: [
createBeforeOperationHook(uploadCollections[slug]!),
...(c.hooks?.beforeOperation || []),
],
},
}
} else {
return c
}
})
return config
}
}
type Dimensions = {
height: number
width: number
}
const createBeforeOperationHook = (aspectRatios: AspectRatios) => {
const beforeOperationHook: CollectionBeforeOperationHook = async ({
req,
args,
collection,
operation,
}) => {
if (operation == 'update' || operation == 'create') {
let originalDimensions: Dimensions | undefined = undefined
if (req.file) {
const sharp = req.payload.config.sharp
const metadata = await sharp(req.file.data).metadata()
if (metadata.width && metadata.height) {
originalDimensions = {
width: metadata.width,
height: metadata.height,
}
}
} else {
const duplicateFromID: string | number | undefined =
args.duplicateFromID
if (duplicateFromID) {
const data = await req.payload.findByID({
collection: collection.slug as UploadCollectionSlug,
id: duplicateFromID,
select: {
width: true,
height: true,
},
})
originalDimensions = {
width: data.width!,
height: data.height!,
}
} else {
const data = args.data as FileData
originalDimensions = {
width: data.width,
height: data.height,
}
}
}
if (originalDimensions) {
const originalWidth = originalDimensions.width
const originalHeight = originalDimensions.height
const originalAspectRatio = originalWidth / originalHeight
collection.upload.imageSizes = (collection.upload.imageSizes || []).map(
(size) => {
if (size.name in aspectRatios) {
const desiredAspectRatio = aspectRatios[size.name]!
const dimensions =
desiredAspectRatio > originalAspectRatio
? {
width: originalWidth,
height: Math.round(originalWidth / desiredAspectRatio),
}
: {
width: Math.round(originalHeight * desiredAspectRatio),
height: originalHeight,
}
return {
...size,
...dimensions,
}
}
return size
}
)
}
}
return args
}
return beforeOperationHook
}
export default buildConfig({
collections: [{
slug: 'media',
fields: [],
upload: {
adminThumbnail: 'thumbnail',
imageSizes: [
{
name: 'thumbnail',
width: 300,
height: 300,
},
{
name: 'landscape',
// Width and height can be anything, even omitted/undefined since
// they always get overwritten by the plugin on a per-image basis
width: 1,
height: 1,
},
{
name: 'portrait',
},
],
},
}],
plugins: [
imageAspectRatiosPlugin({
collections: {
media: {
portrait: 9 / 16,
landscape: 16 / 9,
},
},
}),
],
sharp,
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment