feat: store focal point on uploads (#6436)

Store focal point data on uploads as `focalX` and `focalY`

Addresses https://github.com/payloadcms/payload/discussions/4082

Mirrors #6364 for beta branch.
This commit is contained in:
Elliot DeNolf
2024-05-20 15:57:52 -04:00
committed by GitHub
parent fa7cc376d1
commit 36fda30c61
20 changed files with 390 additions and 127 deletions

View File

@@ -152,7 +152,7 @@ type FetchAPIFileUpload = (args: {
request: Request
}) => Promise<FetchAPIFileUploadResponse>
export const fetchAPIFileUpload: FetchAPIFileUpload = async ({ options, request }) => {
const uploadOptions = { ...DEFAULT_OPTIONS, ...options }
const uploadOptions: FetchAPIFileUploadOptions = { ...DEFAULT_OPTIONS, ...options }
if (!isEligibleRequest(request)) {
debugLog(uploadOptions, 'Request is not eligible for file upload!')
return {

View File

@@ -121,6 +121,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
collection,
config,
data,
operation: 'create',
overwriteExistingFiles,
req,
throwOnMissingFile:

View File

@@ -156,6 +156,7 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
collection,
config,
data: bulkUpdateData,
operation: 'update',
overwriteExistingFiles,
req,
throwOnMissingFile: false,

View File

@@ -147,6 +147,7 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
collection,
config,
data,
operation: 'update',
overwriteExistingFiles,
req,
throwOnMissingFile: false,

View File

@@ -12,7 +12,7 @@ export { traverseFields as beforeValidateTraverseFields } from '../fields/hooks/
export { formatFilesize } from '../uploads/formatFilesize.js'
export { default as isImage } from '../uploads/isImage.js'
export { isImage } from '../uploads/isImage.js'
export { combineMerge } from '../utilities/combineMerge.js'
export {

View File

@@ -7,19 +7,6 @@ import type { GeneratedTypes } from '../index.js'
import type { validOperators } from './constants.js'
export type { Payload as Payload } from '../index.js'
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
}
export type CustomPayloadRequestProperties = {
context: RequestContext
/** The locale that should be used for a field when it is not translated to the requested locale */

View File

@@ -1,3 +1,3 @@
export default function canResizeImage(mimeType: string): boolean {
export function canResizeImage(mimeType: string): boolean {
return ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff'].indexOf(mimeType) > -1
}

View File

@@ -2,7 +2,7 @@ export const percentToPixel = (value, dimension) => {
return Math.floor((parseFloat(value) / 100) * dimension)
}
export default async function cropImage({ cropData, dimensions, file, sharp }) {
export async function cropImage({ cropData, dimensions, file, sharp }) {
try {
const { height, width, x, y } = cropData

View File

@@ -9,22 +9,24 @@ import sanitize from 'sanitize-filename'
import type { Collection } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequestWithData } from '../types/index.js'
import type { FileData, FileToSave, ProbedImageSize } from './types.js'
import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types.js'
import { FileRetrievalError, FileUploadError, MissingFile } from '../errors/index.js'
import canResizeImage from './canResizeImage.js'
import cropImage from './cropImage.js'
import { canResizeImage } from './canResizeImage.js'
import { cropImage } from './cropImage.js'
import { getExternalFile } from './getExternalFile.js'
import { getFileByPath } from './getFileByPath.js'
import { getImageSize } from './getImageSize.js'
import getSafeFileName from './getSafeFilename.js'
import resizeAndTransformImageSizes from './imageResizer.js'
import isImage from './isImage.js'
import { getSafeFileName } from './getSafeFilename.js'
import { resizeAndTransformImageSizes } from './imageResizer.js'
import { isImage } from './isImage.js'
type Args<T> = {
collection: Collection
config: SanitizedConfig
data: T
operation: 'create' | 'update'
originalDoc?: T
overwriteExistingFiles?: boolean
req: PayloadRequestWithData
throwOnMissingFile?: boolean
@@ -38,6 +40,8 @@ type Result<T> = Promise<{
export const generateFileData = async <T>({
collection: { config: collectionConfig },
data,
operation,
originalDoc,
overwriteExistingFiles,
req,
throwOnMissingFile,
@@ -53,10 +57,22 @@ export const generateFileData = async <T>({
let file = req.file
const uploadEdits = req.query['uploadEdits'] || {}
const uploadEdits = parseUploadEditsFromReqOrIncomingData({
data,
operation,
originalDoc,
req,
})
const { disableLocalStorage, formatOptions, imageSizes, resizeOptions, staticDir, trimOptions } =
collectionConfig.upload
const {
disableLocalStorage,
focalPoint: focalPointEnabled = true,
formatOptions,
imageSizes,
resizeOptions,
staticDir,
trimOptions,
} = collectionConfig.upload
const staticPath = staticDir
@@ -228,9 +244,9 @@ export const generateFileData = async <T>({
}
}
if (Array.isArray(imageSizes) && fileSupportsResize && sharp) {
if (fileSupportsResize && (Array.isArray(imageSizes) || focalPointEnabled !== false)) {
req.payloadUploadSizes = {}
const { sizeData, sizesToSave } = await resizeAndTransformImageSizes({
const { focalPoint, sizeData, sizesToSave } = await resizeAndTransformImageSizes({
config: collectionConfig,
dimensions: !cropData
? dimensions
@@ -245,13 +261,16 @@ export const generateFileData = async <T>({
savedFilename: fsSafeName || file.name,
sharp,
staticPath,
uploadEdits,
})
fileData.sizes = sizeData
fileData.focalX = focalPoint?.x
fileData.focalY = focalPoint?.y
filesToSave.push(...sizesToSave)
}
} catch (err) {
console.error(err)
req.payload.logger.error(err)
throw new FileUploadError(req.t)
}
@@ -265,3 +284,50 @@ export const generateFileData = async <T>({
files: filesToSave,
}
}
/**
* Parse upload edits from req or incoming data
*/
function parseUploadEditsFromReqOrIncomingData(args: {
data: unknown
operation: 'create' | 'update'
originalDoc: unknown
req: PayloadRequestWithData
}): UploadEdits {
const { data, operation, originalDoc, req } = args
// Get intended focal point change from query string or incoming data
const {
uploadEdits = {},
}: {
uploadEdits?: UploadEdits
} = req.query || {}
if (uploadEdits.focalPoint) return uploadEdits
const incomingData = data as FileData
const origDoc = originalDoc as FileData
// If no change in focal point, return undefined.
// This prevents a refocal operation triggered from admin, because it always sends the focal point.
if (origDoc && incomingData.focalX === origDoc.focalX && incomingData.focalY === origDoc.focalY) {
return undefined
}
if (incomingData?.focalX && incomingData?.focalY) {
uploadEdits.focalPoint = {
x: incomingData.focalX,
y: incomingData.focalY,
}
return uploadEdits
}
// If no focal point is set, default to center
if (operation === 'create') {
uploadEdits.focalPoint = {
x: 50,
y: 50,
}
}
return uploadEdits
}

View File

@@ -149,6 +149,25 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
height,
]
// Add focal point fields if not disabled
if (
uploadOptions.focalPoint !== false ||
uploadOptions.imageSizes ||
uploadOptions.resizeOptions
) {
uploadFields = uploadFields.concat(
['focalX', 'focalY'].map((name) => {
return {
name,
type: 'number',
admin: {
hidden: true,
},
}
}),
)
}
if (uploadOptions.mimeTypes) {
mimeType.validate = mimeTypeValidator(uploadOptions.mimeTypes)
}

View File

@@ -29,7 +29,7 @@ type Args = {
staticPath: string
}
async function getSafeFileName({
export async function getSafeFileName({
collectionSlug,
desiredFilename,
req,
@@ -51,5 +51,3 @@ async function getSafeFileName({
}
return modifiedFilename
}
export default getSafeFileName

View File

@@ -8,8 +8,15 @@ import sanitize from 'sanitize-filename'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SharpDependency } from '../config/types.js'
import type { PayloadRequestWithData, UploadEdits } from '../types/index.js'
import type { FileSize, FileSizes, FileToSave, ImageSize, ProbedImageSize } from './types.js'
import type { PayloadRequestWithData } from '../types/index.js'
import type {
FileSize,
FileSizes,
FileToSave,
ImageSize,
ProbedImageSize,
UploadEdits,
} from './types.js'
import { isNumber } from '../utilities/isNumber.js'
import fileExists from './fileExists.js'
@@ -19,18 +26,16 @@ type ResizeArgs = {
dimensions: ProbedImageSize
file: PayloadRequestWithData['file']
mimeType: string
req: PayloadRequestWithData & {
query?: {
uploadEdits?: UploadEdits
}
}
req: PayloadRequestWithData
savedFilename: string
sharp: SharpDependency
sharp?: SharpDependency
staticPath: string
uploadEdits?: UploadEdits
}
/** Result from resizing and transforming the requested image sizes */
type ImageSizesResult = {
focalPoint?: UploadEdits['focalPoint']
sizeData: FileSizes
sizesToSave: FileToSave[]
}
@@ -71,6 +76,16 @@ const createImageName = (
extension: string,
) => `${outputImageName}-${width}x${height}.${extension}`
type CreateResultArgs = {
filename?: FileSize['filename']
filesize?: FileSize['filesize']
height?: FileSize['height']
mimeType?: FileSize['mimeType']
name: string
sizesToSave?: FileToSave[]
width?: FileSize['width']
}
/**
* Create the result object for the image resize operation based on the
* provided parameters. If the name is not provided, an empty result object
@@ -85,26 +100,28 @@ const createImageName = (
* @param sizesToSave - the sizes to save
* @returns the result object
*/
const createResult = (
name: string,
filename: FileSize['filename'] = null,
width: FileSize['width'] = null,
height: FileSize['height'] = null,
filesize: FileSize['filesize'] = null,
mimeType: FileSize['mimeType'] = null,
sizesToSave: FileToSave[] = [],
): ImageSizesResult => ({
sizeData: {
[name]: {
filename,
filesize,
height,
mimeType,
width,
const createResult = ({
name,
filename = null,
filesize = null,
height = null,
mimeType = null,
sizesToSave = [],
width = null,
}: CreateResultArgs): ImageSizesResult => {
return {
sizeData: {
[name]: {
filename,
filesize,
height,
mimeType,
width,
},
},
},
sizesToSave,
})
sizesToSave,
}
}
/**
* Check if the image needs to be resized according to the requested dimensions
@@ -208,7 +225,7 @@ const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
* @param resizeConfig - the resize config
* @returns the result of the resize operation(s)
*/
export default async function resizeAndTransformImageSizes({
export async function resizeAndTransformImageSizes({
config,
dimensions,
file,
@@ -217,10 +234,27 @@ export default async function resizeAndTransformImageSizes({
savedFilename,
sharp,
staticPath,
uploadEdits,
}: ResizeArgs): Promise<ImageSizesResult> {
const { imageSizes } = config.upload
// Noting to resize here so return as early as possible
if (!imageSizes) return { sizeData: {}, sizesToSave: [] }
const { focalPoint: focalPointEnabled = true, imageSizes } = config.upload
// Focal point adjustments
const incomingFocalPoint = uploadEdits.focalPoint
? {
x: isNumber(uploadEdits.focalPoint.x) ? Math.round(uploadEdits.focalPoint.x) : 50,
y: isNumber(uploadEdits.focalPoint.y) ? Math.round(uploadEdits.focalPoint.y) : 50,
}
: undefined
const defaultResult: ImageSizesResult = {
...(focalPointEnabled && incomingFocalPoint && { focalPoint: incomingFocalPoint }),
sizeData: {},
sizesToSave: [],
}
if (!imageSizes || !sharp) {
return defaultResult
}
const sharpBase = sharp(file.tempFilePath || file.data).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
@@ -232,16 +266,13 @@ export default async function resizeAndTransformImageSizes({
// skipped COMPLETELY and thus will not be included in the resulting images.
// All further format/trim options will thus be skipped as well.
if (preventResize(imageResizeConfig, dimensions)) {
return createResult(imageResizeConfig.name)
return createResult({ name: imageResizeConfig.name })
}
const imageToResize = sharpBase.clone()
let resized = imageToResize
if (
req.query?.uploadEdits?.focalPoint &&
applyPayloadAdjustments(imageResizeConfig, dimensions)
) {
if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) {
const { height: resizeHeight, width: resizeWidth } = imageResizeConfig
const resizeAspectRatio = resizeWidth / resizeHeight
const originalAspectRatio = dimensions.width / dimensions.height
@@ -254,27 +285,17 @@ export default async function resizeAndTransformImageSizes({
})
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
// Focal point adjustments
const focalPoint = {
x: isNumber(req.query.uploadEdits.focalPoint?.x)
? req.query.uploadEdits.focalPoint.x
: 50,
y: isNumber(req.query.uploadEdits.focalPoint?.y)
? req.query.uploadEdits.focalPoint.y
: 50,
}
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
const leftFocalEdge = Math.round(
scaledImageInfo.width * (focalPoint.x / 100) - safeResizeWidth / 2,
scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2,
)
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
const safeResizeHeight = resizeHeight ?? scaledImageInfo.height
const maxOffsetY = scaledImageInfo.height - safeResizeHeight
const topFocalEdge = Math.round(
scaledImageInfo.height * (focalPoint.y / 100) - safeResizeHeight / 2,
scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2,
)
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
@@ -306,7 +327,9 @@ export default async function resizeAndTransformImageSizes({
const sanitizedImage = getSanitizedImageData(savedFilename)
req.payloadUploadSizes[imageResizeConfig.name] = bufferData
if (req.payloadUploadSizes) {
req.payloadUploadSizes[imageResizeConfig.name] = bufferData
}
const mimeInfo = await fromBuffer(bufferData)
@@ -327,15 +350,15 @@ export default async function resizeAndTransformImageSizes({
}
const { height, size, width } = bufferInfo
return createResult(
imageResizeConfig.name,
imageNameWithDimensions,
width,
return createResult({
name: imageResizeConfig.name,
filename: imageNameWithDimensions,
filesize: size,
height,
size,
mimeInfo?.mime || mimeType,
[{ buffer: bufferData, path: imagePath }],
)
mimeType: mimeInfo?.mime || mimeType,
sizesToSave: [{ buffer: bufferData, path: imagePath }],
width,
})
}),
)
@@ -345,6 +368,6 @@ export default async function resizeAndTransformImageSizes({
acc.sizesToSave.push(...result.sizesToSave)
return acc
},
{ sizeData: {}, sizesToSave: [] },
{ ...defaultResult },
)
}

View File

@@ -1,4 +1,4 @@
export default function isImage(mimeType: string): boolean {
export function isImage(mimeType: string): boolean {
return (
['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif'].indexOf(
mimeType,

View File

@@ -18,6 +18,8 @@ export type FileSizes = {
export type FileData = {
filename: string
filesize: number
focalX?: number
focalY?: number
height: number
mimeType: string
sizes: FileSizes
@@ -117,3 +119,16 @@ export type FileToSave = {
buffer: Buffer
path: string
}
export type UploadEdits = {
crop?: {
height?: number
width?: number
x?: number
y?: number
}
focalPoint?: {
x?: number
y?: number
}
}

View File

@@ -32,13 +32,12 @@ type FocalPosition = {
}
export type EditUploadProps = {
doc?: Data
fileName: string
fileSrc: string
imageCacheTag?: string
initialCrop?: CropType
initialFocalPoint?: FocalPosition
onSave?: ({ crop, pointPosition }: { crop: CropType; pointPosition: FocalPosition }) => void
onSave?: ({ crop, focalPosition }: { crop: CropType; focalPosition: FocalPosition }) => void
showCrop?: boolean
showFocalPoint?: boolean
}
@@ -51,11 +50,6 @@ const defaultCrop: CropType = {
y: 0,
}
const defaultPointPosition: FocalPosition = {
x: 50,
y: 50,
}
export const EditUpload: React.FC<EditUploadProps> = ({
fileName,
fileSrc,
@@ -76,8 +70,15 @@ export const EditUpload: React.FC<EditUploadProps> = ({
...initialCrop,
}))
const [pointPosition, setPointPosition] = useState<FocalPosition>(() => ({
...defaultPointPosition,
const defaultFocalPosition: FocalPosition = {
x: 50,
y: 50,
}
console.log({ initialFocalPoint })
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
...defaultFocalPosition,
...initialFocalPoint,
}))
const [checkBounds, setCheckBounds] = useState<boolean>(false)
@@ -103,10 +104,16 @@ export const EditUpload: React.FC<EditUploadProps> = ({
})
}
const fineTuneFocalPoint = ({ coordinate, value }: { coordinate: 'x' | 'y'; value: string }) => {
const fineTuneFocalPosition = ({
coordinate,
value,
}: {
coordinate: 'x' | 'y'
value: string
}) => {
const intValue = parseInt(value)
if (intValue >= 0 && intValue <= 100) {
setPointPosition((prevPosition) => ({ ...prevPosition, [coordinate]: intValue }))
setFocalPosition((prevPosition) => ({ ...prevPosition, [coordinate]: intValue }))
}
}
@@ -114,13 +121,13 @@ export const EditUpload: React.FC<EditUploadProps> = ({
if (typeof onSave === 'function')
onSave({
crop,
pointPosition,
focalPosition,
})
closeModal(editDrawerSlug)
}
const onDragEnd = React.useCallback(({ x, y }) => {
setPointPosition({ x, y })
setFocalPosition({ x, y })
setCheckBounds(false)
}, [])
@@ -133,7 +140,7 @@ export const EditUpload: React.FC<EditUploadProps> = ({
((boundsRect.left - containerRect.left + boundsRect.width / 2) / containerRect.width) * 100
const yCenter =
((boundsRect.top - containerRect.top + boundsRect.height / 2) / containerRect.height) * 100
setPointPosition({ x: xCenter, y: yCenter })
setFocalPosition({ x: xCenter, y: yCenter })
}
const fileSrcToUse = imageCacheTag ? `${fileSrc}?${imageCacheTag}` : fileSrc
@@ -209,7 +216,7 @@ export const EditUpload: React.FC<EditUploadProps> = ({
checkBounds={showCrop ? checkBounds : false}
className={`${baseClass}__focalPoint`}
containerRef={focalWrapRef}
initialPosition={pointPosition}
initialPosition={focalPosition}
onDragEnd={onDragEnd}
setCheckBounds={showCrop ? setCheckBounds : false}
>
@@ -280,13 +287,13 @@ export const EditUpload: React.FC<EditUploadProps> = ({
<div className={`${baseClass}__inputsWrap`}>
<Input
name="X %"
onChange={(value) => fineTuneFocalPoint({ coordinate: 'x', value })}
value={pointPosition.x.toFixed(0)}
onChange={(value) => fineTuneFocalPosition({ coordinate: 'x', value })}
value={focalPosition.x.toFixed(0)}
/>
<Input
name="Y %"
onChange={(value) => fineTuneFocalPoint({ coordinate: 'y', value })}
value={pointPosition.y.toFixed(0)}
onChange={(value) => fineTuneFocalPosition({ coordinate: 'y', value })}
value={focalPosition.y.toFixed(0)}
/>
</div>
</div>

View File

@@ -101,7 +101,7 @@ export const Upload: React.FC<UploadProps> = (props) => {
}, [setValue])
const onEditsSave = React.useCallback(
({ crop, pointPosition }) => {
({ crop, focalPosition }) => {
setCrop({
x: crop.x || 0,
y: crop.y || 0,
@@ -122,10 +122,10 @@ export const Upload: React.FC<UploadProps> = (props) => {
type: 'SET',
params: {
uploadEdits:
crop || pointPosition
crop || focalPosition
? {
crop: crop || null,
focalPoint: pointPosition ? pointPosition : null,
focalPoint: focalPosition ? focalPosition : null,
}
: null,
},
@@ -164,10 +164,12 @@ export const Upload: React.FC<UploadProps> = (props) => {
const hasImageSizes = uploadConfig?.imageSizes?.length > 0
const hasResizeOptions = Boolean(uploadConfig?.resizeOptions)
// Explicity check if set to true, default is undefined
const focalPointEnabled = uploadConfig?.focalPoint === true
const { crop: showCrop = true, focalPoint = true } = uploadConfig
const showFocalPoint = focalPoint && (hasImageSizes || hasResizeOptions)
const showFocalPoint = focalPoint && (hasImageSizes || hasResizeOptions || focalPointEnabled)
const lastSubmittedTime = submitted ? new Date().toISOString() : null
@@ -234,14 +236,13 @@ export const Upload: React.FC<UploadProps> = (props) => {
{(value || doc.filename) && (
<Drawer Header={null} slug={editDrawerSlug}>
<EditUpload
doc={doc || undefined}
fileName={value?.name || doc?.filename}
fileSrc={fileSrc || doc?.url}
imageCacheTag={lastSubmittedTime}
initialCrop={formQueryParams?.uploadEdits?.crop ?? {}}
initialFocalPoint={{
x: formQueryParams?.uploadEdits?.focalPoint.x || 0,
y: formQueryParams?.uploadEdits?.focalPoint.y || 0,
x: formQueryParams?.uploadEdits?.focalPoint.x || doc.focalX || 50,
y: formQueryParams?.uploadEdits?.focalPoint.y || doc.focalY || 50,
}}
onSave={onEditsSave}
showCrop={showCrop}

View File

@@ -12,6 +12,7 @@ import { Uploads2 } from './collections/Upload2/index.js'
import {
audioSlug,
enlargeSlug,
focalNoSizesSlug,
mediaSlug,
reduceSlug,
relationSlug,
@@ -183,6 +184,16 @@ export default buildConfigWithDefaults({
],
},
},
{
slug: focalNoSizesSlug,
fields: [],
upload: {
crop: false,
focalPoint: true,
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './focal-no-sizes',
},
},
{
slug: mediaSlug,
fields: [],

View File

@@ -14,6 +14,8 @@ import configPromise from './config.js'
import { createStreamableFile } from './createStreamableFile.js'
import {
enlargeSlug,
focalNoSizesSlug,
focalOnlySlug,
mediaSlug,
reduceSlug,
relationSlug,
@@ -73,6 +75,8 @@ describe('Collections - Uploads', () => {
// Check api response
expect(doc.mimeType).toEqual('image/png')
expect(doc.focalX).toEqual(50)
expect(doc.focalY).toEqual(50)
expect(sizes.maintainedAspectRatio.url).toContain('/api/media/file/image')
expect(sizes.maintainedAspectRatio.url).toContain('.png')
expect(sizes.maintainedAspectRatio.width).toEqual(1024)
@@ -286,7 +290,6 @@ describe('Collections - Uploads', () => {
expect(await fileExists(path.join(expectedPath, mediaDoc.sizes.icon.filename))).toBe(false)
})
})
describe('delete', () => {
it('should remove related files when deleting by ID', async () => {
const formData = new FormData()
@@ -527,6 +530,88 @@ describe('Collections - Uploads', () => {
})
})
describe('focal point', () => {
let file
beforeAll(async () => {
// Create image
const filePath = path.resolve(dirname, './image.png')
file = await getFileByPath(filePath)
file.name = 'focal.png'
})
it('should be able to set focal point through local API', async () => {
const doc = await payload.create({
collection: focalOnlySlug,
data: {
focalX: 5,
focalY: 5,
},
file,
})
expect(doc.focalX).toEqual(5)
expect(doc.focalY).toEqual(5)
const updatedFocal = await payload.update({
collection: focalOnlySlug,
id: doc.id,
data: {
focalX: 10,
focalY: 10,
},
})
expect(updatedFocal.focalX).toEqual(10)
expect(updatedFocal.focalY).toEqual(10)
const updateWithoutFocal = await payload.update({
collection: focalOnlySlug,
id: doc.id,
data: {},
})
// Expect focal point to be the same
expect(updateWithoutFocal.focalX).toEqual(10)
expect(updateWithoutFocal.focalY).toEqual(10)
})
it('should default focal point to 50, 50', async () => {
const doc = await payload.create({
collection: focalOnlySlug,
data: {
// No focal point
},
file,
})
expect(doc.focalX).toEqual(50)
expect(doc.focalY).toEqual(50)
const updateWithoutFocal = await payload.update({
collection: focalOnlySlug,
id: doc.id,
data: {},
})
expect(updateWithoutFocal.focalX).toEqual(50)
expect(updateWithoutFocal.focalY).toEqual(50)
})
it('should set focal point even if no sizes defined', async () => {
const doc = await payload.create({
collection: focalNoSizesSlug, // config without sizes
data: {
// No focal point
},
file,
})
expect(doc.focalX).toEqual(50)
expect(doc.focalY).toEqual(50)
})
})
describe('Image Manipulation', () => {
it('should enlarge images if resize options `withoutEnlargement` is set to false', async () => {
const small = await getFileByPath(path.resolve(dirname, './small.png'))

View File

@@ -15,6 +15,7 @@ export interface Config {
'object-fit': ObjectFit;
'crop-only': CropOnly;
'focal-only': FocalOnly;
'focal-no-sizes': FocalNoSize;
media: Media;
enlarge: Enlarge;
reduce: Reduce;
@@ -64,6 +65,8 @@ export interface Media {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
maintainedAspectRatio?: {
url?: string | null;
@@ -204,6 +207,8 @@ export interface Version {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -230,6 +235,8 @@ export interface GifResize {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
small?: {
url?: string | null;
@@ -264,6 +271,8 @@ export interface NoImageSize {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -280,6 +289,8 @@ export interface ObjectFit {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
fitContain?: {
url?: string | null;
@@ -330,6 +341,8 @@ export interface CropOnly {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
focalTest?: {
url?: string | null;
@@ -372,6 +385,8 @@ export interface FocalOnly {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
focalTest?: {
url?: string | null;
@@ -399,6 +414,24 @@ export interface FocalOnly {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "focal-no-sizes".
*/
export interface FocalNoSize {
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;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "enlarge".
@@ -414,6 +447,8 @@ export interface Enlarge {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
accidentalSameSize?: {
url?: string | null;
@@ -472,6 +507,8 @@ export interface Reduce {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
accidentalSameSize?: {
url?: string | null;
@@ -522,6 +559,8 @@ export interface MediaTrim {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
trimNumber?: {
url?: string | null;
@@ -564,6 +603,8 @@ export interface UnstoredMedia {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -580,6 +621,8 @@ export interface ExternallyServedMedia {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -612,6 +655,8 @@ export interface Uploads1 {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -629,6 +674,8 @@ export interface Uploads2 {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -645,6 +692,8 @@ export interface AdminThumbnailFunction {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -661,6 +710,8 @@ export interface AdminThumbnailSize {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
small?: {
url?: string | null;
@@ -695,6 +746,8 @@ export interface OptionalFile {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -711,6 +764,8 @@ export interface RequiredFile {
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -1,19 +1,12 @@
export const usersSlug = 'users'
export const mediaSlug = 'media'
export const relationSlug = 'relation'
export const audioSlug = 'audio'
export const enlargeSlug = 'enlarge'
export const focalNoSizesSlug = 'focal-no-sizes'
export const focalOnlySlug = 'focal-only'
export const reduceSlug = 'reduce'
export const adminThumbnailFunctionSlug = 'admin-thumbnail-function'
export const adminThumbnailSizeSlug = 'admin-thumbnail-size'
export const unstoredMediaSlug = 'unstored-media'
export const versionSlug = 'versions'