@humaan/payload-storage-imagekit 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,8 +9,10 @@ This plugin integrates `@payloadcms/plugin-cloud-storage` with ImageKit and prov
9
9
  - Required root folder with per-collection subfolders
10
10
  - Stored ImageKit references (`imagekit.fileId`, `imagekit.url`, `imagekit.thumbnailUrl`, `imagekit.isPrivate`)
11
11
  - `crop: false` for configured upload collections
12
+ - Opt-in `focalPoint` toggle for configured upload collections
12
13
  - Admin thumbnails using ImageKit transforms (`tr=w-300,h-300`)
13
14
  - Built-in thumbnail path for videos and PDFs in admin previews
15
+ - Browser-safe ImageKit transform helpers for frontend image rendering
14
16
  - Migration utility for copying collection files between ImageKit folders
15
17
 
16
18
  ## Installation
@@ -38,7 +40,7 @@ export default buildConfig({
38
40
  clientUploads: true,
39
41
  folder: '/payload-uploads',
40
42
  collections: {
41
- media: true,
43
+ media: { focalPoint: true },
42
44
  },
43
45
  privateKey: process.env.IMAGEKIT_PRIVATE_KEY || '',
44
46
  publicKey: process.env.IMAGEKIT_PUBLIC_KEY || '',
@@ -52,7 +54,7 @@ export default buildConfig({
52
54
 
53
55
  `imagekitStorage(options)` options:
54
56
 
55
- - `collections` (required): `Partial<Record<UploadCollectionSlug, true | { private?: boolean }>>`. Use `true` for a normal (public) collection, or `{ private: true }` to deliver the collection's files as ImageKit [private files](#private-files).
57
+ - `collections` (required): `Partial<Record<UploadCollectionSlug, true | { focalPoint?: boolean; private?: boolean }>>`. Use `true` for a normal (public) collection, `{ focalPoint: true }` to enable Payload's native focal-point picker for that collection, or `{ private: true }` to deliver the collection's files as ImageKit [private files](#private-files).
56
58
  - `folder` (required): root upload folder in ImageKit, e.g. `/payload-uploads`
57
59
  - `privateKey` (required): ImageKit private API key
58
60
  - `publicKey` (required): ImageKit public key
@@ -61,6 +63,7 @@ export default buildConfig({
61
63
  Duplicate filenames still follow Payload's normal safe-name behavior, so a second `pug.jpg` becomes `pug-1.jpg` instead of overwriting the first asset in ImageKit.
62
64
  - `signedUrlExpiresIn` (optional): expiry in seconds for the signed URLs generated for [private](#private-files) collections. Defaults to `900` (15 minutes). Signed URLs are generated fresh on every request, so a short window is safe.
63
65
  - `enabled` (optional): enable / disable plugin
66
+ - `focalPoint` (optional): broad default for Payload's native focal-point picker (`upload.focalPoint`) on every configured collection. Focal points are disabled by default for configured collections, so prefer the per-collection `collections.<slug>.focalPoint` option for mixed upload collections, e.g. enable it for `media` and leave it off for `documents`. When `true`, editors can place a focal point and `focalX`/`focalY` (0–100) are stored on each doc for focal-aware cropping at render time. A collection's own `upload.focalPoint` still takes precedence.
64
67
  - `alwaysInsertFields` (optional): pass-through to cloud storage plugin
65
68
  - `disablePayloadAccessControl` (optional): pass-through to cloud storage plugin
66
69
 
@@ -68,7 +71,62 @@ export default buildConfig({
68
71
 
69
72
  - `imagekitStorage` (primary)
70
73
  - `migrateFolder`
71
- - `payloadStorageImagekit` (alias of `imagekitStorage`)
74
+ - `payloadStorageImageKit` (alias of `imagekitStorage`)
75
+ - `@humaan/payload-storage-imagekit/image`:
76
+ - `buildImageKitTransformation`
77
+ - `withImageKitTransformation`
78
+ - `computeFocalCropRect`
79
+ - `getImageKitVideoSrc`
80
+ - `getImageKitThumbnailSrc`
81
+ - `getVideoPosterSrc`
82
+ - `srcParamsPayloadImageKit` (alias of `withImageKitTransformation`)
83
+
84
+ ## Frontend Image Transforms
85
+
86
+ Use the browser-safe `image` export in app image components to build ImageKit URLs from Payload upload data without importing the server plugin entry.
87
+
88
+ ```ts
89
+ import {
90
+ withImageKitTransformation,
91
+ } from '@humaan/payload-storage-imagekit/image'
92
+
93
+ const src = withImageKitTransformation(media.imagekit.url, {
94
+ focalX: media.focalX,
95
+ focalY: media.focalY,
96
+ height: 800,
97
+ originalHeight: media.height,
98
+ originalWidth: media.width,
99
+ quality: 80,
100
+ width: 1200,
101
+ })
102
+ ```
103
+
104
+ Provide `focalX`/`focalY` with the original image dimensions when you want the helper to compute a clamped `cm-extract` crop before resizing. This avoids ImageKit rejecting out-of-bounds coordinate crops.
105
+
106
+ Use `getImageKitVideoSrc` before adding video transforms when the stored ImageKit asset URL may not include a `.mp4` or `.mov` extension. It appends ImageKit's `/ik-video.mp4` optimization hint only when the URL lacks a supported video extension.
107
+
108
+ Use `getVideoPosterSrc` when you need ImageKit's generated `ik-thumbnail.jpg` URL for a video poster. It also applies the `/ik-video.mp4` hint first when the source URL lacks a `.mp4` or `.mov` extension. Use `getImageKitThumbnailSrc` for non-video thumbnail URLs such as PDF previews.
109
+
110
+ ```ts
111
+ import {
112
+ getImageKitVideoSrc,
113
+ getVideoPosterSrc,
114
+ withImageKitTransformation,
115
+ } from '@humaan/payload-storage-imagekit/image'
116
+
117
+ const video = withImageKitTransformation(getImageKitVideoSrc(media.imagekit.url), {
118
+ height: 720,
119
+ quality: 80,
120
+ vc: 'h264',
121
+ width: 1280,
122
+ })
123
+
124
+ const poster = withImageKitTransformation(getVideoPosterSrc(video), {
125
+ height: 720,
126
+ quality: 80,
127
+ width: 1280,
128
+ })
129
+ ```
72
130
 
73
131
  ## Private Files
74
132
 
@@ -93,11 +151,11 @@ imagekitStorage({
93
151
  How it works:
94
152
 
95
153
  - **Upload:** files in a private collection are uploaded with `isPrivateFile: true` (both server-side and client-side uploads).
96
- - **Per-document status:** each document records its actual ImageKit privacy at upload time in `imagekit.isPrivate` (a read-only "Private file" checkbox in the admin). A file's privacy on ImageKit is fixed when it is uploaded, so this stored flag — not the collection's current setting — is the **source of truth** for delivery. This means existing files keep working if you later flip a collection's `private` setting. (Documents uploaded before this flag existed fall back to the collection setting.)
154
+ - **Per-document status:** each document records its actual ImageKit privacy at upload time in `imagekit.isPrivate` (hidden from the admin form). A file's privacy on ImageKit is fixed when it is uploaded, so this stored flag — not the collection's current setting — is the **source of truth** for delivery. This means existing files keep working if you later flip a collection's `private` setting. (Documents uploaded before this flag existed fall back to the collection setting.)
97
155
  - **Delivery:** files are served through Payload's own file route (`/api/<collection>/file/<filename>`), which enforces the collection's `access.read` control and then redirects to a freshly signed, expiring ImageKit URL. Nothing signed is ever persisted to the database.
98
- - **Admin thumbnails** are routed through the same Payload file route so they sign on demand and respect access control (including ImageKit's `ik-thumbnail.jpg` previews for videos and PDFs).
99
- - **Admin indicators:** private collections get a note in their admin description (visible in the list and edit views), and each document shows the read-only `imagekit.isPrivate` checkbox.
100
- - **Signed URL preview:** because the stored `imagekit.url` is the unsigned URL (which 401s for private files), private documents also expose a read-only virtual `imagekit.signedUrl` field — a freshly signed, time-limited URL computed on each read (never persisted). Use it to reach the file directly on ImageKit. The access-control-gated alternative is the document's top-level `url` (the Payload file route).
156
+ - **Admin thumbnails** are routed through the same Payload file route so they sign on demand and respect access control (including ImageKit's `ik-thumbnail.jpg` previews for videos and PDFs, with `/ik-video.mp4` applied first for videos without a `.mp4` or `.mov` extension).
157
+ - **Admin indicators:** private collections get a note in their admin description (visible in the list and edit views). The ImageKit metadata group is hidden while uploading and shown after a file has been stored, with `imagekit.url` visible as a read-only field and `imagekit.isPrivate` shown as read-only when the stored file is private.
158
+ - **Signed URLs:** because the stored `imagekit.url` is the unsigned URL (which 401s for private files), private documents also expose a virtual `imagekit.signedUrl` field — a freshly signed, time-limited URL computed on each read (never persisted). Use it through Payload's API to reach the file directly on ImageKit. The access-control-gated alternative is the document's top-level `url` (the Payload file route).
101
159
  - **Access control is required.** Because signing happens at request time behind Payload access control, `disablePayloadAccessControl` cannot be combined with private collections — doing so throws at startup. Set per-collection `access.read` rules to control who can read each file.
102
160
 
103
161
  ## Folder Migration
@@ -110,8 +168,9 @@ Behavior:
110
168
  - Creates destination folders automatically if they do not exist
111
169
  - Optional database update via Payload local API
112
170
  - Dry-run is enabled by default (`dryRun: true`)
113
- - Copies from the configured source folder path first, with stored `imagekit.url` as a fallback only when the source file cannot be found
114
- - Handles [private files](#private-files): mark a collection `{ private: true }` and migrated documents are stored with `imagekit.isPrivate: true` and a signed-URL admin thumbnail. The fast server-side copy preserves the source file's ImageKit privacy; when the copy falls back to re-uploading from the source URL, the source URL is signed (so a private source can be read) and the file is re-uploaded with `isPrivateFile: true`. Because ImageKit privacy is fixed at upload, migrating assumes the destination collection's privacy matches the source — copying a public file into a `{ private: true }` collection keeps the file public on ImageKit.
171
+ - Resolves each source asset by the stored `imagekit.fileId`, then copies the ImageKit-returned file path into the destination folder. This avoids mismatches between Payload's `filename` field and ImageKit's normalized file names.
172
+ - Reruns are idempotent, including dry-runs: if a document's stored `imagekit.fileId` already resolves to a file in the destination collection folder, that document is skipped.
173
+ - Handles [private files](#private-files): mark a collection `{ private: true }` and migrated documents are stored with `imagekit.isPrivate: true`. The fast server-side copy preserves the source file's ImageKit privacy; when the copy falls back to re-uploading from the source URL, the source URL is signed (so a private source can be read) and the file is re-uploaded with `isPrivateFile: true`. Because ImageKit privacy is fixed at upload, migrating assumes the destination collection's privacy matches the source — copying a public file into a `{ private: true }` collection keeps the file public on ImageKit.
115
174
 
116
175
  Example:
117
176
 
@@ -164,7 +223,8 @@ DRY_RUN=false pnpx tsx --env-file=../.env imagekit-migration.ts
164
223
  - Deletion prefers stored `imagekit.fileId`, with path lookup fallback.
165
224
  - Admin thumbnail behavior:
166
225
  - Images: `<imagekit.url>?tr=w-300,h-300`
167
- - Videos / PDFs: `<imagekit.url>/ik-thumbnail.jpg?tr=w-300,h-300`
226
+ - Videos: `<imagekit.url>/ik-thumbnail.jpg?tr=w-300,h-300` or `<imagekit.url>/ik-video.mp4/ik-thumbnail.jpg?tr=w-300,h-300` when the video URL has no `.mp4` or `.mov` extension
227
+ - PDFs: `<imagekit.url>/ik-thumbnail.jpg?tr=w-300,h-300`
168
228
 
169
229
  ## Development
170
230
 
@@ -3,7 +3,10 @@ export declare const getImageKitUploadAuthPath: (collectionSlug: string) => `/${
3
3
  export type ImageKitClientUploadContext = {
4
4
  fileId?: string;
5
5
  filePath?: string;
6
+ fileType?: string;
7
+ mimeType?: string;
6
8
  name?: string;
9
+ size?: number;
7
10
  thumbnailUrl?: string;
8
11
  url?: string;
9
12
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["export const imageKitUploadAuthBasePath = '/imagekit-upload-auth'\n\nexport const getImageKitUploadAuthPath = (collectionSlug: string): `/${string}` =>\n `${imageKitUploadAuthBasePath}/${collectionSlug}`\n\nexport type ImageKitClientUploadContext = {\n fileId?: string\n filePath?: string\n name?: string\n thumbnailUrl?: string\n url?: string\n}\n\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n isPrivate?: boolean\n publicKey: string\n}\n\nexport const isImageKitClientUploadContext = (\n value: unknown,\n): value is ImageKitClientUploadContext => {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n return 'fileId' in value || 'thumbnailUrl' in value || 'url' in value\n}\n"],"names":["imageKitUploadAuthBasePath","getImageKitUploadAuthPath","collectionSlug","isImageKitClientUploadContext","value"],"mappings":"AAAA,OAAO,MAAMA,6BAA6B,wBAAuB;AAEjE,OAAO,MAAMC,4BAA4B,CAACC,iBACxC,GAAGF,2BAA2B,CAAC,EAAEE,gBAAgB,CAAA;AAkBnD,OAAO,MAAMC,gCAAgC,CAC3CC;IAEA,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU;QACvC,OAAO;IACT;IAEA,OAAO,YAAYA,SAAS,kBAAkBA,SAAS,SAASA;AAClE,EAAC"}
1
+ {"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["export const imageKitUploadAuthBasePath = '/imagekit-upload-auth'\n\nexport const getImageKitUploadAuthPath = (collectionSlug: string): `/${string}` =>\n `${imageKitUploadAuthBasePath}/${collectionSlug}`\n\nexport type ImageKitClientUploadContext = {\n fileId?: string\n filePath?: string\n fileType?: string\n mimeType?: string\n name?: string\n size?: number\n thumbnailUrl?: string\n url?: string\n}\n\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n isPrivate?: boolean\n publicKey: string\n}\n\nexport const isImageKitClientUploadContext = (\n value: unknown,\n): value is ImageKitClientUploadContext => {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n return 'fileId' in value || 'thumbnailUrl' in value || 'url' in value\n}\n"],"names":["imageKitUploadAuthBasePath","getImageKitUploadAuthPath","collectionSlug","isImageKitClientUploadContext","value"],"mappings":"AAAA,OAAO,MAAMA,6BAA6B,wBAAuB;AAEjE,OAAO,MAAMC,4BAA4B,CAACC,iBACxC,GAAGF,2BAA2B,CAAC,EAAEE,gBAAgB,CAAA;AAqBnD,OAAO,MAAMC,gCAAgC,CAC3CC;IAEA,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU;QACvC,OAAO;IACT;IAEA,OAAO,YAAYA,SAAS,kBAAkBA,SAAS,SAASA;AAClE,EAAC"}
@@ -63,6 +63,9 @@ export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, isPriv
63
63
  name: uploadedFilename,
64
64
  fileId: uploaded.fileId,
65
65
  filePath: uploaded.filePath,
66
+ fileType: uploaded.fileType,
67
+ mimeType: file.type,
68
+ size: typeof uploaded.size === 'number' ? uploaded.size : file.size,
66
69
  thumbnailUrl: uploaded.thumbnailUrl,
67
70
  url: uploaded.url
68
71
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useUploadHandlers } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect } from 'react'\n\nimport type { ImageKitClientUploadContext, ImageKitUploadHandlerProps } from '../clientUploads.js'\n\ntype ImageKitAuthResponse = {\n expire: number\n fileName?: string\n signature: string\n token: string\n}\n\ntype ImageKitUploadResponse = {\n message?: string\n name?: string\n} & ImageKitClientUploadContext\n\nconst uploadAPIURL = 'https://upload.imagekit.io/api/v1/files/upload'\n\nconst readErrorMessage = async (response: Response, fallback: string): Promise<string> => {\n try {\n const result = (await response.json()) as { message?: string }\n\n if (typeof result.message === 'string' && result.message.trim().length > 0) {\n return result.message\n }\n } catch {\n // Ignore non-JSON responses and fall back to the default message.\n }\n\n return fallback\n}\n\nexport const ImageKitUploadHandler = ({\n authPath,\n collectionSlug,\n folder,\n isPrivate,\n publicKey,\n}: ImageKitUploadHandlerProps) => {\n const { config } = useConfig()\n const { setUploadHandler } = useUploadHandlers()\n\n useEffect(() => {\n const authURL = formatAdminURL({\n apiRoute: config.routes.api,\n path: authPath,\n })\n\n setUploadHandler({\n collectionSlug,\n handler: async ({ file, updateFilename }) => {\n const authRequestURL = new URL(authURL, window.location.origin)\n\n authRequestURL.searchParams.set('filename', file.name)\n\n const authResponse = await fetch(authRequestURL, {\n credentials: 'include',\n })\n\n if (!authResponse.ok) {\n throw new Error(\n await readErrorMessage(authResponse, 'Failed to authenticate direct ImageKit upload.'),\n )\n }\n\n const auth = (await authResponse.json()) as ImageKitAuthResponse\n const safeFilename =\n typeof auth.fileName === 'string' && auth.fileName.length > 0 ? auth.fileName : file.name\n const formData = new FormData()\n\n formData.set('expire', String(auth.expire))\n formData.set('file', file)\n formData.set('fileName', safeFilename)\n formData.set('folder', folder)\n formData.set('publicKey', publicKey)\n formData.set('signature', auth.signature)\n formData.set('token', auth.token)\n formData.set('useUniqueFileName', 'false')\n\n if (isPrivate) {\n // ImageKit's upload signature only covers `token` + `expire`, so adding\n // this flag does not invalidate the signed auth parameters.\n formData.set('isPrivateFile', 'true')\n }\n\n const uploadResponse = await fetch(uploadAPIURL, {\n body: formData,\n method: 'POST',\n })\n\n if (!uploadResponse.ok) {\n throw new Error(\n await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.'),\n )\n }\n\n const uploaded = (await uploadResponse.json()) as ImageKitUploadResponse\n const uploadedFilename =\n typeof uploaded.name === 'string' && uploaded.name.length > 0\n ? uploaded.name\n : safeFilename\n\n updateFilename(uploadedFilename)\n\n return {\n name: uploadedFilename,\n fileId: uploaded.fileId,\n filePath: uploaded.filePath,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n },\n })\n }, [authPath, collectionSlug, config.routes.api, folder, isPrivate, publicKey, setUploadHandler])\n\n return null\n}\n"],"names":["useConfig","useUploadHandlers","formatAdminURL","useEffect","uploadAPIURL","readErrorMessage","response","fallback","result","json","message","trim","length","ImageKitUploadHandler","authPath","collectionSlug","folder","isPrivate","publicKey","config","setUploadHandler","authURL","apiRoute","routes","api","path","handler","file","updateFilename","authRequestURL","URL","window","location","origin","searchParams","set","name","authResponse","fetch","credentials","ok","Error","auth","safeFilename","fileName","formData","FormData","String","expire","signature","token","uploadResponse","body","method","uploaded","uploadedFilename","fileId","filePath","thumbnailUrl","url"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useUploadHandlers } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect } from 'react'\n\nimport type { ImageKitClientUploadContext, ImageKitUploadHandlerProps } from '../clientUploads.js'\n\ntype ImageKitAuthResponse = {\n expire: number\n fileName?: string\n signature: string\n token: string\n}\n\ntype ImageKitUploadResponse = {\n fileType?: string\n message?: string\n name?: string\n size?: number\n} & ImageKitClientUploadContext\n\nconst uploadAPIURL = 'https://upload.imagekit.io/api/v1/files/upload'\n\nconst readErrorMessage = async (response: Response, fallback: string): Promise<string> => {\n try {\n const result = (await response.json()) as { message?: string }\n\n if (typeof result.message === 'string' && result.message.trim().length > 0) {\n return result.message\n }\n } catch {\n // Ignore non-JSON responses and fall back to the default message.\n }\n\n return fallback\n}\n\nexport const ImageKitUploadHandler = ({\n authPath,\n collectionSlug,\n folder,\n isPrivate,\n publicKey,\n}: ImageKitUploadHandlerProps) => {\n const { config } = useConfig()\n const { setUploadHandler } = useUploadHandlers()\n\n useEffect(() => {\n const authURL = formatAdminURL({\n apiRoute: config.routes.api,\n path: authPath,\n })\n\n setUploadHandler({\n collectionSlug,\n handler: async ({ file, updateFilename }) => {\n const authRequestURL = new URL(authURL, window.location.origin)\n\n authRequestURL.searchParams.set('filename', file.name)\n\n const authResponse = await fetch(authRequestURL, {\n credentials: 'include',\n })\n\n if (!authResponse.ok) {\n throw new Error(\n await readErrorMessage(authResponse, 'Failed to authenticate direct ImageKit upload.'),\n )\n }\n\n const auth = (await authResponse.json()) as ImageKitAuthResponse\n const safeFilename =\n typeof auth.fileName === 'string' && auth.fileName.length > 0 ? auth.fileName : file.name\n const formData = new FormData()\n\n formData.set('expire', String(auth.expire))\n formData.set('file', file)\n formData.set('fileName', safeFilename)\n formData.set('folder', folder)\n formData.set('publicKey', publicKey)\n formData.set('signature', auth.signature)\n formData.set('token', auth.token)\n formData.set('useUniqueFileName', 'false')\n\n if (isPrivate) {\n // ImageKit's upload signature only covers `token` + `expire`, so adding\n // this flag does not invalidate the signed auth parameters.\n formData.set('isPrivateFile', 'true')\n }\n\n const uploadResponse = await fetch(uploadAPIURL, {\n body: formData,\n method: 'POST',\n })\n\n if (!uploadResponse.ok) {\n throw new Error(\n await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.'),\n )\n }\n\n const uploaded = (await uploadResponse.json()) as ImageKitUploadResponse\n const uploadedFilename =\n typeof uploaded.name === 'string' && uploaded.name.length > 0\n ? uploaded.name\n : safeFilename\n\n updateFilename(uploadedFilename)\n\n return {\n name: uploadedFilename,\n fileId: uploaded.fileId,\n filePath: uploaded.filePath,\n fileType: uploaded.fileType,\n mimeType: file.type,\n size: typeof uploaded.size === 'number' ? uploaded.size : file.size,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n },\n })\n }, [authPath, collectionSlug, config.routes.api, folder, isPrivate, publicKey, setUploadHandler])\n\n return null\n}\n"],"names":["useConfig","useUploadHandlers","formatAdminURL","useEffect","uploadAPIURL","readErrorMessage","response","fallback","result","json","message","trim","length","ImageKitUploadHandler","authPath","collectionSlug","folder","isPrivate","publicKey","config","setUploadHandler","authURL","apiRoute","routes","api","path","handler","file","updateFilename","authRequestURL","URL","window","location","origin","searchParams","set","name","authResponse","fetch","credentials","ok","Error","auth","safeFilename","fileName","formData","FormData","String","expire","signature","token","uploadResponse","body","method","uploaded","uploadedFilename","fileId","filePath","fileType","mimeType","type","size","thumbnailUrl","url"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ export declare const isVideoMimeType: (mimeType?: string) => boolean;
2
+ export declare const isPdfMimeType: (mimeType?: string) => boolean;
1
3
  export declare const isVideoOrPdfMimeType: (mimeType?: string) => boolean;
2
4
  export declare const getAdminThumbnailURL: (doc: unknown) => string;
3
5
  /**
@@ -1,25 +1,39 @@
1
- export const isVideoOrPdfMimeType = (mimeType)=>{
2
- const isVideo = mimeType?.startsWith('video');
1
+ import { getImageKitThumbnailSrc, getVideoPosterSrc, withImageKitTransformation } from './imageTransforms.js';
2
+ const getThumbnailTransform = (file)=>{
3
+ if (!isVideoOrPdfMimeType(file.mimeType) && typeof file.focalX === 'number' && typeof file.focalY === 'number' && typeof file.width === 'number' && typeof file.height === 'number') {
4
+ return {
5
+ focalX: file.focalX,
6
+ focalY: file.focalY,
7
+ height: 300,
8
+ originalHeight: file.height,
9
+ originalWidth: file.width,
10
+ width: 300
11
+ };
12
+ }
13
+ return {
14
+ height: 300,
15
+ width: 300
16
+ };
17
+ };
18
+ export const isVideoMimeType = (mimeType)=>Boolean(mimeType?.startsWith('video'));
19
+ export const isPdfMimeType = (mimeType)=>{
3
20
  const isPDF = mimeType === 'application/pdf' || mimeType?.endsWith('/pdf');
4
- return Boolean(isVideo || isPDF);
21
+ return Boolean(isPDF);
5
22
  };
23
+ export const isVideoOrPdfMimeType = (mimeType)=>isVideoMimeType(mimeType) || isPdfMimeType(mimeType);
6
24
  export const getAdminThumbnailURL = (doc)=>{
7
25
  const file = doc;
8
- const baseURL = file.imagekit?.url;
26
+ const baseURL = file.imagekit?.url || file.imagekit?.thumbnailUrl;
9
27
  if (!baseURL) {
10
28
  return '';
11
29
  }
12
30
  let thumbnailBaseURL = baseURL;
13
- if (isVideoOrPdfMimeType(file.mimeType)) {
14
- try {
15
- const parsedURL = new URL(baseURL);
16
- thumbnailBaseURL = `${parsedURL.origin}${parsedURL.pathname}/ik-thumbnail.jpg`;
17
- } catch {
18
- const normalizedBaseURL = baseURL.split('?')[0].split('#')[0];
19
- thumbnailBaseURL = `${normalizedBaseURL}/ik-thumbnail.jpg`;
20
- }
31
+ if (isVideoMimeType(file.mimeType)) {
32
+ thumbnailBaseURL = getVideoPosterSrc(baseURL);
33
+ } else if (isPdfMimeType(file.mimeType)) {
34
+ thumbnailBaseURL = getImageKitThumbnailSrc(baseURL);
21
35
  }
22
- return `${thumbnailBaseURL}${thumbnailBaseURL.includes('?') ? '&' : '?'}tr=w-300,h-300`;
36
+ return withImageKitTransformation(thumbnailBaseURL, getThumbnailTransform(file));
23
37
  };
24
38
  /**
25
39
  * Admin thumbnail URL for a private file.
@@ -30,12 +44,13 @@ export const getAdminThumbnailURL = (doc)=>{
30
44
  * signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for
31
45
  * videos and PDFs, derived from the document's mimeType).
32
46
  */ export const getPrivateAdminThumbnailURL = (doc, collectionSlug, apiRoute)=>{
33
- const filename = doc?.filename;
47
+ const file = doc;
48
+ const filename = file.filename;
34
49
  if (!filename) {
35
50
  return '';
36
51
  }
37
52
  const base = `${apiRoute.replace(/\/+$/, '')}/${collectionSlug}/file/${encodeURIComponent(filename)}`;
38
- return `${base}?tr=w-300,h-300`;
53
+ return withImageKitTransformation(base, getThumbnailTransform(file));
39
54
  };
40
55
 
41
56
  //# sourceMappingURL=getAdminThumbnailURL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/getAdminThumbnailURL.ts"],"sourcesContent":["type ImagekitReference = {\n imagekit?: {\n url?: string\n }\n}\n\nexport const isVideoOrPdfMimeType = (mimeType?: string): boolean => {\n const isVideo = mimeType?.startsWith('video')\n const isPDF = mimeType === 'application/pdf' || mimeType?.endsWith('/pdf')\n\n return Boolean(isVideo || isPDF)\n}\n\nexport const getAdminThumbnailURL = (doc: unknown): string => {\n const file = doc as { mimeType?: string } & ImagekitReference\n const baseURL = file.imagekit?.url\n\n if (!baseURL) {\n return ''\n }\n\n let thumbnailBaseURL = baseURL\n\n if (isVideoOrPdfMimeType(file.mimeType)) {\n try {\n const parsedURL = new URL(baseURL)\n thumbnailBaseURL = `${parsedURL.origin}${parsedURL.pathname}/ik-thumbnail.jpg`\n } catch {\n const normalizedBaseURL = baseURL.split('?')[0].split('#')[0]\n thumbnailBaseURL = `${normalizedBaseURL}/ik-thumbnail.jpg`\n }\n }\n\n return `${thumbnailBaseURL}${thumbnailBaseURL.includes('?') ? '&' : '?'}tr=w-300,h-300`\n}\n\n/**\n * Admin thumbnail URL for a private file.\n *\n * Private files 401 on their plain ImageKit URL, so the preview must be served\n * through Payload's own file route (`<apiRoute>/<collection>/file/<filename>`).\n * That route enforces collection access control and the adapter's `staticHandler`\n * signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for\n * videos and PDFs, derived from the document's mimeType).\n */\nexport const getPrivateAdminThumbnailURL = (\n doc: unknown,\n collectionSlug: string,\n apiRoute: string,\n): string => {\n const filename = (doc as { filename?: string } | undefined)?.filename\n\n if (!filename) {\n return ''\n }\n\n const base = `${apiRoute.replace(/\\/+$/, '')}/${collectionSlug}/file/${encodeURIComponent(filename)}`\n\n return `${base}?tr=w-300,h-300`\n}\n"],"names":["isVideoOrPdfMimeType","mimeType","isVideo","startsWith","isPDF","endsWith","Boolean","getAdminThumbnailURL","doc","file","baseURL","imagekit","url","thumbnailBaseURL","parsedURL","URL","origin","pathname","normalizedBaseURL","split","includes","getPrivateAdminThumbnailURL","collectionSlug","apiRoute","filename","base","replace","encodeURIComponent"],"mappings":"AAMA,OAAO,MAAMA,uBAAuB,CAACC;IACnC,MAAMC,UAAUD,UAAUE,WAAW;IACrC,MAAMC,QAAQH,aAAa,qBAAqBA,UAAUI,SAAS;IAEnE,OAAOC,QAAQJ,WAAWE;AAC5B,EAAC;AAED,OAAO,MAAMG,uBAAuB,CAACC;IACnC,MAAMC,OAAOD;IACb,MAAME,UAAUD,KAAKE,QAAQ,EAAEC;IAE/B,IAAI,CAACF,SAAS;QACZ,OAAO;IACT;IAEA,IAAIG,mBAAmBH;IAEvB,IAAIV,qBAAqBS,KAAKR,QAAQ,GAAG;QACvC,IAAI;YACF,MAAMa,YAAY,IAAIC,IAAIL;YAC1BG,mBAAmB,GAAGC,UAAUE,MAAM,GAAGF,UAAUG,QAAQ,CAAC,iBAAiB,CAAC;QAChF,EAAE,OAAM;YACN,MAAMC,oBAAoBR,QAAQS,KAAK,CAAC,IAAI,CAAC,EAAE,CAACA,KAAK,CAAC,IAAI,CAAC,EAAE;YAC7DN,mBAAmB,GAAGK,kBAAkB,iBAAiB,CAAC;QAC5D;IACF;IAEA,OAAO,GAAGL,mBAAmBA,iBAAiBO,QAAQ,CAAC,OAAO,MAAM,IAAI,cAAc,CAAC;AACzF,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMC,8BAA8B,CACzCb,KACAc,gBACAC;IAEA,MAAMC,WAAYhB,KAA2CgB;IAE7D,IAAI,CAACA,UAAU;QACb,OAAO;IACT;IAEA,MAAMC,OAAO,GAAGF,SAASG,OAAO,CAAC,QAAQ,IAAI,CAAC,EAAEJ,eAAe,MAAM,EAAEK,mBAAmBH,WAAW;IAErG,OAAO,GAAGC,KAAK,eAAe,CAAC;AACjC,EAAC"}
1
+ {"version":3,"sources":["../src/getAdminThumbnailURL.ts"],"sourcesContent":["import {\n getImageKitThumbnailSrc,\n getVideoPosterSrc,\n withImageKitTransformation,\n} from './imageTransforms.js'\n\ntype ImageKitReference = {\n imagekit?: {\n thumbnailUrl?: string\n url?: string\n }\n}\n\ntype UploadPreviewDocument = {\n filename?: string\n focalX?: null | number\n focalY?: null | number\n height?: null | number\n mimeType?: string\n width?: null | number\n} & ImageKitReference\n\nconst getThumbnailTransform = (file: UploadPreviewDocument) => {\n if (\n !isVideoOrPdfMimeType(file.mimeType) &&\n typeof file.focalX === 'number' &&\n typeof file.focalY === 'number' &&\n typeof file.width === 'number' &&\n typeof file.height === 'number'\n ) {\n return {\n focalX: file.focalX,\n focalY: file.focalY,\n height: 300,\n originalHeight: file.height,\n originalWidth: file.width,\n width: 300,\n }\n }\n\n return {\n height: 300,\n width: 300,\n }\n}\n\nexport const isVideoMimeType = (mimeType?: string): boolean =>\n Boolean(mimeType?.startsWith('video'))\n\nexport const isPdfMimeType = (mimeType?: string): boolean => {\n const isPDF = mimeType === 'application/pdf' || mimeType?.endsWith('/pdf')\n\n return Boolean(isPDF)\n}\n\nexport const isVideoOrPdfMimeType = (mimeType?: string): boolean =>\n isVideoMimeType(mimeType) || isPdfMimeType(mimeType)\n\nexport const getAdminThumbnailURL = (doc: unknown): string => {\n const file = doc as UploadPreviewDocument\n const baseURL = file.imagekit?.url || file.imagekit?.thumbnailUrl\n\n if (!baseURL) {\n return ''\n }\n\n let thumbnailBaseURL = baseURL\n\n if (isVideoMimeType(file.mimeType)) {\n thumbnailBaseURL = getVideoPosterSrc(baseURL)\n } else if (isPdfMimeType(file.mimeType)) {\n thumbnailBaseURL = getImageKitThumbnailSrc(baseURL)\n }\n\n return withImageKitTransformation(thumbnailBaseURL, getThumbnailTransform(file))\n}\n\n/**\n * Admin thumbnail URL for a private file.\n *\n * Private files 401 on their plain ImageKit URL, so the preview must be served\n * through Payload's own file route (`<apiRoute>/<collection>/file/<filename>`).\n * That route enforces collection access control and the adapter's `staticHandler`\n * signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for\n * videos and PDFs, derived from the document's mimeType).\n */\nexport const getPrivateAdminThumbnailURL = (\n doc: unknown,\n collectionSlug: string,\n apiRoute: string,\n): string => {\n const file = doc as UploadPreviewDocument\n const filename = file.filename\n\n if (!filename) {\n return ''\n }\n\n const base = `${apiRoute.replace(/\\/+$/, '')}/${collectionSlug}/file/${encodeURIComponent(filename)}`\n\n return withImageKitTransformation(base, getThumbnailTransform(file))\n}\n"],"names":["getImageKitThumbnailSrc","getVideoPosterSrc","withImageKitTransformation","getThumbnailTransform","file","isVideoOrPdfMimeType","mimeType","focalX","focalY","width","height","originalHeight","originalWidth","isVideoMimeType","Boolean","startsWith","isPdfMimeType","isPDF","endsWith","getAdminThumbnailURL","doc","baseURL","imagekit","url","thumbnailUrl","thumbnailBaseURL","getPrivateAdminThumbnailURL","collectionSlug","apiRoute","filename","base","replace","encodeURIComponent"],"mappings":"AAAA,SACEA,uBAAuB,EACvBC,iBAAiB,EACjBC,0BAA0B,QACrB,uBAAsB;AAkB7B,MAAMC,wBAAwB,CAACC;IAC7B,IACE,CAACC,qBAAqBD,KAAKE,QAAQ,KACnC,OAAOF,KAAKG,MAAM,KAAK,YACvB,OAAOH,KAAKI,MAAM,KAAK,YACvB,OAAOJ,KAAKK,KAAK,KAAK,YACtB,OAAOL,KAAKM,MAAM,KAAK,UACvB;QACA,OAAO;YACLH,QAAQH,KAAKG,MAAM;YACnBC,QAAQJ,KAAKI,MAAM;YACnBE,QAAQ;YACRC,gBAAgBP,KAAKM,MAAM;YAC3BE,eAAeR,KAAKK,KAAK;YACzBA,OAAO;QACT;IACF;IAEA,OAAO;QACLC,QAAQ;QACRD,OAAO;IACT;AACF;AAEA,OAAO,MAAMI,kBAAkB,CAACP,WAC9BQ,QAAQR,UAAUS,WAAW,UAAS;AAExC,OAAO,MAAMC,gBAAgB,CAACV;IAC5B,MAAMW,QAAQX,aAAa,qBAAqBA,UAAUY,SAAS;IAEnE,OAAOJ,QAAQG;AACjB,EAAC;AAED,OAAO,MAAMZ,uBAAuB,CAACC,WACnCO,gBAAgBP,aAAaU,cAAcV,UAAS;AAEtD,OAAO,MAAMa,uBAAuB,CAACC;IACnC,MAAMhB,OAAOgB;IACb,MAAMC,UAAUjB,KAAKkB,QAAQ,EAAEC,OAAOnB,KAAKkB,QAAQ,EAAEE;IAErD,IAAI,CAACH,SAAS;QACZ,OAAO;IACT;IAEA,IAAII,mBAAmBJ;IAEvB,IAAIR,gBAAgBT,KAAKE,QAAQ,GAAG;QAClCmB,mBAAmBxB,kBAAkBoB;IACvC,OAAO,IAAIL,cAAcZ,KAAKE,QAAQ,GAAG;QACvCmB,mBAAmBzB,wBAAwBqB;IAC7C;IAEA,OAAOnB,2BAA2BuB,kBAAkBtB,sBAAsBC;AAC5E,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMsB,8BAA8B,CACzCN,KACAO,gBACAC;IAEA,MAAMxB,OAAOgB;IACb,MAAMS,WAAWzB,KAAKyB,QAAQ;IAE9B,IAAI,CAACA,UAAU;QACb,OAAO;IACT;IAEA,MAAMC,OAAO,GAAGF,SAASG,OAAO,CAAC,QAAQ,IAAI,CAAC,EAAEJ,eAAe,MAAM,EAAEK,mBAAmBH,WAAW;IAErG,OAAO3B,2BAA2B4B,MAAM3B,sBAAsBC;AAChE,EAAC"}
@@ -0,0 +1,37 @@
1
+ export type ImageTransformationOptions = {
2
+ dpr?: number;
3
+ fit?: 'contain' | 'cover';
4
+ /** Focal point X as a percentage (0-100) of the original image width. */
5
+ focalX?: null | number;
6
+ /** Focal point Y as a percentage (0-100) of the original image height. */
7
+ focalY?: null | number;
8
+ format?: string;
9
+ height?: number;
10
+ lossless?: boolean;
11
+ /** The original uploaded image height in pixels, required to compute a custom focal crop. */
12
+ originalHeight?: null | number;
13
+ /** The original uploaded image width in pixels, required to compute a custom focal crop. */
14
+ originalWidth?: null | number;
15
+ quality?: number;
16
+ vc?: string;
17
+ width?: number;
18
+ };
19
+ export type CropRect = {
20
+ height: number;
21
+ width: number;
22
+ x: number;
23
+ y: number;
24
+ };
25
+ export declare const getImageKitVideoSrc: (src: string) => string;
26
+ export declare const getImageKitThumbnailSrc: (src: string) => string;
27
+ export declare const getVideoPosterSrc: (src: string) => string;
28
+ /**
29
+ * Compute the largest target-aspect rectangle that fits inside the source image,
30
+ * centred on a focal point and clamped so ImageKit's `cm-extract` never receives
31
+ * an out-of-bounds crop.
32
+ */
33
+ export declare const computeFocalCropRect: (originalWidth: number, originalHeight: number, targetWidth: number, targetHeight: number, focalX: number, focalY: number) => CropRect;
34
+ export declare const buildImageKitTransformation: (params: ImageTransformationOptions) => string;
35
+ export declare const withImageKitTransformation: (src: string, params: ImageTransformationOptions) => string;
36
+ export declare const srcParamsPayloadImageKit: (src: string, params: ImageTransformationOptions) => string;
37
+ export declare const srcParamsPayloadImagekit: (src: string, params: ImageTransformationOptions) => string;
@@ -0,0 +1,117 @@
1
+ const hasImageKitVideoExtension = (pathname)=>/\.(?:mov|mp4)$/i.test(pathname.replace(/\/$/, ''));
2
+ export const getImageKitVideoSrc = (src)=>{
3
+ try {
4
+ const url = new URL(src);
5
+ if (hasImageKitVideoExtension(url.pathname)) {
6
+ return src;
7
+ }
8
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/ik-video.mp4`;
9
+ return url.toString();
10
+ } catch {
11
+ const hashIndex = src.indexOf('#');
12
+ const beforeHash = hashIndex === -1 ? src : src.slice(0, hashIndex);
13
+ const hash = hashIndex === -1 ? '' : src.slice(hashIndex);
14
+ const queryIndex = beforeHash.indexOf('?');
15
+ const pathname = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);
16
+ const search = queryIndex === -1 ? '' : beforeHash.slice(queryIndex);
17
+ if (hasImageKitVideoExtension(pathname)) {
18
+ return src;
19
+ }
20
+ return `${pathname.replace(/\/$/, '')}/ik-video.mp4${search}${hash}`;
21
+ }
22
+ };
23
+ export const getImageKitThumbnailSrc = (src)=>{
24
+ try {
25
+ const url = new URL(src);
26
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/ik-thumbnail.jpg`;
27
+ url.search = '';
28
+ url.hash = '';
29
+ return url.toString();
30
+ } catch {
31
+ const normalizedSrc = src.split('?')[0].split('#')[0].replace(/\/$/, '');
32
+ return `${normalizedSrc}/ik-thumbnail.jpg`;
33
+ }
34
+ };
35
+ export const getVideoPosterSrc = (src)=>getImageKitThumbnailSrc(getImageKitVideoSrc(src));
36
+ const isPositiveNumber = (value)=>typeof value === 'number' && Number.isFinite(value) && value > 0;
37
+ const clamp = (value, min, max)=>Math.min(Math.max(value, min), max);
38
+ /**
39
+ * Compute the largest target-aspect rectangle that fits inside the source image,
40
+ * centred on a focal point and clamped so ImageKit's `cm-extract` never receives
41
+ * an out-of-bounds crop.
42
+ */ export const computeFocalCropRect = (originalWidth, originalHeight, targetWidth, targetHeight, focalX, focalY)=>{
43
+ const targetAspect = targetWidth / targetHeight;
44
+ let cropWidth;
45
+ let cropHeight;
46
+ if (originalWidth / originalHeight > targetAspect) {
47
+ cropHeight = originalHeight;
48
+ cropWidth = Math.round(originalHeight * targetAspect);
49
+ } else {
50
+ cropWidth = originalWidth;
51
+ cropHeight = Math.round(originalWidth / targetAspect);
52
+ }
53
+ cropWidth = Math.min(cropWidth, originalWidth);
54
+ cropHeight = Math.min(cropHeight, originalHeight);
55
+ const centreX = clamp(focalX, 0, 100) / 100 * originalWidth;
56
+ const centreY = clamp(focalY, 0, 100) / 100 * originalHeight;
57
+ const x = clamp(Math.round(centreX - cropWidth / 2), 0, originalWidth - cropWidth);
58
+ const y = clamp(Math.round(centreY - cropHeight / 2), 0, originalHeight - cropHeight);
59
+ return {
60
+ height: cropHeight,
61
+ width: cropWidth,
62
+ x,
63
+ y
64
+ };
65
+ };
66
+ const getCustomCropRect = (params)=>{
67
+ if (params.fit !== 'contain' && isPositiveNumber(params.width) && isPositiveNumber(params.height) && isPositiveNumber(params.originalWidth) && isPositiveNumber(params.originalHeight) && typeof params.focalX === 'number' && Number.isFinite(params.focalX) && typeof params.focalY === 'number' && Number.isFinite(params.focalY)) {
68
+ return computeFocalCropRect(params.originalWidth, params.originalHeight, params.width, params.height, params.focalX, params.focalY);
69
+ }
70
+ return null;
71
+ };
72
+ export const buildImageKitTransformation = (params)=>{
73
+ const transformParams = [];
74
+ if (params.width) {
75
+ transformParams.push(`w-${params.width}`);
76
+ }
77
+ if (params.height) {
78
+ transformParams.push(`h-${params.height}`);
79
+ }
80
+ if (params.fit === 'contain') {
81
+ transformParams.push('c-at_max');
82
+ }
83
+ if (params.dpr) {
84
+ transformParams.push(`dpr-${params.dpr}`);
85
+ }
86
+ if (params.format) {
87
+ transformParams.push(`f-${params.format}`);
88
+ }
89
+ if (params.vc) {
90
+ transformParams.push(`vc-${params.vc}`);
91
+ }
92
+ if (params.lossless) {
93
+ transformParams.push('lo-true', 'q-100');
94
+ } else if (params.quality) {
95
+ transformParams.push(`q-${params.quality}`);
96
+ }
97
+ const customCrop = getCustomCropRect(params);
98
+ if (customCrop) {
99
+ return [
100
+ `cm-extract,x-${customCrop.x},y-${customCrop.y},w-${customCrop.width},h-${customCrop.height}`,
101
+ transformParams.join(',')
102
+ ].filter(Boolean).join(':');
103
+ }
104
+ return transformParams.join(',');
105
+ };
106
+ export const withImageKitTransformation = (src, params)=>{
107
+ const transformation = buildImageKitTransformation(params);
108
+ if (!transformation) {
109
+ return src;
110
+ }
111
+ return `${src}${src.includes('?') ? '&' : '?'}tr=${transformation}`;
112
+ };
113
+ export const srcParamsPayloadImageKit = withImageKitTransformation;
114
+ // Backward-compatible alias for the helper name used in older project templates.
115
+ export const srcParamsPayloadImagekit = srcParamsPayloadImageKit;
116
+
117
+ //# sourceMappingURL=imageTransforms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/imageTransforms.ts"],"sourcesContent":["export type ImageTransformationOptions = {\n dpr?: number\n fit?: 'contain' | 'cover'\n /** Focal point X as a percentage (0-100) of the original image width. */\n focalX?: null | number\n /** Focal point Y as a percentage (0-100) of the original image height. */\n focalY?: null | number\n format?: string\n height?: number\n lossless?: boolean\n /** The original uploaded image height in pixels, required to compute a custom focal crop. */\n originalHeight?: null | number\n /** The original uploaded image width in pixels, required to compute a custom focal crop. */\n originalWidth?: null | number\n quality?: number\n vc?: string\n width?: number\n}\n\nexport type CropRect = {\n height: number\n width: number\n x: number\n y: number\n}\n\nconst hasImageKitVideoExtension = (pathname: string): boolean =>\n /\\.(?:mov|mp4)$/i.test(pathname.replace(/\\/$/, ''))\n\nexport const getImageKitVideoSrc = (src: string): string => {\n try {\n const url = new URL(src)\n\n if (hasImageKitVideoExtension(url.pathname)) {\n return src\n }\n\n url.pathname = `${url.pathname.replace(/\\/$/, '')}/ik-video.mp4`\n\n return url.toString()\n } catch {\n const hashIndex = src.indexOf('#')\n const beforeHash = hashIndex === -1 ? src : src.slice(0, hashIndex)\n const hash = hashIndex === -1 ? '' : src.slice(hashIndex)\n const queryIndex = beforeHash.indexOf('?')\n const pathname = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex)\n const search = queryIndex === -1 ? '' : beforeHash.slice(queryIndex)\n\n if (hasImageKitVideoExtension(pathname)) {\n return src\n }\n\n return `${pathname.replace(/\\/$/, '')}/ik-video.mp4${search}${hash}`\n }\n}\n\nexport const getImageKitThumbnailSrc = (src: string): string => {\n try {\n const url = new URL(src)\n url.pathname = `${url.pathname.replace(/\\/$/, '')}/ik-thumbnail.jpg`\n url.search = ''\n url.hash = ''\n\n return url.toString()\n } catch {\n const normalizedSrc = src.split('?')[0].split('#')[0].replace(/\\/$/, '')\n\n return `${normalizedSrc}/ik-thumbnail.jpg`\n }\n}\n\nexport const getVideoPosterSrc = (src: string): string =>\n getImageKitThumbnailSrc(getImageKitVideoSrc(src))\n\nconst isPositiveNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n\nconst clamp = (value: number, min: number, max: number): number =>\n Math.min(Math.max(value, min), max)\n\n/**\n * Compute the largest target-aspect rectangle that fits inside the source image,\n * centred on a focal point and clamped so ImageKit's `cm-extract` never receives\n * an out-of-bounds crop.\n */\nexport const computeFocalCropRect = (\n originalWidth: number,\n originalHeight: number,\n targetWidth: number,\n targetHeight: number,\n focalX: number,\n focalY: number,\n): CropRect => {\n const targetAspect = targetWidth / targetHeight\n let cropWidth: number\n let cropHeight: number\n\n if (originalWidth / originalHeight > targetAspect) {\n cropHeight = originalHeight\n cropWidth = Math.round(originalHeight * targetAspect)\n } else {\n cropWidth = originalWidth\n cropHeight = Math.round(originalWidth / targetAspect)\n }\n\n cropWidth = Math.min(cropWidth, originalWidth)\n cropHeight = Math.min(cropHeight, originalHeight)\n\n const centreX = (clamp(focalX, 0, 100) / 100) * originalWidth\n const centreY = (clamp(focalY, 0, 100) / 100) * originalHeight\n const x = clamp(Math.round(centreX - cropWidth / 2), 0, originalWidth - cropWidth)\n const y = clamp(Math.round(centreY - cropHeight / 2), 0, originalHeight - cropHeight)\n\n return { height: cropHeight, width: cropWidth, x, y }\n}\n\nconst getCustomCropRect = (params: ImageTransformationOptions): CropRect | null => {\n if (\n params.fit !== 'contain' &&\n isPositiveNumber(params.width) &&\n isPositiveNumber(params.height) &&\n isPositiveNumber(params.originalWidth) &&\n isPositiveNumber(params.originalHeight) &&\n typeof params.focalX === 'number' &&\n Number.isFinite(params.focalX) &&\n typeof params.focalY === 'number' &&\n Number.isFinite(params.focalY)\n ) {\n return computeFocalCropRect(\n params.originalWidth,\n params.originalHeight,\n params.width,\n params.height,\n params.focalX,\n params.focalY,\n )\n }\n\n return null\n}\n\nexport const buildImageKitTransformation = (params: ImageTransformationOptions): string => {\n const transformParams: string[] = []\n\n if (params.width) {\n transformParams.push(`w-${params.width}`)\n }\n\n if (params.height) {\n transformParams.push(`h-${params.height}`)\n }\n\n if (params.fit === 'contain') {\n transformParams.push('c-at_max')\n }\n\n if (params.dpr) {\n transformParams.push(`dpr-${params.dpr}`)\n }\n\n if (params.format) {\n transformParams.push(`f-${params.format}`)\n }\n\n if (params.vc) {\n transformParams.push(`vc-${params.vc}`)\n }\n\n if (params.lossless) {\n transformParams.push('lo-true', 'q-100')\n } else if (params.quality) {\n transformParams.push(`q-${params.quality}`)\n }\n\n const customCrop = getCustomCropRect(params)\n\n if (customCrop) {\n return [\n `cm-extract,x-${customCrop.x},y-${customCrop.y},w-${customCrop.width},h-${customCrop.height}`,\n transformParams.join(','),\n ]\n .filter(Boolean)\n .join(':')\n }\n\n return transformParams.join(',')\n}\n\nexport const withImageKitTransformation = (\n src: string,\n params: ImageTransformationOptions,\n): string => {\n const transformation = buildImageKitTransformation(params)\n\n if (!transformation) {\n return src\n }\n\n return `${src}${src.includes('?') ? '&' : '?'}tr=${transformation}`\n}\n\nexport const srcParamsPayloadImageKit = withImageKitTransformation\n\n// Backward-compatible alias for the helper name used in older project templates.\nexport const srcParamsPayloadImagekit = srcParamsPayloadImageKit\n"],"names":["hasImageKitVideoExtension","pathname","test","replace","getImageKitVideoSrc","src","url","URL","toString","hashIndex","indexOf","beforeHash","slice","hash","queryIndex","search","getImageKitThumbnailSrc","normalizedSrc","split","getVideoPosterSrc","isPositiveNumber","value","Number","isFinite","clamp","min","max","Math","computeFocalCropRect","originalWidth","originalHeight","targetWidth","targetHeight","focalX","focalY","targetAspect","cropWidth","cropHeight","round","centreX","centreY","x","y","height","width","getCustomCropRect","params","fit","buildImageKitTransformation","transformParams","push","dpr","format","vc","lossless","quality","customCrop","join","filter","Boolean","withImageKitTransformation","transformation","includes","srcParamsPayloadImageKit","srcParamsPayloadImagekit"],"mappings":"AA0BA,MAAMA,4BAA4B,CAACC,WACjC,kBAAkBC,IAAI,CAACD,SAASE,OAAO,CAAC,OAAO;AAEjD,OAAO,MAAMC,sBAAsB,CAACC;IAClC,IAAI;QACF,MAAMC,MAAM,IAAIC,IAAIF;QAEpB,IAAIL,0BAA0BM,IAAIL,QAAQ,GAAG;YAC3C,OAAOI;QACT;QAEAC,IAAIL,QAAQ,GAAG,GAAGK,IAAIL,QAAQ,CAACE,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC;QAEhE,OAAOG,IAAIE,QAAQ;IACrB,EAAE,OAAM;QACN,MAAMC,YAAYJ,IAAIK,OAAO,CAAC;QAC9B,MAAMC,aAAaF,cAAc,CAAC,IAAIJ,MAAMA,IAAIO,KAAK,CAAC,GAAGH;QACzD,MAAMI,OAAOJ,cAAc,CAAC,IAAI,KAAKJ,IAAIO,KAAK,CAACH;QAC/C,MAAMK,aAAaH,WAAWD,OAAO,CAAC;QACtC,MAAMT,WAAWa,eAAe,CAAC,IAAIH,aAAaA,WAAWC,KAAK,CAAC,GAAGE;QACtE,MAAMC,SAASD,eAAe,CAAC,IAAI,KAAKH,WAAWC,KAAK,CAACE;QAEzD,IAAId,0BAA0BC,WAAW;YACvC,OAAOI;QACT;QAEA,OAAO,GAAGJ,SAASE,OAAO,CAAC,OAAO,IAAI,aAAa,EAAEY,SAASF,MAAM;IACtE;AACF,EAAC;AAED,OAAO,MAAMG,0BAA0B,CAACX;IACtC,IAAI;QACF,MAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIL,QAAQ,GAAG,GAAGK,IAAIL,QAAQ,CAACE,OAAO,CAAC,OAAO,IAAI,iBAAiB,CAAC;QACpEG,IAAIS,MAAM,GAAG;QACbT,IAAIO,IAAI,GAAG;QAEX,OAAOP,IAAIE,QAAQ;IACrB,EAAE,OAAM;QACN,MAAMS,gBAAgBZ,IAAIa,KAAK,CAAC,IAAI,CAAC,EAAE,CAACA,KAAK,CAAC,IAAI,CAAC,EAAE,CAACf,OAAO,CAAC,OAAO;QAErE,OAAO,GAAGc,cAAc,iBAAiB,CAAC;IAC5C;AACF,EAAC;AAED,OAAO,MAAME,oBAAoB,CAACd,MAChCW,wBAAwBZ,oBAAoBC,MAAK;AAEnD,MAAMe,mBAAmB,CAACC,QACxB,OAAOA,UAAU,YAAYC,OAAOC,QAAQ,CAACF,UAAUA,QAAQ;AAEjE,MAAMG,QAAQ,CAACH,OAAeI,KAAaC,MACzCC,KAAKF,GAAG,CAACE,KAAKD,GAAG,CAACL,OAAOI,MAAMC;AAEjC;;;;CAIC,GACD,OAAO,MAAME,uBAAuB,CAClCC,eACAC,gBACAC,aACAC,cACAC,QACAC;IAEA,MAAMC,eAAeJ,cAAcC;IACnC,IAAII;IACJ,IAAIC;IAEJ,IAAIR,gBAAgBC,iBAAiBK,cAAc;QACjDE,aAAaP;QACbM,YAAYT,KAAKW,KAAK,CAACR,iBAAiBK;IAC1C,OAAO;QACLC,YAAYP;QACZQ,aAAaV,KAAKW,KAAK,CAACT,gBAAgBM;IAC1C;IAEAC,YAAYT,KAAKF,GAAG,CAACW,WAAWP;IAChCQ,aAAaV,KAAKF,GAAG,CAACY,YAAYP;IAElC,MAAMS,UAAU,AAACf,MAAMS,QAAQ,GAAG,OAAO,MAAOJ;IAChD,MAAMW,UAAU,AAAChB,MAAMU,QAAQ,GAAG,OAAO,MAAOJ;IAChD,MAAMW,IAAIjB,MAAMG,KAAKW,KAAK,CAACC,UAAUH,YAAY,IAAI,GAAGP,gBAAgBO;IACxE,MAAMM,IAAIlB,MAAMG,KAAKW,KAAK,CAACE,UAAUH,aAAa,IAAI,GAAGP,iBAAiBO;IAE1E,OAAO;QAAEM,QAAQN;QAAYO,OAAOR;QAAWK;QAAGC;IAAE;AACtD,EAAC;AAED,MAAMG,oBAAoB,CAACC;IACzB,IACEA,OAAOC,GAAG,KAAK,aACf3B,iBAAiB0B,OAAOF,KAAK,KAC7BxB,iBAAiB0B,OAAOH,MAAM,KAC9BvB,iBAAiB0B,OAAOjB,aAAa,KACrCT,iBAAiB0B,OAAOhB,cAAc,KACtC,OAAOgB,OAAOb,MAAM,KAAK,YACzBX,OAAOC,QAAQ,CAACuB,OAAOb,MAAM,KAC7B,OAAOa,OAAOZ,MAAM,KAAK,YACzBZ,OAAOC,QAAQ,CAACuB,OAAOZ,MAAM,GAC7B;QACA,OAAON,qBACLkB,OAAOjB,aAAa,EACpBiB,OAAOhB,cAAc,EACrBgB,OAAOF,KAAK,EACZE,OAAOH,MAAM,EACbG,OAAOb,MAAM,EACba,OAAOZ,MAAM;IAEjB;IAEA,OAAO;AACT;AAEA,OAAO,MAAMc,8BAA8B,CAACF;IAC1C,MAAMG,kBAA4B,EAAE;IAEpC,IAAIH,OAAOF,KAAK,EAAE;QAChBK,gBAAgBC,IAAI,CAAC,CAAC,EAAE,EAAEJ,OAAOF,KAAK,EAAE;IAC1C;IAEA,IAAIE,OAAOH,MAAM,EAAE;QACjBM,gBAAgBC,IAAI,CAAC,CAAC,EAAE,EAAEJ,OAAOH,MAAM,EAAE;IAC3C;IAEA,IAAIG,OAAOC,GAAG,KAAK,WAAW;QAC5BE,gBAAgBC,IAAI,CAAC;IACvB;IAEA,IAAIJ,OAAOK,GAAG,EAAE;QACdF,gBAAgBC,IAAI,CAAC,CAAC,IAAI,EAAEJ,OAAOK,GAAG,EAAE;IAC1C;IAEA,IAAIL,OAAOM,MAAM,EAAE;QACjBH,gBAAgBC,IAAI,CAAC,CAAC,EAAE,EAAEJ,OAAOM,MAAM,EAAE;IAC3C;IAEA,IAAIN,OAAOO,EAAE,EAAE;QACbJ,gBAAgBC,IAAI,CAAC,CAAC,GAAG,EAAEJ,OAAOO,EAAE,EAAE;IACxC;IAEA,IAAIP,OAAOQ,QAAQ,EAAE;QACnBL,gBAAgBC,IAAI,CAAC,WAAW;IAClC,OAAO,IAAIJ,OAAOS,OAAO,EAAE;QACzBN,gBAAgBC,IAAI,CAAC,CAAC,EAAE,EAAEJ,OAAOS,OAAO,EAAE;IAC5C;IAEA,MAAMC,aAAaX,kBAAkBC;IAErC,IAAIU,YAAY;QACd,OAAO;YACL,CAAC,aAAa,EAAEA,WAAWf,CAAC,CAAC,GAAG,EAAEe,WAAWd,CAAC,CAAC,GAAG,EAAEc,WAAWZ,KAAK,CAAC,GAAG,EAAEY,WAAWb,MAAM,EAAE;YAC7FM,gBAAgBQ,IAAI,CAAC;SACtB,CACEC,MAAM,CAACC,SACPF,IAAI,CAAC;IACV;IAEA,OAAOR,gBAAgBQ,IAAI,CAAC;AAC9B,EAAC;AAED,OAAO,MAAMG,6BAA6B,CACxCvD,KACAyC;IAEA,MAAMe,iBAAiBb,4BAA4BF;IAEnD,IAAI,CAACe,gBAAgB;QACnB,OAAOxD;IACT;IAEA,OAAO,GAAGA,MAAMA,IAAIyD,QAAQ,CAAC,OAAO,MAAM,IAAI,GAAG,EAAED,gBAAgB;AACrE,EAAC;AAED,OAAO,MAAME,2BAA2BH,2BAA0B;AAElE,iFAAiF;AACjF,OAAO,MAAMI,2BAA2BD,yBAAwB"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { Config, UploadCollectionSlug } from 'payload';
2
2
  export type ImageKitCollectionOptions = {
3
+ /**
4
+ * Override Payload's native focal-point picker (`upload.focalPoint`) for this
5
+ * collection.
6
+ */
7
+ focalPoint?: boolean;
3
8
  /**
4
9
  * Upload files in this collection as ImageKit private files (`isPrivateFile: true`)
5
10
  * and deliver them only through short-lived signed URLs, behind Payload access control.
@@ -8,12 +13,20 @@ export type ImageKitCollectionOptions = {
8
13
  };
9
14
  type ImageKitCollectionConfig = ImageKitCollectionOptions | true;
10
15
  type ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>;
11
- export type PayloadStorageImagekitConfig = {
16
+ export type PayloadStorageImageKitConfig = {
12
17
  alwaysInsertFields?: boolean;
13
18
  clientUploads?: boolean;
14
19
  collections: ImageKitCollectionMap;
15
20
  disablePayloadAccessControl?: true;
16
21
  enabled?: boolean;
22
+ /**
23
+ * Broad default for Payload's native focal-point picker (`upload.focalPoint`)
24
+ * on every configured collection. Focal points are disabled by default for
25
+ * configured collections, so prefer collection-level `focalPoint: true` for
26
+ * image collections. A collection's own `upload.focalPoint` still takes
27
+ * precedence.
28
+ */
29
+ focalPoint?: boolean;
17
30
  folder: string;
18
31
  privateKey: string;
19
32
  publicKey: string;
@@ -25,7 +38,9 @@ export type PayloadStorageImagekitConfig = {
25
38
  signedUrlExpiresIn?: number;
26
39
  urlEndpoint: string;
27
40
  };
28
- export declare const imagekitStorage: (pluginOptions: PayloadStorageImagekitConfig) => (config: Config) => Config;
41
+ export declare const imagekitStorage: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
29
42
  export { migrateFolder } from './migrate.js';
30
43
  export type { MigrateFolderOptions, MigrationJobResult } from './migrate.js';
31
- export declare const payloadStorageImagekit: (pluginOptions: PayloadStorageImagekitConfig) => (config: Config) => Config;
44
+ export declare const payloadStorageImagekit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
45
+ export declare const payloadStorageImageKit: (pluginOptions: PayloadStorageImageKitConfig) => (config: Config) => Config;
46
+ export type PayloadStorageImagekitConfig = PayloadStorageImageKitConfig;