@pro-laico/payload-images 0.0.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/LICENSE.md +22 -0
- package/README.md +11 -0
- package/dist/access.d.ts +4 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +4 -0
- package/dist/access.js.map +1 -0
- package/dist/collections/generatedImages.d.ts +26 -0
- package/dist/collections/generatedImages.d.ts.map +1 -0
- package/dist/collections/generatedImages.js +91 -0
- package/dist/collections/generatedImages.js.map +1 -0
- package/dist/collections/images.d.ts +60 -0
- package/dist/collections/images.d.ts.map +1 -0
- package/dist/collections/images.js +223 -0
- package/dist/collections/images.js.map +1 -0
- package/dist/components/admin/focalPreview.d.ts +8 -0
- package/dist/components/admin/focalPreview.d.ts.map +1 -0
- package/dist/components/admin/focalPreview.js +234 -0
- package/dist/components/admin/focalPreview.js.map +1 -0
- package/dist/components/admin/purgeVariants.d.ts +16 -0
- package/dist/components/admin/purgeVariants.d.ts.map +1 -0
- package/dist/components/admin/purgeVariants.js +66 -0
- package/dist/components/admin/purgeVariants.js.map +1 -0
- package/dist/components/image.d.ts +90 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +162 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/inlineLqip.d.ts +36 -0
- package/dist/components/inlineLqip.d.ts.map +1 -0
- package/dist/components/inlineLqip.js +54 -0
- package/dist/components/inlineLqip.js.map +1 -0
- package/dist/endpoints/transform.d.ts +40 -0
- package/dist/endpoints/transform.d.ts.map +1 -0
- package/dist/endpoints/transform.js +181 -0
- package/dist/endpoints/transform.js.map +1 -0
- package/dist/fields/virtualUrls.d.ts +26 -0
- package/dist/fields/virtualUrls.d.ts.map +1 -0
- package/dist/fields/virtualUrls.js +185 -0
- package/dist/fields/virtualUrls.js.map +1 -0
- package/dist/hooks/purge.d.ts +33 -0
- package/dist/hooks/purge.d.ts.map +1 -0
- package/dist/hooks/purge.js +63 -0
- package/dist/hooks/purge.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/configStash.d.ts +6 -0
- package/dist/lib/configStash.d.ts.map +1 -0
- package/dist/lib/configStash.js +10 -0
- package/dist/lib/configStash.js.map +1 -0
- package/dist/lib/getServerSideURL.d.ts +8 -0
- package/dist/lib/getServerSideURL.d.ts.map +1 -0
- package/dist/lib/getServerSideURL.js +12 -0
- package/dist/lib/getServerSideURL.js.map +1 -0
- package/dist/lib/mergeCollection.d.ts +17 -0
- package/dist/lib/mergeCollection.d.ts.map +1 -0
- package/dist/lib/mergeCollection.js +55 -0
- package/dist/lib/mergeCollection.js.map +1 -0
- package/dist/plugin.d.ts +128 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +184 -0
- package/dist/plugin.js.map +1 -0
- package/dist/transform/coalesce.d.ts +16 -0
- package/dist/transform/coalesce.d.ts.map +1 -0
- package/dist/transform/coalesce.js +25 -0
- package/dist/transform/coalesce.js.map +1 -0
- package/dist/transform/geometry.d.ts +46 -0
- package/dist/transform/geometry.d.ts.map +1 -0
- package/dist/transform/geometry.js +64 -0
- package/dist/transform/geometry.js.map +1 -0
- package/dist/transform/getVariantBytes.d.ts +60 -0
- package/dist/transform/getVariantBytes.d.ts.map +1 -0
- package/dist/transform/getVariantBytes.js +163 -0
- package/dist/transform/getVariantBytes.js.map +1 -0
- package/dist/transform/limit.d.ts +5 -0
- package/dist/transform/limit.d.ts.map +1 -0
- package/dist/transform/limit.js +41 -0
- package/dist/transform/limit.js.map +1 -0
- package/dist/transform/lqip.d.ts +21 -0
- package/dist/transform/lqip.d.ts.map +1 -0
- package/dist/transform/lqip.js +32 -0
- package/dist/transform/lqip.js.map +1 -0
- package/dist/transform/params.d.ts +100 -0
- package/dist/transform/params.d.ts.map +1 -0
- package/dist/transform/params.js +144 -0
- package/dist/transform/params.js.map +1 -0
- package/dist/transform/sharp.d.ts +28 -0
- package/dist/transform/sharp.d.ts.map +1 -0
- package/dist/transform/sharp.js +85 -0
- package/dist/transform/sharp.js.map +1 -0
- package/dist/transform/sharpInstance.d.ts +21 -0
- package/dist/transform/sharpInstance.d.ts.map +1 -0
- package/dist/transform/sharpInstance.js +38 -0
- package/dist/transform/sharpInstance.js.map +1 -0
- package/dist/transform/source.d.ts +32 -0
- package/dist/transform/source.d.ts.map +1 -0
- package/dist/transform/source.js +121 -0
- package/dist/transform/source.js.map +1 -0
- package/dist/utils/urls.d.ts +108 -0
- package/dist/utils/urls.d.ts.map +1 -0
- package/dist/utils/urls.js +113 -0
- package/dist/utils/urls.js.map +1 -0
- package/dist/variants/key.d.ts +9 -0
- package/dist/variants/key.d.ts.map +1 -0
- package/dist/variants/key.js +29 -0
- package/dist/variants/key.js.map +1 -0
- package/package.json +98 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createGeneratedImagesCollection, GENERATED_IMAGES_SLUG } from "./collections/generatedImages.js";
|
|
2
|
+
import { createImagesCollection, imageEnhancements } from "./collections/images.js";
|
|
3
|
+
import { createPurgeEndpoint, createTransformEndpoint } from "./endpoints/transform.js";
|
|
4
|
+
import { stashConfig } from "./lib/configStash.js";
|
|
5
|
+
import { mergeCollection } from "./lib/mergeCollection.js";
|
|
6
|
+
import { SHARP_INSTALL_HINT } from "./transform/getVariantBytes.js";
|
|
7
|
+
import { DEFAULT_CONSTRAINTS, DEFAULT_PIXEL_STEP } from "./transform/params.js";
|
|
8
|
+
import { loadSharp } from "./transform/sharpInstance.js";
|
|
9
|
+
/** Fill in the placeholder defaults (24px / q40 / webp / maxWidth 64), or `false` when disabled. */ export const resolvePlaceholder = (p)=>p === false ? false : {
|
|
10
|
+
width: p?.width ?? 24,
|
|
11
|
+
quality: p?.quality ?? 40,
|
|
12
|
+
format: p?.format ?? 'webp',
|
|
13
|
+
maxWidth: p?.maxWidth ?? 64
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Registers the `images` (source) and hidden `generated-images` (variant cache) collections,
|
|
17
|
+
* plus the on-demand transform + purge endpoints.
|
|
18
|
+
*
|
|
19
|
+
* Uploads store only the original; every rendered size is generated the first time a page asks
|
|
20
|
+
* for it (resized and cropped to the focal point set in the admin), then cached in
|
|
21
|
+
* `generated-images` so it's only ever built once. The LQIP placeholder is generated on demand by
|
|
22
|
+
* `<ResponsiveImage>` (a tiny inline base64, via the same variant cache) — nothing is stored on the
|
|
23
|
+
* source doc.
|
|
24
|
+
*
|
|
25
|
+
* Pass `extendCollection: '<slug>'` to add the pipeline to an upload collection you already have
|
|
26
|
+
* instead of creating `images`. The transform endpoint mounts at `/api/img`; do not name a
|
|
27
|
+
* collection `img` or it shadows the endpoint.
|
|
28
|
+
*/ export const imagesPlugin = (opts = {})=>(config)=>{
|
|
29
|
+
const { enabled = true, extendCollection, imagesOverrides, generatedImagesOverrides, pixelStep = DEFAULT_PIXEL_STEP, transform = {}, focalUI = true, previewRatios, localizeAlt = false, mimeTypes, folders, maxOriginalSize, placeholder } = opts;
|
|
30
|
+
if (!enabled) return config;
|
|
31
|
+
const transformCfg = transform === false ? {} : transform;
|
|
32
|
+
const variantSlug = transformCfg.variantSlug || GENERATED_IMAGES_SLUG;
|
|
33
|
+
// With extendCollection, the source IS that collection; otherwise the default `images`.
|
|
34
|
+
const sourceSlug = extendCollection || transformCfg.sourceSlug || 'images';
|
|
35
|
+
const basePath = '/img';
|
|
36
|
+
const purgePath = `${basePath}/purge`;
|
|
37
|
+
const apiRoute = config.routes?.api ?? '/api';
|
|
38
|
+
const endpointsEnabled = transform !== false;
|
|
39
|
+
// With transforms off the virtual URL fields would point at an unregistered endpoint, so they
|
|
40
|
+
// default off; an explicit `virtualFields: true` is honored (warned at boot — the URLs 404).
|
|
41
|
+
const virtualFields = opts.virtualFields ?? endpointsEnabled;
|
|
42
|
+
const generated = mergeCollection(createGeneratedImagesCollection({
|
|
43
|
+
slug: variantSlug,
|
|
44
|
+
sourceSlug
|
|
45
|
+
}), generatedImagesOverrides);
|
|
46
|
+
let collections;
|
|
47
|
+
if (extendCollection) {
|
|
48
|
+
const target = (config.collections ?? []).find((c)=>c.slug === extendCollection);
|
|
49
|
+
if (!target) throw new Error(`[payload-images] extendCollection: collection '${extendCollection}' not found`);
|
|
50
|
+
if (!target.upload) throw new Error(`[payload-images] extendCollection: collection '${extendCollection}' is not an upload collection`);
|
|
51
|
+
// On-demand admin thumbnail only when the endpoint exists AND the target hasn't set its own.
|
|
52
|
+
const ownThumbnail = typeof target.upload === 'object' && !!target.upload.adminThumbnail || !!target.admin?.thumbnail;
|
|
53
|
+
const enh = imageEnhancements({
|
|
54
|
+
focalUI,
|
|
55
|
+
previewRatios,
|
|
56
|
+
variantSlug,
|
|
57
|
+
purgePath,
|
|
58
|
+
virtualFields,
|
|
59
|
+
folders,
|
|
60
|
+
apiRoute,
|
|
61
|
+
endpointsEnabled,
|
|
62
|
+
adminThumbnail: !endpointsEnabled || ownThumbnail ? false : undefined
|
|
63
|
+
});
|
|
64
|
+
// Merge, never overwrite: the target's own defaultPopulate / forceSelect entries win over
|
|
65
|
+
// (and add to) the parity defaults, so its populated reads keep behaving as it declared.
|
|
66
|
+
const parity = {
|
|
67
|
+
...enh,
|
|
68
|
+
defaultPopulate: {
|
|
69
|
+
...enh.defaultPopulate,
|
|
70
|
+
...target.defaultPopulate
|
|
71
|
+
},
|
|
72
|
+
...enh.forceSelect || target.forceSelect ? {
|
|
73
|
+
forceSelect: {
|
|
74
|
+
...enh.forceSelect,
|
|
75
|
+
...target.forceSelect
|
|
76
|
+
}
|
|
77
|
+
} : {}
|
|
78
|
+
};
|
|
79
|
+
const enhanced = mergeCollection(mergeCollection(target, parity), imagesOverrides);
|
|
80
|
+
collections = [
|
|
81
|
+
...(config.collections ?? []).filter((c)=>c.slug !== extendCollection),
|
|
82
|
+
enhanced,
|
|
83
|
+
generated
|
|
84
|
+
];
|
|
85
|
+
} else {
|
|
86
|
+
const images = mergeCollection(// No transform endpoint → no on-demand thumbnail; fall back to Payload's default.
|
|
87
|
+
createImagesCollection({
|
|
88
|
+
focalUI,
|
|
89
|
+
previewRatios,
|
|
90
|
+
variantSlug,
|
|
91
|
+
purgePath,
|
|
92
|
+
virtualFields,
|
|
93
|
+
localizeAlt,
|
|
94
|
+
mimeTypes,
|
|
95
|
+
folders,
|
|
96
|
+
maxOriginalSize,
|
|
97
|
+
apiRoute,
|
|
98
|
+
endpointsEnabled,
|
|
99
|
+
adminThumbnail: endpointsEnabled ? undefined : false
|
|
100
|
+
}), imagesOverrides);
|
|
101
|
+
collections = [
|
|
102
|
+
...config.collections ?? [],
|
|
103
|
+
images,
|
|
104
|
+
generated
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
// A custom transform.sourceSlug gets the same guard extendCollection does — a typo here would
|
|
108
|
+
// otherwise just 404/500 every image request at runtime. Checked against the FINAL collections
|
|
109
|
+
// array (registration is unchanged; the default `images` collection is still created).
|
|
110
|
+
if (!extendCollection && transformCfg.sourceSlug) {
|
|
111
|
+
const src = collections.find((c)=>c.slug === transformCfg.sourceSlug);
|
|
112
|
+
if (!src) throw new Error(`[payload-images] transform.sourceSlug: collection '${transformCfg.sourceSlug}' not found`);
|
|
113
|
+
if (!src.upload) throw new Error(`[payload-images] transform.sourceSlug: collection '${transformCfg.sourceSlug}' is not an upload collection`);
|
|
114
|
+
}
|
|
115
|
+
const endpoints = transform === false ? config.endpoints : [
|
|
116
|
+
...config.endpoints ?? [],
|
|
117
|
+
createPurgeEndpoint({
|
|
118
|
+
variantSlug,
|
|
119
|
+
sourceSlug
|
|
120
|
+
}),
|
|
121
|
+
// A custom width ladder (array) can't double as the endpoint's numeric snap grid, so the
|
|
122
|
+
// grid falls back to the default; a numeric pixelStep stays coupled to it as before.
|
|
123
|
+
createTransformEndpoint({
|
|
124
|
+
dimensionStep: Array.isArray(pixelStep) ? DEFAULT_PIXEL_STEP : pixelStep,
|
|
125
|
+
...transformCfg,
|
|
126
|
+
variantSlug,
|
|
127
|
+
sourceSlug
|
|
128
|
+
})
|
|
129
|
+
];
|
|
130
|
+
const baseSegment = basePath.replace(/^\//, '').split('/')[0];
|
|
131
|
+
const shadowed = transform !== false && collections.some((c)=>c.slug === baseSegment);
|
|
132
|
+
// These options only shape the CREATED `images` collection; with extendCollection the target's
|
|
133
|
+
// own upload config governs, so a set value would be silently ignored — surface it at boot.
|
|
134
|
+
const ignoredWithExtend = extendCollection ? [
|
|
135
|
+
'mimeTypes',
|
|
136
|
+
'localizeAlt',
|
|
137
|
+
'maxOriginalSize'
|
|
138
|
+
].filter((k)=>opts[k] !== undefined) : [];
|
|
139
|
+
return {
|
|
140
|
+
...config,
|
|
141
|
+
collections,
|
|
142
|
+
endpoints,
|
|
143
|
+
// Stash the resolved config so decoupled tooling (an OG/sitemap generator, a CDN purge
|
|
144
|
+
// script, a migration) and `<ResponsiveImage>` (inline LQIP) can read the slugs + options
|
|
145
|
+
// from just `payload`, no import.
|
|
146
|
+
custom: {
|
|
147
|
+
...config.custom,
|
|
148
|
+
payloadImages: {
|
|
149
|
+
options: opts,
|
|
150
|
+
sourceSlug,
|
|
151
|
+
variantSlug,
|
|
152
|
+
basePath,
|
|
153
|
+
pixelStep,
|
|
154
|
+
placeholder: resolvePlaceholder(placeholder),
|
|
155
|
+
maxInputPixels: transformCfg.maxInputPixels ?? DEFAULT_CONSTRAINTS.maxInputPixels
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
onInit: async (payload)=>{
|
|
159
|
+
await config.onInit?.(payload);
|
|
160
|
+
// Remember the app's config so <ResponsiveImage> resolves it from globalThis — no
|
|
161
|
+
// `@payload-config` alias (and thus no transpilePackages) required once Payload has booted.
|
|
162
|
+
stashConfig(payload.config);
|
|
163
|
+
if (shadowed) {
|
|
164
|
+
payload.logger.warn(`[payload-images] a collection is named "${baseSegment}", which shadows the transform endpoint at /api/${baseSegment} — rename the collection so it doesn't collide.`);
|
|
165
|
+
}
|
|
166
|
+
if (ignoredWithExtend.length) {
|
|
167
|
+
payload.logger.warn(`[payload-images] extendCollection: option(s) ${ignoredWithExtend.join(', ')} are ignored — you own '${extendCollection}'s upload config; set the equivalent on the collection itself.`);
|
|
168
|
+
}
|
|
169
|
+
if (!endpointsEnabled && opts.virtualFields === true) {
|
|
170
|
+
payload.logger.warn('[payload-images] virtualFields: true with transform: false — the virtual src/srcset/placeholderURL/thumbnailURL fields point at the unregistered transform endpoint and will 404.');
|
|
171
|
+
}
|
|
172
|
+
// Probe Sharp once at boot so a missing install / broken native binding surfaces as one
|
|
173
|
+
// actionable error here instead of a 500 on the first image request.
|
|
174
|
+
try {
|
|
175
|
+
await loadSharp();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
payload.logger.error(`[payload-images] sharp failed to load — transforms and LQIPs will fail; ${SHARP_INSTALL_HINT}. (${String(err)})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
export default imagesPlugin;
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionConfig, Config, Plugin } from 'payload'\n\nimport { createGeneratedImagesCollection, GENERATED_IMAGES_SLUG } from './collections/generatedImages'\nimport { createImagesCollection, imageEnhancements } from './collections/images'\nimport { createPurgeEndpoint, createTransformEndpoint, type TransformEndpointConfig } from './endpoints/transform'\nimport { stashConfig } from './lib/configStash'\nimport { mergeCollection } from './lib/mergeCollection'\nimport { SHARP_INSTALL_HINT } from './transform/getVariantBytes'\nimport { DEFAULT_CONSTRAINTS, DEFAULT_PIXEL_STEP } from './transform/params'\nimport { loadSharp } from './transform/sharpInstance'\n\nexport interface ImagesPluginOptions {\n /**\n * When false, the plugin registers NO collections, endpoints, or hooks. This is\n * \"not installed\", not \"temporarily disabled\": on SQL adapters, flipping it off for\n * an existing project produces a migration that DROPS the images / generated-images\n * tables (and their data). Defaults to true.\n */\n enabled?: boolean\n /**\n * Slug of an EXISTING upload collection to add the image pipeline to (focal UI, the\n * `variants` join, the purge hooks, and `upload.focalPoint`), instead of creating the\n * default `images` collection. Use this when your project already has a `media`/`images`\n * upload collection — no second collection, no migration. The target must be an upload\n * collection (you own the collection's `upload` config, including any `imageSizes`).\n */\n extendCollection?: string\n /**\n * Override for the Images collection. Top-level keys replace, but\n * `upload`/`access`/`admin` are deep-merged and `fields`/`hooks` are merged. Note\n * `fields` are APPENDED (not replaced) — don't redeclare a base field's `name`\n * (e.g. `alt`/`variants`), or Payload errors on the duplicate. With `extendCollection`,\n * these tweaks are merged onto the target collection instead.\n */\n imagesOverrides?: Partial<CollectionConfig>\n /** Override for the hidden generated-images (variant cache) collection. */\n generatedImagesOverrides?: Partial<CollectionConfig>\n /**\n * The project-wide srcset widths, set once and applied uniformly to the API virtual `srcset`,\n * `<ResponsiveImage>`, and the endpoint's anti-DoS dimension grid. Two forms:\n * - a **number** (default 50): the width increment AND the grid the endpoint snaps requests\n * to — a bigger step means fewer srcset widths and fewer cached variants.\n * - an **array**: an explicit, non-linear width ladder (e.g. `[200, 450, 750, 1200, 2000]`)\n * for the srcset — denser where it matters, fewer entries than a fine linear step. The\n * endpoint's snap grid then falls back to the default 50, so use ladder widths that are\n * multiples of 50 (or set `transform.dimensionStep`) to have them pass through unchanged.\n *\n * `transform.maxDimension` caps the largest width in either form.\n */\n pixelStep?: number | number[]\n /** On-demand transform endpoint config. Pass `false` to not register the endpoints. */\n transform?: TransformEndpointConfig | false\n /** Render the focal + ratio-preview field and purge-variants button. Default true. */\n focalUI?: boolean\n /** Aspect ratios shown in the focal preview tiles. */\n previewRatios?: string[]\n /**\n * Add virtual `src` / `srcset` / `placeholderURL` / `thumbnailURL` fields, computed on read,\n * so optimized URLs ride along in every REST / GraphQL / Local-API response (and through\n * relationship population). Absolute when `serverURL` is set, relative otherwise. Default true;\n * defaults to false with `transform: false` (the URLs would 404 — an explicit true is honored,\n * with a boot warning).\n */\n virtualFields?: boolean\n /** Mark the `alt` field `localized: true` (requires Payload localization). Ignored with\n * `extendCollection`. Default false. */\n localizeAlt?: boolean\n /**\n * Accepted upload mime types for the `images` collection. Defaults to the raster formats the\n * transform pipeline can process (avif/webp/jpeg/png). Widen it to accept more (e.g. add\n * `'image/svg+xml'`) or narrow it — but the endpoint only meaningfully resizes/crops raster\n * images, so non-raster uploads are stored and served as-is. Ignored with `extendCollection`\n * (you own that collection's `upload.mimeTypes`).\n */\n mimeTypes?: string[]\n /**\n * Enable Payload's native **folder organization** on the managed collection (the created `images`,\n * or the `extendCollection` target) — adds a folder relationship to the schema so editors can\n * organize a large library. Default false.\n */\n folders?: boolean\n /**\n * Cap the *stored* original's longest edge, in px (applied once on upload via Payload's\n * `resizeOptions`). **Off by default — your original stays untouched** (so the collection can\n * double as original storage); set it only to bound storage. Ignored with `extendCollection`.\n */\n maxOriginalSize?: number\n /**\n * Inline LQIP placeholder for `<ResponsiveImage>`. A tiny, faithful (per aspect-ratio +\n * focal point) image is generated server-side, base64-inlined behind the real image, and\n * painted instantly with zero network. Pass `false` to disable it project-wide. Defaults:\n * 24px / quality 40 / webp.\n */\n placeholder?: false | PlaceholderConfig\n}\n\n/** Inline-LQIP placeholder settings (see {@link ImagesPluginOptions.placeholder}). */\nexport interface PlaceholderConfig {\n /**\n * Default longest edge of the LQIP, in px. Default 24. Keep it tiny — the LQIP is inlined as\n * base64 in **every** response, so size compounds: ~0.6 KB at 24px, ~2 KB at 48, ~3–4 KB at 64.\n * **24–64 is the sensible range**; past ~64 it stops being a placeholder and bloats payloads.\n */\n width?: number\n /** Encode quality for the LQIP. Default 40 (clamped to 20–70 — LQIPs gain nothing from more). */\n quality?: number\n /** LQIP encode format. Default `webp`. */\n format?: 'webp' | 'jpeg'\n /**\n * Hard ceiling for a per-read width override coming from the **untrusted** external door\n * (`req.context.lqip` / `X-LQIP`): such widths are clamped to this and snapped to a /8 grid.\n * Default 64. The trusted `<ResponsiveImage placeholder={…}>` prop is *not* bound by this\n * (it honors your value up to a typo guard) — raise this only if you let external callers pick\n * larger LQIPs, knowing each px inlines more base64. Recommended ≤ ~96.\n */\n maxWidth?: number\n}\n\n/** The resolved placeholder settings stashed for the component, or `false` when disabled. */\nexport type ResolvedPlaceholder = false | Required<PlaceholderConfig>\n\n/** Fill in the placeholder defaults (24px / q40 / webp / maxWidth 64), or `false` when disabled. */\nexport const resolvePlaceholder = (p: ImagesPluginOptions['placeholder']): ResolvedPlaceholder =>\n p === false ? false : { width: p?.width ?? 24, quality: p?.quality ?? 40, format: p?.format ?? 'webp', maxWidth: p?.maxWidth ?? 64 }\n\n/**\n * Registers the `images` (source) and hidden `generated-images` (variant cache) collections,\n * plus the on-demand transform + purge endpoints.\n *\n * Uploads store only the original; every rendered size is generated the first time a page asks\n * for it (resized and cropped to the focal point set in the admin), then cached in\n * `generated-images` so it's only ever built once. The LQIP placeholder is generated on demand by\n * `<ResponsiveImage>` (a tiny inline base64, via the same variant cache) — nothing is stored on the\n * source doc.\n *\n * Pass `extendCollection: '<slug>'` to add the pipeline to an upload collection you already have\n * instead of creating `images`. The transform endpoint mounts at `/api/img`; do not name a\n * collection `img` or it shadows the endpoint.\n */\nexport const imagesPlugin =\n (opts: ImagesPluginOptions = {}): Plugin =>\n (config: Config): Config => {\n const {\n enabled = true,\n extendCollection,\n imagesOverrides,\n generatedImagesOverrides,\n pixelStep = DEFAULT_PIXEL_STEP,\n transform = {},\n focalUI = true,\n previewRatios,\n localizeAlt = false,\n mimeTypes,\n folders,\n maxOriginalSize,\n placeholder,\n } = opts\n if (!enabled) return config\n\n const transformCfg: TransformEndpointConfig = transform === false ? {} : transform\n const variantSlug = transformCfg.variantSlug || GENERATED_IMAGES_SLUG\n // With extendCollection, the source IS that collection; otherwise the default `images`.\n const sourceSlug = extendCollection || transformCfg.sourceSlug || 'images'\n const basePath = '/img'\n const purgePath = `${basePath}/purge`\n const apiRoute = config.routes?.api ?? '/api'\n const endpointsEnabled = transform !== false\n // With transforms off the virtual URL fields would point at an unregistered endpoint, so they\n // default off; an explicit `virtualFields: true` is honored (warned at boot — the URLs 404).\n const virtualFields = opts.virtualFields ?? endpointsEnabled\n\n const generated = mergeCollection(createGeneratedImagesCollection({ slug: variantSlug, sourceSlug }), generatedImagesOverrides)\n\n let collections: CollectionConfig[]\n if (extendCollection) {\n const target = (config.collections ?? []).find((c) => c.slug === extendCollection)\n if (!target) throw new Error(`[payload-images] extendCollection: collection '${extendCollection}' not found`)\n if (!target.upload) throw new Error(`[payload-images] extendCollection: collection '${extendCollection}' is not an upload collection`)\n // On-demand admin thumbnail only when the endpoint exists AND the target hasn't set its own.\n const ownThumbnail =\n (typeof target.upload === 'object' && !!target.upload.adminThumbnail) ||\n !!(target.admin as { thumbnail?: unknown } | undefined)?.thumbnail\n const enh = imageEnhancements({\n focalUI,\n previewRatios,\n variantSlug,\n purgePath,\n virtualFields,\n folders,\n apiRoute,\n endpointsEnabled,\n adminThumbnail: !endpointsEnabled || ownThumbnail ? false : undefined,\n })\n // Merge, never overwrite: the target's own defaultPopulate / forceSelect entries win over\n // (and add to) the parity defaults, so its populated reads keep behaving as it declared.\n const parity: Partial<CollectionConfig> = {\n ...enh,\n defaultPopulate: {\n ...(enh.defaultPopulate as Record<string, unknown>),\n ...(target.defaultPopulate as Record<string, unknown> | undefined),\n } as CollectionConfig['defaultPopulate'],\n ...(enh.forceSelect || target.forceSelect\n ? {\n forceSelect: {\n ...(enh.forceSelect as Record<string, unknown> | undefined),\n ...(target.forceSelect as Record<string, unknown> | undefined),\n } as CollectionConfig['forceSelect'],\n }\n : {}),\n }\n const enhanced = mergeCollection(mergeCollection(target, parity), imagesOverrides)\n collections = [...(config.collections ?? []).filter((c) => c.slug !== extendCollection), enhanced, generated]\n } else {\n const images = mergeCollection(\n // No transform endpoint → no on-demand thumbnail; fall back to Payload's default.\n createImagesCollection({\n focalUI,\n previewRatios,\n variantSlug,\n purgePath,\n virtualFields,\n localizeAlt,\n mimeTypes,\n folders,\n maxOriginalSize,\n apiRoute,\n endpointsEnabled,\n adminThumbnail: endpointsEnabled ? undefined : false,\n }),\n imagesOverrides,\n )\n collections = [...(config.collections ?? []), images, generated]\n }\n\n // A custom transform.sourceSlug gets the same guard extendCollection does — a typo here would\n // otherwise just 404/500 every image request at runtime. Checked against the FINAL collections\n // array (registration is unchanged; the default `images` collection is still created).\n if (!extendCollection && transformCfg.sourceSlug) {\n const src = collections.find((c) => c.slug === transformCfg.sourceSlug)\n if (!src) throw new Error(`[payload-images] transform.sourceSlug: collection '${transformCfg.sourceSlug}' not found`)\n if (!src.upload)\n throw new Error(`[payload-images] transform.sourceSlug: collection '${transformCfg.sourceSlug}' is not an upload collection`)\n }\n\n const endpoints =\n transform === false\n ? config.endpoints\n : [\n ...(config.endpoints ?? []),\n createPurgeEndpoint({ variantSlug, sourceSlug }),\n // A custom width ladder (array) can't double as the endpoint's numeric snap grid, so the\n // grid falls back to the default; a numeric pixelStep stays coupled to it as before.\n createTransformEndpoint({\n dimensionStep: Array.isArray(pixelStep) ? DEFAULT_PIXEL_STEP : pixelStep,\n ...transformCfg,\n variantSlug,\n sourceSlug,\n }),\n ]\n\n const baseSegment = basePath.replace(/^\\//, '').split('/')[0]\n const shadowed = transform !== false && collections.some((c) => c.slug === baseSegment)\n // These options only shape the CREATED `images` collection; with extendCollection the target's\n // own upload config governs, so a set value would be silently ignored — surface it at boot.\n const ignoredWithExtend = extendCollection\n ? (['mimeTypes', 'localizeAlt', 'maxOriginalSize'] as const).filter((k) => opts[k] !== undefined)\n : []\n\n return {\n ...config,\n collections,\n endpoints,\n // Stash the resolved config so decoupled tooling (an OG/sitemap generator, a CDN purge\n // script, a migration) and `<ResponsiveImage>` (inline LQIP) can read the slugs + options\n // from just `payload`, no import.\n custom: {\n ...config.custom,\n payloadImages: {\n options: opts,\n sourceSlug,\n variantSlug,\n basePath,\n pixelStep,\n placeholder: resolvePlaceholder(placeholder),\n maxInputPixels: transformCfg.maxInputPixels ?? DEFAULT_CONSTRAINTS.maxInputPixels,\n },\n },\n onInit: async (payload) => {\n await config.onInit?.(payload)\n // Remember the app's config so <ResponsiveImage> resolves it from globalThis — no\n // `@payload-config` alias (and thus no transpilePackages) required once Payload has booted.\n stashConfig(payload.config)\n if (shadowed) {\n payload.logger.warn(\n `[payload-images] a collection is named \"${baseSegment}\", which shadows the transform endpoint at /api/${baseSegment} — rename the collection so it doesn't collide.`,\n )\n }\n if (ignoredWithExtend.length) {\n payload.logger.warn(\n `[payload-images] extendCollection: option(s) ${ignoredWithExtend.join(', ')} are ignored — you own '${extendCollection}'s upload config; set the equivalent on the collection itself.`,\n )\n }\n if (!endpointsEnabled && opts.virtualFields === true) {\n payload.logger.warn(\n '[payload-images] virtualFields: true with transform: false — the virtual src/srcset/placeholderURL/thumbnailURL fields point at the unregistered transform endpoint and will 404.',\n )\n }\n // Probe Sharp once at boot so a missing install / broken native binding surfaces as one\n // actionable error here instead of a 500 on the first image request.\n try {\n await loadSharp()\n } catch (err) {\n payload.logger.error(\n `[payload-images] sharp failed to load — transforms and LQIPs will fail; ${SHARP_INSTALL_HINT}. (${String(err)})`,\n )\n }\n },\n }\n }\n\nexport default imagesPlugin\n"],"names":["createGeneratedImagesCollection","GENERATED_IMAGES_SLUG","createImagesCollection","imageEnhancements","createPurgeEndpoint","createTransformEndpoint","stashConfig","mergeCollection","SHARP_INSTALL_HINT","DEFAULT_CONSTRAINTS","DEFAULT_PIXEL_STEP","loadSharp","resolvePlaceholder","p","width","quality","format","maxWidth","imagesPlugin","opts","config","enabled","extendCollection","imagesOverrides","generatedImagesOverrides","pixelStep","transform","focalUI","previewRatios","localizeAlt","mimeTypes","folders","maxOriginalSize","placeholder","transformCfg","variantSlug","sourceSlug","basePath","purgePath","apiRoute","routes","api","endpointsEnabled","virtualFields","generated","slug","collections","target","find","c","Error","upload","ownThumbnail","adminThumbnail","admin","thumbnail","enh","undefined","parity","defaultPopulate","forceSelect","enhanced","filter","images","src","endpoints","dimensionStep","Array","isArray","baseSegment","replace","split","shadowed","some","ignoredWithExtend","k","custom","payloadImages","options","maxInputPixels","onInit","payload","logger","warn","length","join","err","error","String"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory single-flight: collapse concurrent calls for the same key onto one shared
|
|
3
|
+
* promise, clearing the entry the moment it settles. Two uses in the transform endpoint:
|
|
4
|
+
*
|
|
5
|
+
* - dedupe the source-identity read across the many srcset widths of one `<img>` that
|
|
6
|
+
* arrive together (all the same source id), so a cold page does one source lookup per
|
|
7
|
+
* image instead of one per requested width;
|
|
8
|
+
* - coalesce variant generation under a thundering herd (many requests for the same
|
|
9
|
+
* uncached transform), so the expensive read + encode runs once and the rest await it.
|
|
10
|
+
*
|
|
11
|
+
* Per-process and zero-TTL: because the entry is dropped on settle, there's no staleness
|
|
12
|
+
* window — the next call after a burst reads fresh. (Across separate serverless instances
|
|
13
|
+
* it can't dedupe; it's a best-effort per-instance optimization, like the concurrency gate.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const createSingleFlight: <K, V>() => ((key: K, fn: () => Promise<V>) => Promise<V>);
|
|
16
|
+
//# sourceMappingURL=coalesce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/transform/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,CAAC,OAAK,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAWxF,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory single-flight: collapse concurrent calls for the same key onto one shared
|
|
3
|
+
* promise, clearing the entry the moment it settles. Two uses in the transform endpoint:
|
|
4
|
+
*
|
|
5
|
+
* - dedupe the source-identity read across the many srcset widths of one `<img>` that
|
|
6
|
+
* arrive together (all the same source id), so a cold page does one source lookup per
|
|
7
|
+
* image instead of one per requested width;
|
|
8
|
+
* - coalesce variant generation under a thundering herd (many requests for the same
|
|
9
|
+
* uncached transform), so the expensive read + encode runs once and the rest await it.
|
|
10
|
+
*
|
|
11
|
+
* Per-process and zero-TTL: because the entry is dropped on settle, there's no staleness
|
|
12
|
+
* window — the next call after a burst reads fresh. (Across separate serverless instances
|
|
13
|
+
* it can't dedupe; it's a best-effort per-instance optimization, like the concurrency gate.)
|
|
14
|
+
*/ export const createSingleFlight = ()=>{
|
|
15
|
+
const inflight = new Map();
|
|
16
|
+
return (key, fn)=>{
|
|
17
|
+
const existing = inflight.get(key);
|
|
18
|
+
if (existing) return existing;
|
|
19
|
+
const p = Promise.resolve().then(fn).finally(()=>inflight.delete(key));
|
|
20
|
+
inflight.set(key, p);
|
|
21
|
+
return p;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=coalesce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transform/coalesce.ts"],"sourcesContent":["/**\n * In-memory single-flight: collapse concurrent calls for the same key onto one shared\n * promise, clearing the entry the moment it settles. Two uses in the transform endpoint:\n *\n * - dedupe the source-identity read across the many srcset widths of one `<img>` that\n * arrive together (all the same source id), so a cold page does one source lookup per\n * image instead of one per requested width;\n * - coalesce variant generation under a thundering herd (many requests for the same\n * uncached transform), so the expensive read + encode runs once and the rest await it.\n *\n * Per-process and zero-TTL: because the entry is dropped on settle, there's no staleness\n * window — the next call after a burst reads fresh. (Across separate serverless instances\n * it can't dedupe; it's a best-effort per-instance optimization, like the concurrency gate.)\n */\nexport const createSingleFlight = <K, V>(): ((key: K, fn: () => Promise<V>) => Promise<V>) => {\n const inflight = new Map<K, Promise<V>>()\n return (key, fn) => {\n const existing = inflight.get(key)\n if (existing) return existing\n const p = Promise.resolve()\n .then(fn)\n .finally(() => inflight.delete(key))\n inflight.set(key, p)\n return p\n }\n}\n"],"names":["createSingleFlight","inflight","Map","key","fn","existing","get","p","Promise","resolve","then","finally","delete","set"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMA,qBAAqB;IAChC,MAAMC,WAAW,IAAIC;IACrB,OAAO,CAACC,KAAKC;QACX,MAAMC,WAAWJ,SAASK,GAAG,CAACH;QAC9B,IAAIE,UAAU,OAAOA;QACrB,MAAME,IAAIC,QAAQC,OAAO,GACtBC,IAAI,CAACN,IACLO,OAAO,CAAC,IAAMV,SAASW,MAAM,CAACT;QACjCF,SAASY,GAAG,CAACV,KAAKI;QAClB,OAAOA;IACT;AACF,EAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure focal-crop geometry — no Sharp, no Node APIs. Isomorphic on purpose: the
|
|
3
|
+
* transform endpoint (server) and the admin focal preview (client) both import it, so
|
|
4
|
+
* what the author sets against the preview is exactly what the endpoint renders.
|
|
5
|
+
*/
|
|
6
|
+
export declare const clamp: (v: number, lo: number, hi: number) => number;
|
|
7
|
+
export interface CropGeometry {
|
|
8
|
+
/** Resize the source to these dims first (preserves aspect, covers the target). */
|
|
9
|
+
resizeWidth: number;
|
|
10
|
+
resizeHeight: number;
|
|
11
|
+
/** Then extract this window (centered on the focal point, clamped to bounds). */
|
|
12
|
+
left: number;
|
|
13
|
+
top: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cover-crop geometry honoring a focal point. Given the (orientation-corrected)
|
|
19
|
+
* source dims, a target box (one side may be omitted and is derived from the
|
|
20
|
+
* source aspect), and focal percentages (0–100), returns the resize dims and the
|
|
21
|
+
* extract window centered on the focal point.
|
|
22
|
+
*/
|
|
23
|
+
export declare const coverCropGeometry: (sw: number, sh: number, targetW: number | undefined, targetH: number | undefined, focalX?: number, focalY?: number) => CropGeometry;
|
|
24
|
+
/**
|
|
25
|
+
* No-upscale clamp: shrink a target box so it fits within the source, preserving the
|
|
26
|
+
* box's aspect ratio. A request larger than the original therefore renders at the
|
|
27
|
+
* source's resolution instead of being enlarged (saves work + storage, and bounds
|
|
28
|
+
* how large a variant can get regardless of the requested dimensions).
|
|
29
|
+
*/
|
|
30
|
+
export declare const fitWithinSource: (tw: number, th: number, sw: number, sh: number) => {
|
|
31
|
+
w: number;
|
|
32
|
+
h: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The CSS `object-position` percentages (x, y) that reproduce {@link coverCropGeometry}
|
|
36
|
+
* for a box of the given aspect ratio. A plain `object-position: <focalX>% <focalY>%`
|
|
37
|
+
* pans *proportionally* (subject lands at focal% of the frame); the endpoint instead
|
|
38
|
+
* centers the focal point in the crop and only drifts near the source edges. This maps
|
|
39
|
+
* a focal point to the equivalent window offset so an `object-fit: cover` preview
|
|
40
|
+
* matches the endpoint exactly. Falls back to centered when an axis isn't cropped.
|
|
41
|
+
*/
|
|
42
|
+
export declare const coverObjectPosition: (sw: number, sh: number, ratioW: number, ratioH: number, focalX?: number, focalY?: number) => {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=geometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/transform/geometry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,KAAK,GAAI,GAAG,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,KAAG,MAAuC,CAAA;AAEjG,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,IAAI,MAAM,EACV,IAAI,MAAM,EACV,SAAS,MAAM,GAAG,SAAS,EAC3B,SAAS,MAAM,GAAG,SAAS,EAC3B,eAAW,EACX,eAAW,KACV,YAWF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,KAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAGtG,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAC9B,IAAI,MAAM,EACV,IAAI,MAAM,EACV,QAAQ,MAAM,EACd,QAAQ,MAAM,EACd,eAAW,EACX,eAAW,KACV;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAOxB,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure focal-crop geometry — no Sharp, no Node APIs. Isomorphic on purpose: the
|
|
3
|
+
* transform endpoint (server) and the admin focal preview (client) both import it, so
|
|
4
|
+
* what the author sets against the preview is exactly what the endpoint renders.
|
|
5
|
+
*/ export const clamp = (v, lo, hi)=>Math.max(lo, Math.min(hi, v));
|
|
6
|
+
/**
|
|
7
|
+
* Cover-crop geometry honoring a focal point. Given the (orientation-corrected)
|
|
8
|
+
* source dims, a target box (one side may be omitted and is derived from the
|
|
9
|
+
* source aspect), and focal percentages (0–100), returns the resize dims and the
|
|
10
|
+
* extract window centered on the focal point.
|
|
11
|
+
*/ export const coverCropGeometry = (sw, sh, targetW, targetH, focalX = 50, focalY = 50)=>{
|
|
12
|
+
const tw = targetW ?? Math.max(1, Math.round(targetH * sw / sh));
|
|
13
|
+
const th = targetH ?? Math.max(1, Math.round(targetW * sh / sw));
|
|
14
|
+
const scale = Math.max(tw / sw, th / sh);
|
|
15
|
+
const resizeWidth = Math.max(tw, Math.round(sw * scale));
|
|
16
|
+
const resizeHeight = Math.max(th, Math.round(sh * scale));
|
|
17
|
+
const fx = clamp(focalX, 0, 100) / 100 * resizeWidth;
|
|
18
|
+
const fy = clamp(focalY, 0, 100) / 100 * resizeHeight;
|
|
19
|
+
const left = clamp(Math.round(fx - tw / 2), 0, resizeWidth - tw);
|
|
20
|
+
const top = clamp(Math.round(fy - th / 2), 0, resizeHeight - th);
|
|
21
|
+
return {
|
|
22
|
+
resizeWidth,
|
|
23
|
+
resizeHeight,
|
|
24
|
+
left,
|
|
25
|
+
top,
|
|
26
|
+
width: tw,
|
|
27
|
+
height: th
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* No-upscale clamp: shrink a target box so it fits within the source, preserving the
|
|
32
|
+
* box's aspect ratio. A request larger than the original therefore renders at the
|
|
33
|
+
* source's resolution instead of being enlarged (saves work + storage, and bounds
|
|
34
|
+
* how large a variant can get regardless of the requested dimensions).
|
|
35
|
+
*/ export const fitWithinSource = (tw, th, sw, sh)=>{
|
|
36
|
+
const f = Math.min(1, sw / tw, sh / th);
|
|
37
|
+
return {
|
|
38
|
+
w: Math.max(1, Math.round(tw * f)),
|
|
39
|
+
h: Math.max(1, Math.round(th * f))
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The CSS `object-position` percentages (x, y) that reproduce {@link coverCropGeometry}
|
|
44
|
+
* for a box of the given aspect ratio. A plain `object-position: <focalX>% <focalY>%`
|
|
45
|
+
* pans *proportionally* (subject lands at focal% of the frame); the endpoint instead
|
|
46
|
+
* centers the focal point in the crop and only drifts near the source edges. This maps
|
|
47
|
+
* a focal point to the equivalent window offset so an `object-fit: cover` preview
|
|
48
|
+
* matches the endpoint exactly. Falls back to centered when an axis isn't cropped.
|
|
49
|
+
*/ export const coverObjectPosition = (sw, sh, ratioW, ratioH, focalX = 50, focalY = 50)=>{
|
|
50
|
+
if (sw <= 0 || sh <= 0 || ratioW <= 0 || ratioH <= 0) return {
|
|
51
|
+
x: 50,
|
|
52
|
+
y: 50
|
|
53
|
+
};
|
|
54
|
+
const k = 1000 / Math.max(ratioW, ratioH);
|
|
55
|
+
const g = coverCropGeometry(sw, sh, Math.round(ratioW * k), Math.round(ratioH * k), focalX, focalY);
|
|
56
|
+
const ox = g.resizeWidth - g.width;
|
|
57
|
+
const oy = g.resizeHeight - g.height;
|
|
58
|
+
return {
|
|
59
|
+
x: ox >= 1 ? g.left / ox * 100 : 50,
|
|
60
|
+
y: oy >= 1 ? g.top / oy * 100 : 50
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transform/geometry.ts"],"sourcesContent":["/**\n * Pure focal-crop geometry — no Sharp, no Node APIs. Isomorphic on purpose: the\n * transform endpoint (server) and the admin focal preview (client) both import it, so\n * what the author sets against the preview is exactly what the endpoint renders.\n */\n\nexport const clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v))\n\nexport interface CropGeometry {\n /** Resize the source to these dims first (preserves aspect, covers the target). */\n resizeWidth: number\n resizeHeight: number\n /** Then extract this window (centered on the focal point, clamped to bounds). */\n left: number\n top: number\n width: number\n height: number\n}\n\n/**\n * Cover-crop geometry honoring a focal point. Given the (orientation-corrected)\n * source dims, a target box (one side may be omitted and is derived from the\n * source aspect), and focal percentages (0–100), returns the resize dims and the\n * extract window centered on the focal point.\n */\nexport const coverCropGeometry = (\n sw: number,\n sh: number,\n targetW: number | undefined,\n targetH: number | undefined,\n focalX = 50,\n focalY = 50,\n): CropGeometry => {\n const tw = targetW ?? Math.max(1, Math.round(((targetH as number) * sw) / sh))\n const th = targetH ?? Math.max(1, Math.round(((targetW as number) * sh) / sw))\n const scale = Math.max(tw / sw, th / sh)\n const resizeWidth = Math.max(tw, Math.round(sw * scale))\n const resizeHeight = Math.max(th, Math.round(sh * scale))\n const fx = (clamp(focalX, 0, 100) / 100) * resizeWidth\n const fy = (clamp(focalY, 0, 100) / 100) * resizeHeight\n const left = clamp(Math.round(fx - tw / 2), 0, resizeWidth - tw)\n const top = clamp(Math.round(fy - th / 2), 0, resizeHeight - th)\n return { resizeWidth, resizeHeight, left, top, width: tw, height: th }\n}\n\n/**\n * No-upscale clamp: shrink a target box so it fits within the source, preserving the\n * box's aspect ratio. A request larger than the original therefore renders at the\n * source's resolution instead of being enlarged (saves work + storage, and bounds\n * how large a variant can get regardless of the requested dimensions).\n */\nexport const fitWithinSource = (tw: number, th: number, sw: number, sh: number): { w: number; h: number } => {\n const f = Math.min(1, sw / tw, sh / th)\n return { w: Math.max(1, Math.round(tw * f)), h: Math.max(1, Math.round(th * f)) }\n}\n\n/**\n * The CSS `object-position` percentages (x, y) that reproduce {@link coverCropGeometry}\n * for a box of the given aspect ratio. A plain `object-position: <focalX>% <focalY>%`\n * pans *proportionally* (subject lands at focal% of the frame); the endpoint instead\n * centers the focal point in the crop and only drifts near the source edges. This maps\n * a focal point to the equivalent window offset so an `object-fit: cover` preview\n * matches the endpoint exactly. Falls back to centered when an axis isn't cropped.\n */\nexport const coverObjectPosition = (\n sw: number,\n sh: number,\n ratioW: number,\n ratioH: number,\n focalX = 50,\n focalY = 50,\n): { x: number; y: number } => {\n if (sw <= 0 || sh <= 0 || ratioW <= 0 || ratioH <= 0) return { x: 50, y: 50 }\n const k = 1000 / Math.max(ratioW, ratioH)\n const g = coverCropGeometry(sw, sh, Math.round(ratioW * k), Math.round(ratioH * k), focalX, focalY)\n const ox = g.resizeWidth - g.width\n const oy = g.resizeHeight - g.height\n return { x: ox >= 1 ? (g.left / ox) * 100 : 50, y: oy >= 1 ? (g.top / oy) * 100 : 50 }\n}\n"],"names":["clamp","v","lo","hi","Math","max","min","coverCropGeometry","sw","sh","targetW","targetH","focalX","focalY","tw","round","th","scale","resizeWidth","resizeHeight","fx","fy","left","top","width","height","fitWithinSource","f","w","h","coverObjectPosition","ratioW","ratioH","x","y","k","g","ox","oy"],"mappings":"AAAA;;;;CAIC,GAED,OAAO,MAAMA,QAAQ,CAACC,GAAWC,IAAYC,KAAuBC,KAAKC,GAAG,CAACH,IAAIE,KAAKE,GAAG,CAACH,IAAIF,IAAG;AAajG;;;;;CAKC,GACD,OAAO,MAAMM,oBAAoB,CAC/BC,IACAC,IACAC,SACAC,SACAC,SAAS,EAAE,EACXC,SAAS,EAAE;IAEX,MAAMC,KAAKJ,WAAWN,KAAKC,GAAG,CAAC,GAAGD,KAAKW,KAAK,CAAC,AAAEJ,UAAqBH,KAAMC;IAC1E,MAAMO,KAAKL,WAAWP,KAAKC,GAAG,CAAC,GAAGD,KAAKW,KAAK,CAAC,AAAEL,UAAqBD,KAAMD;IAC1E,MAAMS,QAAQb,KAAKC,GAAG,CAACS,KAAKN,IAAIQ,KAAKP;IACrC,MAAMS,cAAcd,KAAKC,GAAG,CAACS,IAAIV,KAAKW,KAAK,CAACP,KAAKS;IACjD,MAAME,eAAef,KAAKC,GAAG,CAACW,IAAIZ,KAAKW,KAAK,CAACN,KAAKQ;IAClD,MAAMG,KAAK,AAACpB,MAAMY,QAAQ,GAAG,OAAO,MAAOM;IAC3C,MAAMG,KAAK,AAACrB,MAAMa,QAAQ,GAAG,OAAO,MAAOM;IAC3C,MAAMG,OAAOtB,MAAMI,KAAKW,KAAK,CAACK,KAAKN,KAAK,IAAI,GAAGI,cAAcJ;IAC7D,MAAMS,MAAMvB,MAAMI,KAAKW,KAAK,CAACM,KAAKL,KAAK,IAAI,GAAGG,eAAeH;IAC7D,OAAO;QAAEE;QAAaC;QAAcG;QAAMC;QAAKC,OAAOV;QAAIW,QAAQT;IAAG;AACvE,EAAC;AAED;;;;;CAKC,GACD,OAAO,MAAMU,kBAAkB,CAACZ,IAAYE,IAAYR,IAAYC;IAClE,MAAMkB,IAAIvB,KAAKE,GAAG,CAAC,GAAGE,KAAKM,IAAIL,KAAKO;IACpC,OAAO;QAAEY,GAAGxB,KAAKC,GAAG,CAAC,GAAGD,KAAKW,KAAK,CAACD,KAAKa;QAAKE,GAAGzB,KAAKC,GAAG,CAAC,GAAGD,KAAKW,KAAK,CAACC,KAAKW;IAAI;AAClF,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,MAAMG,sBAAsB,CACjCtB,IACAC,IACAsB,QACAC,QACApB,SAAS,EAAE,EACXC,SAAS,EAAE;IAEX,IAAIL,MAAM,KAAKC,MAAM,KAAKsB,UAAU,KAAKC,UAAU,GAAG,OAAO;QAAEC,GAAG;QAAIC,GAAG;IAAG;IAC5E,MAAMC,IAAI,OAAO/B,KAAKC,GAAG,CAAC0B,QAAQC;IAClC,MAAMI,IAAI7B,kBAAkBC,IAAIC,IAAIL,KAAKW,KAAK,CAACgB,SAASI,IAAI/B,KAAKW,KAAK,CAACiB,SAASG,IAAIvB,QAAQC;IAC5F,MAAMwB,KAAKD,EAAElB,WAAW,GAAGkB,EAAEZ,KAAK;IAClC,MAAMc,KAAKF,EAAEjB,YAAY,GAAGiB,EAAEX,MAAM;IACpC,OAAO;QAAEQ,GAAGI,MAAM,IAAI,AAACD,EAAEd,IAAI,GAAGe,KAAM,MAAM;QAAIH,GAAGI,MAAM,IAAI,AAACF,EAAEb,GAAG,GAAGe,KAAM,MAAM;IAAG;AACvF,EAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import { type OutputFormat, type ParsedParams } from './params';
|
|
3
|
+
import { type UploadDocLike } from './source';
|
|
4
|
+
/** A resolved source doc: id + where-the-bytes-live + focal point. */
|
|
5
|
+
export type VariantSourceDoc = UploadDocLike & {
|
|
6
|
+
id: string | number;
|
|
7
|
+
focalX?: number | null;
|
|
8
|
+
focalY?: number | null;
|
|
9
|
+
};
|
|
10
|
+
/** Generation outcome (bytes or a typed failure). */
|
|
11
|
+
export type GenBytes = {
|
|
12
|
+
ok: true;
|
|
13
|
+
data: Buffer;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
status: number;
|
|
18
|
+
msg: string;
|
|
19
|
+
};
|
|
20
|
+
/** Optional generation coalescer (the endpoint passes its per-process single-flight; omit at render). */
|
|
21
|
+
type GenFlight = (key: string, fn: () => Promise<GenBytes>) => Promise<GenBytes>;
|
|
22
|
+
/** Result of {@link getOrCreateVariantBytes} — bytes + the cache key (for ETag), or a typed failure. */
|
|
23
|
+
export type VariantBytes = {
|
|
24
|
+
ok: true;
|
|
25
|
+
data: Buffer;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
key: string;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
status: number;
|
|
31
|
+
msg: string;
|
|
32
|
+
key: string;
|
|
33
|
+
};
|
|
34
|
+
export interface GetVariantBytesArgs {
|
|
35
|
+
payload: Payload;
|
|
36
|
+
/** Resolved source doc (id + filename/url + focal point). */
|
|
37
|
+
source: VariantSourceDoc;
|
|
38
|
+
/** Parsed + snapped transform params. */
|
|
39
|
+
params: ParsedParams;
|
|
40
|
+
/** Concrete output format (never `auto`). */
|
|
41
|
+
format: OutputFormat;
|
|
42
|
+
sourceSlug: string;
|
|
43
|
+
variantSlug: string;
|
|
44
|
+
/** Origin used to read originals/variants served from a relative/cloud URL. */
|
|
45
|
+
base: string;
|
|
46
|
+
/** Decompression-bomb / memory guard passed to Sharp. */
|
|
47
|
+
maxInputPixels: number;
|
|
48
|
+
/** Generation coalescer; the endpoint passes its single-flight, render-time callers omit it. */
|
|
49
|
+
genFlight?: GenFlight;
|
|
50
|
+
}
|
|
51
|
+
/** The actionable fix for a Sharp load failure — shared by the boot probe and the request-time catch. */
|
|
52
|
+
export declare const SHARP_INSTALL_HINT = "install it (`pnpm add sharp`) and externalize it in next.config (`serverExternalPackages: ['sharp']`)";
|
|
53
|
+
/**
|
|
54
|
+
* Return the bytes for one variant: cache hit → stored copy; miss → Sharp once, persist after
|
|
55
|
+
* the response (via Next's `after()`, falling back to fire-and-forget), then return the bytes.
|
|
56
|
+
* Generation is coalesced by cache key when a `genFlight` is supplied.
|
|
57
|
+
*/
|
|
58
|
+
export declare const getOrCreateVariantBytes: (args: GetVariantBytesArgs) => Promise<VariantBytes>;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=getVariantBytes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getVariantBytes.d.ts","sourceRoot":"","sources":["../../src/transform/getVariantBytes.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,SAAS,CAAA;AAGtD,OAAO,EAA+B,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5F,OAAO,EAA+B,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAE1E,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAEtH,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhH,yGAAyG;AACzG,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEhF,wGAAwG;AACxG,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9I,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,6DAA6D;IAC7D,MAAM,EAAE,gBAAgB,CAAA;IACxB,yCAAyC;IACzC,MAAM,EAAE,YAAY,CAAA;IACpB,6CAA6C;IAC7C,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAA;IACtB,gGAAgG;IAChG,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AA2BD,yGAAyG;AACzG,eAAO,MAAM,kBAAkB,0GAA0G,CAAA;AAEzI;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAU,MAAM,mBAAmB,KAAG,OAAO,CAAC,YAAY,CAoF7F,CAAA"}
|