@payloadcms/transformer-sharp 4.0.0-internal.58473b9
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 +105 -0
- package/dist/canResizeImage.d.ts +2 -0
- package/dist/canResizeImage.d.ts.map +1 -0
- package/dist/canResizeImage.js +12 -0
- package/dist/canResizeImage.js.map +1 -0
- package/dist/createSharpFromFile.d.ts +8 -0
- package/dist/createSharpFromFile.d.ts.map +1 -0
- package/dist/createSharpFromFile.js +31 -0
- package/dist/createSharpFromFile.js.map +1 -0
- package/dist/createSharpFromFile.spec.js +120 -0
- package/dist/createSharpFromFile.spec.js.map +1 -0
- package/dist/getImageResizeAction.d.ts +24 -0
- package/dist/getImageResizeAction.d.ts.map +1 -0
- package/dist/getImageResizeAction.js +56 -0
- package/dist/getImageResizeAction.js.map +1 -0
- package/dist/handleRequest.d.ts +14 -0
- package/dist/handleRequest.d.ts.map +1 -0
- package/dist/handleRequest.js +143 -0
- package/dist/handleRequest.js.map +1 -0
- package/dist/handleRequest.spec.js +188 -0
- package/dist/handleRequest.spec.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/initSharpCollections.d.ts +14 -0
- package/dist/initSharpCollections.d.ts.map +1 -0
- package/dist/initSharpCollections.js +84 -0
- package/dist/initSharpCollections.js.map +1 -0
- package/dist/initSharpCollections.spec.js +185 -0
- package/dist/initSharpCollections.spec.js.map +1 -0
- package/dist/mapWithBoundedConcurrency.d.ts +7 -0
- package/dist/mapWithBoundedConcurrency.d.ts.map +1 -0
- package/dist/mapWithBoundedConcurrency.js +26 -0
- package/dist/mapWithBoundedConcurrency.js.map +1 -0
- package/dist/mapWithBoundedConcurrency.spec.js +35 -0
- package/dist/mapWithBoundedConcurrency.spec.js.map +1 -0
- package/dist/optionallyAppendMetadata.d.ts +9 -0
- package/dist/optionallyAppendMetadata.d.ts.map +1 -0
- package/dist/optionallyAppendMetadata.js +17 -0
- package/dist/optionallyAppendMetadata.js.map +1 -0
- package/dist/parseDynamicResize.d.ts +17 -0
- package/dist/parseDynamicResize.d.ts.map +1 -0
- package/dist/parseDynamicResize.js +117 -0
- package/dist/parseDynamicResize.js.map +1 -0
- package/dist/parseDynamicResize.spec.js +123 -0
- package/dist/parseDynamicResize.spec.js.map +1 -0
- package/dist/prepareLegacyUpload.d.ts +14 -0
- package/dist/prepareLegacyUpload.d.ts.map +1 -0
- package/dist/prepareLegacyUpload.js +147 -0
- package/dist/prepareLegacyUpload.js.map +1 -0
- package/dist/prepareLegacyUpload.spec.js +95 -0
- package/dist/prepareLegacyUpload.spec.js.map +1 -0
- package/dist/sanitizeResizeConfig.d.ts +11 -0
- package/dist/sanitizeResizeConfig.d.ts.map +1 -0
- package/dist/sanitizeResizeConfig.js +20 -0
- package/dist/sanitizeResizeConfig.js.map +1 -0
- package/dist/sharpTransformer.d.ts +10 -0
- package/dist/sharpTransformer.d.ts.map +1 -0
- package/dist/sharpTransformer.js +122 -0
- package/dist/sharpTransformer.js.map +1 -0
- package/dist/sharpTransformer.spec.js +117 -0
- package/dist/sharpTransformer.spec.js.map +1 -0
- package/dist/transformFile.d.ts +12 -0
- package/dist/transformFile.d.ts.map +1 -0
- package/dist/transformFile.js +264 -0
- package/dist/transformFile.js.map +1 -0
- package/dist/transformFile.spec.js +259 -0
- package/dist/transformFile.spec.js.map +1 -0
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2026 Payload CMS, LLC <info@payloadcms.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Sharp Transformer for Payload
|
|
2
|
+
|
|
3
|
+
Payload's official [file transformer](https://payloadcms.com/docs/upload/transformers) for Sharp-based image processing: upload-time resizing/format conversion (the same behavior Payload has always provided) plus opt-in request-time dynamic resizing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @payloadcms/transformer-sharp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Register `sharpTransformer()` under `upload.transformers` in your Payload Config:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { buildConfig } from 'payload'
|
|
17
|
+
import { sharpTransformer } from '@payloadcms/transformer-sharp'
|
|
18
|
+
|
|
19
|
+
export default buildConfig({
|
|
20
|
+
upload: {
|
|
21
|
+
transformers: [sharpTransformer()],
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Upload-time image processing
|
|
27
|
+
|
|
28
|
+
Configure per-collection Sharp settings through `sharpTransformer({ collections })`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
sharpTransformer({
|
|
32
|
+
collections: {
|
|
33
|
+
media: {
|
|
34
|
+
variants: [
|
|
35
|
+
{ name: 'thumbnail', width: 400, height: 300 },
|
|
36
|
+
{ name: 'card', width: 768, height: 1024 },
|
|
37
|
+
],
|
|
38
|
+
resizeOptions: { width: 2048, height: 2048, withoutEnlargement: true },
|
|
39
|
+
formatOptions: { format: 'webp' },
|
|
40
|
+
trimOptions: undefined,
|
|
41
|
+
constructorOptions: undefined,
|
|
42
|
+
withMetadata: false,
|
|
43
|
+
crop: true,
|
|
44
|
+
focalPoint: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`variants`, `crop`, and `focalPoint` are also written back onto the collection's own sanitized `upload` config at startup (name/`admin`/`generateImageName` only, for `variants`), so the Admin Panel and generated types pick them up. Generated files are stored under each document's `variants` field.
|
|
51
|
+
|
|
52
|
+
### Dynamic (request-time) resizing
|
|
53
|
+
|
|
54
|
+
Dynamic resizing is **disabled by default** — resize query parameters are ignored and the original file is served. Enable it for every upload collection, or only the ones that need it:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
sharpTransformer({ dynamic: true })
|
|
58
|
+
sharpTransformer({ dynamic: { collections: ['media'] } })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Once enabled, requesting a stored image with recognized query parameters resizes it on the fly, without storing the result:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
GET /api/media/file/photo.png?width=400
|
|
65
|
+
GET /api/media/file/photo.png?width=400&height=300
|
|
66
|
+
GET /api/media/file/photo.png?width=400&withoutEnlargement=true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Configure limits and defaults for these requests:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
sharpTransformer({
|
|
73
|
+
dynamic: {
|
|
74
|
+
collections: ['media'], // omit to allow every upload collection
|
|
75
|
+
fit: 'cover', // default
|
|
76
|
+
position: 'center', // default
|
|
77
|
+
maxWidth: 4096, // default
|
|
78
|
+
maxHeight: 4096, // default
|
|
79
|
+
maxPixels: 16_777_216, // default
|
|
80
|
+
withoutEnlargement: false, // default
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Resized output is not cached, and anyone who can read a file can request it at any size within these limits. Before enabling dynamic resizing on a public collection, see [Securing dynamic resizing](https://payloadcms.com/docs/upload/transformers#securing-dynamic-resizing) for how to restrict it with access control, tighten the limits, and cache the output.
|
|
86
|
+
|
|
87
|
+
### Injecting a custom Sharp build
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import customSharp from 'sharp'
|
|
91
|
+
|
|
92
|
+
sharpTransformer({ sharp: customSharp })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See the [File Transformers](https://payloadcms.com/docs/upload/transformers) docs for the full transformer contract, access-control lifecycle, and non-goals (no caching, no CDN integration, no jobs queue).
|
|
96
|
+
|
|
97
|
+
## Migrating from Payload 3.x
|
|
98
|
+
|
|
99
|
+
If you previously configured `sharp`/`resizeOptions`/`imageSizes`/etc. directly on your Payload Config or collections, run:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npx @payloadcms/codemod --transform migrate-sharp-to-transformer
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See the [v4 migration guide](https://payloadcms.com/docs/migration-guide/v4#sharp-moved-out-of-core-into-payloadcmstransformer-sharp) for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canResizeImage.d.ts","sourceRoot":"","sources":["../src/canResizeImage.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMxD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/canResizeImage.ts"],"sourcesContent":["export function canResizeImage(mimeType: string): boolean {\n return (\n ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff', 'image/avif'].indexOf(\n mimeType,\n ) > -1\n )\n}\n"],"names":["canResizeImage","mimeType","indexOf"],"mappings":"AAAA,OAAO,SAASA,eAAeC,QAAgB;IAC7C,OACE;QAAC;QAAc;QAAa;QAAa;QAAc;QAAc;KAAa,CAACC,OAAO,CACxFD,YACE,CAAC;AAET"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Sharp, SharpOptions } from 'sharp';
|
|
2
|
+
import type { SharpDependency } from './types.js';
|
|
3
|
+
export declare function createSharpFromFile({ file, options, sharpDependency, }: {
|
|
4
|
+
file: File;
|
|
5
|
+
options?: SharpOptions;
|
|
6
|
+
sharpDependency: SharpDependency;
|
|
7
|
+
}): Promise<Sharp>;
|
|
8
|
+
//# sourceMappingURL=createSharpFromFile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSharpFromFile.d.ts","sourceRoot":"","sources":["../src/createSharpFromFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAIhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AASjD,wBAAsB,mBAAmB,CAAC,EACxC,IAAI,EACJ,OAAO,EACP,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,eAAe,EAAE,eAAe,CAAA;CACjC,GAAG,OAAO,CAAC,KAAK,CAAC,CASjB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getUploadFilePath } from 'payload/internal';
|
|
2
|
+
/**
|
|
3
|
+
* One upload can open the same in-memory source many times (metadata probes plus
|
|
4
|
+
* every variant, some concurrently). Sharing one buffer per File avoids a fresh
|
|
5
|
+
* full-size copy on each open.
|
|
6
|
+
*/ const inMemorySourceBuffers = new WeakMap();
|
|
7
|
+
export async function createSharpFromFile({ file, options, sharpDependency }) {
|
|
8
|
+
const filePath = getUploadFilePath(file);
|
|
9
|
+
if (filePath) {
|
|
10
|
+
return options && Object.keys(options).length > 0 ? sharpDependency(filePath, options) : sharpDependency(filePath);
|
|
11
|
+
}
|
|
12
|
+
return sharpDependency(await getInMemorySourceBuffer({
|
|
13
|
+
file
|
|
14
|
+
}), options);
|
|
15
|
+
}
|
|
16
|
+
function getInMemorySourceBuffer({ file }) {
|
|
17
|
+
const cachedBuffer = inMemorySourceBuffers.get(file);
|
|
18
|
+
if (cachedBuffer) {
|
|
19
|
+
return cachedBuffer;
|
|
20
|
+
}
|
|
21
|
+
const buffer = file.arrayBuffer().then((arrayBuffer)=>Buffer.from(arrayBuffer));
|
|
22
|
+
inMemorySourceBuffers.set(file, buffer);
|
|
23
|
+
buffer.catch(()=>{
|
|
24
|
+
if (inMemorySourceBuffers.get(file) === buffer) {
|
|
25
|
+
inMemorySourceBuffers.delete(file);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return buffer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=createSharpFromFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/createSharpFromFile.ts"],"sourcesContent":["import type { Sharp, SharpOptions } from 'sharp'\n\nimport { getUploadFilePath } from 'payload/internal'\n\nimport type { SharpDependency } from './types.js'\n\n/**\n * One upload can open the same in-memory source many times (metadata probes plus\n * every variant, some concurrently). Sharing one buffer per File avoids a fresh\n * full-size copy on each open.\n */\nconst inMemorySourceBuffers = new WeakMap<File, Promise<Buffer>>()\n\nexport async function createSharpFromFile({\n file,\n options,\n sharpDependency,\n}: {\n file: File\n options?: SharpOptions\n sharpDependency: SharpDependency\n}): Promise<Sharp> {\n const filePath = getUploadFilePath(file)\n if (filePath) {\n return options && Object.keys(options).length > 0\n ? sharpDependency(filePath, options)\n : sharpDependency(filePath)\n }\n\n return sharpDependency(await getInMemorySourceBuffer({ file }), options)\n}\n\nfunction getInMemorySourceBuffer({ file }: { file: File }): Promise<Buffer> {\n const cachedBuffer = inMemorySourceBuffers.get(file)\n if (cachedBuffer) {\n return cachedBuffer\n }\n\n const buffer = file.arrayBuffer().then((arrayBuffer) => Buffer.from(arrayBuffer))\n inMemorySourceBuffers.set(file, buffer)\n buffer.catch(() => {\n if (inMemorySourceBuffers.get(file) === buffer) {\n inMemorySourceBuffers.delete(file)\n }\n })\n\n return buffer\n}\n"],"names":["getUploadFilePath","inMemorySourceBuffers","WeakMap","createSharpFromFile","file","options","sharpDependency","filePath","Object","keys","length","getInMemorySourceBuffer","cachedBuffer","get","buffer","arrayBuffer","then","Buffer","from","set","catch","delete"],"mappings":"AAEA,SAASA,iBAAiB,QAAQ,mBAAkB;AAIpD;;;;CAIC,GACD,MAAMC,wBAAwB,IAAIC;AAElC,OAAO,eAAeC,oBAAoB,EACxCC,IAAI,EACJC,OAAO,EACPC,eAAe,EAKhB;IACC,MAAMC,WAAWP,kBAAkBI;IACnC,IAAIG,UAAU;QACZ,OAAOF,WAAWG,OAAOC,IAAI,CAACJ,SAASK,MAAM,GAAG,IAC5CJ,gBAAgBC,UAAUF,WAC1BC,gBAAgBC;IACtB;IAEA,OAAOD,gBAAgB,MAAMK,wBAAwB;QAAEP;IAAK,IAAIC;AAClE;AAEA,SAASM,wBAAwB,EAAEP,IAAI,EAAkB;IACvD,MAAMQ,eAAeX,sBAAsBY,GAAG,CAACT;IAC/C,IAAIQ,cAAc;QAChB,OAAOA;IACT;IAEA,MAAME,SAASV,KAAKW,WAAW,GAAGC,IAAI,CAAC,CAACD,cAAgBE,OAAOC,IAAI,CAACH;IACpEd,sBAAsBkB,GAAG,CAACf,MAAMU;IAChCA,OAAOM,KAAK,CAAC;QACX,IAAInB,sBAAsBY,GAAG,CAACT,UAAUU,QAAQ;YAC9Cb,sBAAsBoB,MAAM,CAACjB;QAC/B;IACF;IAEA,OAAOU;AACT"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { setUploadFilePath } from 'payload/internal';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { createSharpFromFile } from './createSharpFromFile.js';
|
|
4
|
+
describe('createSharpFromFile', ()=>{
|
|
5
|
+
it('passes a disk-backed File path directly to Sharp', async ()=>{
|
|
6
|
+
const file = new File([], 'photo.png', {
|
|
7
|
+
type: 'image/png'
|
|
8
|
+
});
|
|
9
|
+
const arrayBuffer = vi.spyOn(file, 'arrayBuffer');
|
|
10
|
+
const stream = vi.spyOn(file, 'stream');
|
|
11
|
+
const sharpFile = {};
|
|
12
|
+
const sharpDependency = vi.fn(()=>sharpFile);
|
|
13
|
+
setUploadFilePath(file, '/tmp/payload-upload.png');
|
|
14
|
+
await expect(createSharpFromFile({
|
|
15
|
+
file,
|
|
16
|
+
options: {
|
|
17
|
+
animated: true
|
|
18
|
+
},
|
|
19
|
+
sharpDependency
|
|
20
|
+
})).resolves.toBe(sharpFile);
|
|
21
|
+
expect(sharpDependency).toHaveBeenCalledWith('/tmp/payload-upload.png', {
|
|
22
|
+
animated: true
|
|
23
|
+
});
|
|
24
|
+
expect(arrayBuffer).not.toHaveBeenCalled();
|
|
25
|
+
expect(stream).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
it('should read an in-memory File once across concurrent calls', async ()=>{
|
|
28
|
+
const file = new File([
|
|
29
|
+
'image-bytes'
|
|
30
|
+
], 'photo.png', {
|
|
31
|
+
type: 'image/png'
|
|
32
|
+
});
|
|
33
|
+
const arrayBuffer = vi.spyOn(file, 'arrayBuffer');
|
|
34
|
+
const sharpDependency = vi.fn(()=>({}));
|
|
35
|
+
await Promise.all(Array.from({
|
|
36
|
+
length: 4
|
|
37
|
+
}, ()=>createSharpFromFile({
|
|
38
|
+
file,
|
|
39
|
+
sharpDependency
|
|
40
|
+
})));
|
|
41
|
+
expect(arrayBuffer).toHaveBeenCalledTimes(1);
|
|
42
|
+
expect(sharpDependency).toHaveBeenCalledTimes(4);
|
|
43
|
+
const inputs = sharpDependency.mock.calls.map(([input])=>input);
|
|
44
|
+
expect(inputs.every((input)=>input === inputs[0])).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('should read an in-memory File once across sequential calls', async ()=>{
|
|
47
|
+
const file = new File([
|
|
48
|
+
'image-bytes'
|
|
49
|
+
], 'photo.png', {
|
|
50
|
+
type: 'image/png'
|
|
51
|
+
});
|
|
52
|
+
const arrayBuffer = vi.spyOn(file, 'arrayBuffer');
|
|
53
|
+
const sharpDependency = vi.fn(()=>({}));
|
|
54
|
+
await createSharpFromFile({
|
|
55
|
+
file,
|
|
56
|
+
sharpDependency
|
|
57
|
+
});
|
|
58
|
+
await createSharpFromFile({
|
|
59
|
+
file,
|
|
60
|
+
options: {
|
|
61
|
+
animated: true
|
|
62
|
+
},
|
|
63
|
+
sharpDependency
|
|
64
|
+
});
|
|
65
|
+
expect(arrayBuffer).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(sharpDependency.mock.calls[0][0]).toBe(sharpDependency.mock.calls[1][0]);
|
|
67
|
+
expect(sharpDependency.mock.calls[1][1]).toEqual({
|
|
68
|
+
animated: true
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('should cache different File objects separately', async ()=>{
|
|
72
|
+
const firstFile = new File([
|
|
73
|
+
'first'
|
|
74
|
+
], 'first.png', {
|
|
75
|
+
type: 'image/png'
|
|
76
|
+
});
|
|
77
|
+
const secondFile = new File([
|
|
78
|
+
'second'
|
|
79
|
+
], 'second.png', {
|
|
80
|
+
type: 'image/png'
|
|
81
|
+
});
|
|
82
|
+
const firstArrayBuffer = vi.spyOn(firstFile, 'arrayBuffer');
|
|
83
|
+
const secondArrayBuffer = vi.spyOn(secondFile, 'arrayBuffer');
|
|
84
|
+
const sharpDependency = vi.fn(()=>({}));
|
|
85
|
+
await createSharpFromFile({
|
|
86
|
+
file: firstFile,
|
|
87
|
+
sharpDependency
|
|
88
|
+
});
|
|
89
|
+
await createSharpFromFile({
|
|
90
|
+
file: secondFile,
|
|
91
|
+
sharpDependency
|
|
92
|
+
});
|
|
93
|
+
expect(firstArrayBuffer).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(secondArrayBuffer).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(String(sharpDependency.mock.calls[0][0])).toBe('first');
|
|
96
|
+
expect(String(sharpDependency.mock.calls[1][0])).toBe('second');
|
|
97
|
+
});
|
|
98
|
+
it('should retry reading a File after a rejected read', async ()=>{
|
|
99
|
+
const file = new File([
|
|
100
|
+
'image-bytes'
|
|
101
|
+
], 'photo.png', {
|
|
102
|
+
type: 'image/png'
|
|
103
|
+
});
|
|
104
|
+
const readError = new Error('read failed');
|
|
105
|
+
const arrayBuffer = vi.spyOn(file, 'arrayBuffer').mockRejectedValueOnce(readError);
|
|
106
|
+
const sharpDependency = vi.fn(()=>({}));
|
|
107
|
+
await expect(createSharpFromFile({
|
|
108
|
+
file,
|
|
109
|
+
sharpDependency
|
|
110
|
+
})).rejects.toBe(readError);
|
|
111
|
+
await createSharpFromFile({
|
|
112
|
+
file,
|
|
113
|
+
sharpDependency
|
|
114
|
+
});
|
|
115
|
+
expect(arrayBuffer).toHaveBeenCalledTimes(2);
|
|
116
|
+
expect(String(sharpDependency.mock.calls[0][0])).toBe('image-bytes');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=createSharpFromFile.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/createSharpFromFile.spec.ts"],"sourcesContent":["import type { Sharp } from 'sharp'\n\nimport { setUploadFilePath } from 'payload/internal'\nimport { describe, expect, it, vi } from 'vitest'\n\nimport type { SharpDependency } from './types.js'\n\nimport { createSharpFromFile } from './createSharpFromFile.js'\n\ndescribe('createSharpFromFile', () => {\n it('passes a disk-backed File path directly to Sharp', async () => {\n const file = new File([], 'photo.png', { type: 'image/png' })\n const arrayBuffer = vi.spyOn(file, 'arrayBuffer')\n const stream = vi.spyOn(file, 'stream')\n const sharpFile = {} as Sharp\n const sharpDependency = vi.fn<SharpDependency>(() => sharpFile)\n setUploadFilePath(file, '/tmp/payload-upload.png')\n\n await expect(\n createSharpFromFile({ file, options: { animated: true }, sharpDependency }),\n ).resolves.toBe(sharpFile)\n expect(sharpDependency).toHaveBeenCalledWith('/tmp/payload-upload.png', { animated: true })\n expect(arrayBuffer).not.toHaveBeenCalled()\n expect(stream).not.toHaveBeenCalled()\n })\n\n it('should read an in-memory File once across concurrent calls', async () => {\n const file = new File(['image-bytes'], 'photo.png', { type: 'image/png' })\n const arrayBuffer = vi.spyOn(file, 'arrayBuffer')\n const sharpDependency = vi.fn<SharpDependency>(() => ({}) as Sharp)\n\n await Promise.all(\n Array.from({ length: 4 }, () => createSharpFromFile({ file, sharpDependency })),\n )\n\n expect(arrayBuffer).toHaveBeenCalledTimes(1)\n expect(sharpDependency).toHaveBeenCalledTimes(4)\n\n const inputs = sharpDependency.mock.calls.map(([input]) => input)\n\n expect(inputs.every((input) => input === inputs[0])).toBe(true)\n })\n\n it('should read an in-memory File once across sequential calls', async () => {\n const file = new File(['image-bytes'], 'photo.png', { type: 'image/png' })\n const arrayBuffer = vi.spyOn(file, 'arrayBuffer')\n const sharpDependency = vi.fn<SharpDependency>(() => ({}) as Sharp)\n\n await createSharpFromFile({ file, sharpDependency })\n await createSharpFromFile({ file, options: { animated: true }, sharpDependency })\n\n expect(arrayBuffer).toHaveBeenCalledTimes(1)\n expect(sharpDependency.mock.calls[0]![0]).toBe(sharpDependency.mock.calls[1]![0])\n expect(sharpDependency.mock.calls[1]![1]).toEqual({ animated: true })\n })\n\n it('should cache different File objects separately', async () => {\n const firstFile = new File(['first'], 'first.png', { type: 'image/png' })\n const secondFile = new File(['second'], 'second.png', { type: 'image/png' })\n const firstArrayBuffer = vi.spyOn(firstFile, 'arrayBuffer')\n const secondArrayBuffer = vi.spyOn(secondFile, 'arrayBuffer')\n const sharpDependency = vi.fn<SharpDependency>(() => ({}) as Sharp)\n\n await createSharpFromFile({ file: firstFile, sharpDependency })\n await createSharpFromFile({ file: secondFile, sharpDependency })\n\n expect(firstArrayBuffer).toHaveBeenCalledTimes(1)\n expect(secondArrayBuffer).toHaveBeenCalledTimes(1)\n expect(String(sharpDependency.mock.calls[0]![0])).toBe('first')\n expect(String(sharpDependency.mock.calls[1]![0])).toBe('second')\n })\n\n it('should retry reading a File after a rejected read', async () => {\n const file = new File(['image-bytes'], 'photo.png', { type: 'image/png' })\n const readError = new Error('read failed')\n const arrayBuffer = vi.spyOn(file, 'arrayBuffer').mockRejectedValueOnce(readError)\n const sharpDependency = vi.fn<SharpDependency>(() => ({}) as Sharp)\n\n await expect(createSharpFromFile({ file, sharpDependency })).rejects.toBe(readError)\n await createSharpFromFile({ file, sharpDependency })\n\n expect(arrayBuffer).toHaveBeenCalledTimes(2)\n expect(String(sharpDependency.mock.calls[0]![0])).toBe('image-bytes')\n })\n})\n"],"names":["setUploadFilePath","describe","expect","it","vi","createSharpFromFile","file","File","type","arrayBuffer","spyOn","stream","sharpFile","sharpDependency","fn","options","animated","resolves","toBe","toHaveBeenCalledWith","not","toHaveBeenCalled","Promise","all","Array","from","length","toHaveBeenCalledTimes","inputs","mock","calls","map","input","every","toEqual","firstFile","secondFile","firstArrayBuffer","secondArrayBuffer","String","readError","Error","mockRejectedValueOnce","rejects"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ImageSize, ProbedImageSize } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Determine whether or not to resize the image.
|
|
4
|
+
* - resize using image config
|
|
5
|
+
* - resize using image config with focal adjustments
|
|
6
|
+
* - do not resize at all
|
|
7
|
+
*
|
|
8
|
+
* `imageResizeConfig.withoutEnlargement`:
|
|
9
|
+
* - undefined [default]: uploading images with smaller width AND height than the image size will return null
|
|
10
|
+
* - false: always enlarge images to the image size
|
|
11
|
+
* - true: if the image is smaller than the image size, return the original image
|
|
12
|
+
*
|
|
13
|
+
* `imageResizeConfig.withoutReduction`:
|
|
14
|
+
* - false [default]: always enlarge images to the image size
|
|
15
|
+
* - true: if the image is smaller than the image size, return the original image
|
|
16
|
+
*
|
|
17
|
+
* @return 'omit' | 'resize' | 'resizeWithFocalPoint'
|
|
18
|
+
*/
|
|
19
|
+
export declare const getImageResizeAction: ({ dimensions: originalImage, hasFocalPoint, imageResizeConfig, }: {
|
|
20
|
+
dimensions: ProbedImageSize;
|
|
21
|
+
hasFocalPoint?: boolean;
|
|
22
|
+
imageResizeConfig: ImageSize;
|
|
23
|
+
}) => 'omit' | 'resize' | 'resizeWithFocalPoint';
|
|
24
|
+
//# sourceMappingURL=getImageResizeAction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getImageResizeAction.d.ts","sourceRoot":"","sources":["../src/getImageResizeAction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAIzD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,qEAI9B;IACD,UAAU,EAAE,eAAe,CAAA;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,SAAS,CAAA;CAC7B,KAAG,MAAM,GAAG,QAAQ,GAAG,sBA8CvB,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isNumber } from 'payload/shared';
|
|
2
|
+
/**
|
|
3
|
+
* Determine whether or not to resize the image.
|
|
4
|
+
* - resize using image config
|
|
5
|
+
* - resize using image config with focal adjustments
|
|
6
|
+
* - do not resize at all
|
|
7
|
+
*
|
|
8
|
+
* `imageResizeConfig.withoutEnlargement`:
|
|
9
|
+
* - undefined [default]: uploading images with smaller width AND height than the image size will return null
|
|
10
|
+
* - false: always enlarge images to the image size
|
|
11
|
+
* - true: if the image is smaller than the image size, return the original image
|
|
12
|
+
*
|
|
13
|
+
* `imageResizeConfig.withoutReduction`:
|
|
14
|
+
* - false [default]: always enlarge images to the image size
|
|
15
|
+
* - true: if the image is smaller than the image size, return the original image
|
|
16
|
+
*
|
|
17
|
+
* @return 'omit' | 'resize' | 'resizeWithFocalPoint'
|
|
18
|
+
*/ export const getImageResizeAction = ({ dimensions: originalImage, hasFocalPoint, imageResizeConfig })=>{
|
|
19
|
+
const { fit, withoutEnlargement, withoutReduction } = imageResizeConfig;
|
|
20
|
+
const targetWidth = imageResizeConfig.width;
|
|
21
|
+
const targetHeight = imageResizeConfig.height;
|
|
22
|
+
// prevent upscaling by default when x and y are both smaller than target image size
|
|
23
|
+
if (targetHeight && targetWidth) {
|
|
24
|
+
const originalImageIsSmallerXAndY = originalImage.width < targetWidth && originalImage.height < targetHeight;
|
|
25
|
+
if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) {
|
|
26
|
+
return 'omit' // prevent image size from being enlarged
|
|
27
|
+
;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (withoutEnlargement === undefined && (!targetWidth || !targetHeight)) {
|
|
31
|
+
if (targetWidth && originalImage.width < targetWidth || targetHeight && originalImage.height < targetHeight) {
|
|
32
|
+
return 'omit';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const originalImageIsSmallerXOrY = originalImage.width < targetWidth || originalImage.height < targetHeight;
|
|
36
|
+
if (fit === 'contain' || fit === 'inside') {
|
|
37
|
+
return 'resize';
|
|
38
|
+
}
|
|
39
|
+
if (!isNumber(targetHeight) && !isNumber(targetWidth)) {
|
|
40
|
+
return 'resize';
|
|
41
|
+
}
|
|
42
|
+
const targetAspectRatio = targetWidth / targetHeight;
|
|
43
|
+
const originalAspectRatio = originalImage.width / originalImage.height;
|
|
44
|
+
if (originalAspectRatio === targetAspectRatio) {
|
|
45
|
+
return 'resize';
|
|
46
|
+
}
|
|
47
|
+
if (withoutEnlargement && originalImageIsSmallerXOrY) {
|
|
48
|
+
return 'resize';
|
|
49
|
+
}
|
|
50
|
+
if (withoutReduction && !originalImageIsSmallerXOrY) {
|
|
51
|
+
return 'resize';
|
|
52
|
+
}
|
|
53
|
+
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=getImageResizeAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/getImageResizeAction.ts"],"sourcesContent":["import type { ImageSize, ProbedImageSize } from 'payload'\n\nimport { isNumber } from 'payload/shared'\n\n/**\n * Determine whether or not to resize the image.\n * - resize using image config\n * - resize using image config with focal adjustments\n * - do not resize at all\n *\n * `imageResizeConfig.withoutEnlargement`:\n * - undefined [default]: uploading images with smaller width AND height than the image size will return null\n * - false: always enlarge images to the image size\n * - true: if the image is smaller than the image size, return the original image\n *\n * `imageResizeConfig.withoutReduction`:\n * - false [default]: always enlarge images to the image size\n * - true: if the image is smaller than the image size, return the original image\n *\n * @return 'omit' | 'resize' | 'resizeWithFocalPoint'\n */\nexport const getImageResizeAction = ({\n dimensions: originalImage,\n hasFocalPoint,\n imageResizeConfig,\n}: {\n dimensions: ProbedImageSize\n hasFocalPoint?: boolean\n imageResizeConfig: ImageSize\n}): 'omit' | 'resize' | 'resizeWithFocalPoint' => {\n const { fit, withoutEnlargement, withoutReduction } = imageResizeConfig\n const targetWidth = imageResizeConfig.width!\n const targetHeight = imageResizeConfig.height!\n\n // prevent upscaling by default when x and y are both smaller than target image size\n if (targetHeight && targetWidth) {\n const originalImageIsSmallerXAndY =\n originalImage.width < targetWidth && originalImage.height < targetHeight\n if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) {\n return 'omit' // prevent image size from being enlarged\n }\n }\n\n if (withoutEnlargement === undefined && (!targetWidth || !targetHeight)) {\n if (\n (targetWidth && originalImage.width < targetWidth) ||\n (targetHeight && originalImage.height < targetHeight)\n ) {\n return 'omit'\n }\n }\n\n const originalImageIsSmallerXOrY =\n originalImage.width < targetWidth || originalImage.height < targetHeight\n if (fit === 'contain' || fit === 'inside') {\n return 'resize'\n }\n if (!isNumber(targetHeight) && !isNumber(targetWidth)) {\n return 'resize'\n }\n\n const targetAspectRatio = targetWidth / targetHeight\n const originalAspectRatio = originalImage.width / originalImage.height\n if (originalAspectRatio === targetAspectRatio) {\n return 'resize'\n }\n\n if (withoutEnlargement && originalImageIsSmallerXOrY) {\n return 'resize'\n }\n if (withoutReduction && !originalImageIsSmallerXOrY) {\n return 'resize'\n }\n\n return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'\n}\n"],"names":["isNumber","getImageResizeAction","dimensions","originalImage","hasFocalPoint","imageResizeConfig","fit","withoutEnlargement","withoutReduction","targetWidth","width","targetHeight","height","originalImageIsSmallerXAndY","undefined","originalImageIsSmallerXOrY","targetAspectRatio","originalAspectRatio"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,iBAAgB;AAEzC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,uBAAuB,CAAC,EACnCC,YAAYC,aAAa,EACzBC,aAAa,EACbC,iBAAiB,EAKlB;IACC,MAAM,EAAEC,GAAG,EAAEC,kBAAkB,EAAEC,gBAAgB,EAAE,GAAGH;IACtD,MAAMI,cAAcJ,kBAAkBK,KAAK;IAC3C,MAAMC,eAAeN,kBAAkBO,MAAM;IAE7C,oFAAoF;IACpF,IAAID,gBAAgBF,aAAa;QAC/B,MAAMI,8BACJV,cAAcO,KAAK,GAAGD,eAAeN,cAAcS,MAAM,GAAGD;QAC9D,IAAIJ,uBAAuBO,aAAaD,6BAA6B;YACnE,OAAO,OAAO,yCAAyC;;QACzD;IACF;IAEA,IAAIN,uBAAuBO,aAAc,CAAA,CAACL,eAAe,CAACE,YAAW,GAAI;QACvE,IACE,AAACF,eAAeN,cAAcO,KAAK,GAAGD,eACrCE,gBAAgBR,cAAcS,MAAM,GAAGD,cACxC;YACA,OAAO;QACT;IACF;IAEA,MAAMI,6BACJZ,cAAcO,KAAK,GAAGD,eAAeN,cAAcS,MAAM,GAAGD;IAC9D,IAAIL,QAAQ,aAAaA,QAAQ,UAAU;QACzC,OAAO;IACT;IACA,IAAI,CAACN,SAASW,iBAAiB,CAACX,SAASS,cAAc;QACrD,OAAO;IACT;IAEA,MAAMO,oBAAoBP,cAAcE;IACxC,MAAMM,sBAAsBd,cAAcO,KAAK,GAAGP,cAAcS,MAAM;IACtE,IAAIK,wBAAwBD,mBAAmB;QAC7C,OAAO;IACT;IAEA,IAAIT,sBAAsBQ,4BAA4B;QACpD,OAAO;IACT;IACA,IAAIP,oBAAoB,CAACO,4BAA4B;QACnD,OAAO;IACT;IAEA,OAAOX,gBAAgB,yBAAyB;AAClD,EAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HandleTransformRequestArgs, HandleTransformRequestResult } from 'payload';
|
|
2
|
+
import type { SharpDependency, SharpDynamicDefaults } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the request-time `handleRequest` stage: parses and validates the v1
|
|
5
|
+
* dynamic parameters (`width`, `height`, `withoutEnlargement`), retrieves the
|
|
6
|
+
* source exactly once — only after validation passes — and resizes it while
|
|
7
|
+
* preserving the source format. Unexpected Sharp errors are left to propagate
|
|
8
|
+
* uncaught; Payload's orchestrator logs and aborts the pipeline.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createHandleRequest({ dynamicDefaults, sharpDependency, }: {
|
|
11
|
+
dynamicDefaults: Required<SharpDynamicDefaults>;
|
|
12
|
+
sharpDependency: SharpDependency;
|
|
13
|
+
}): (args: HandleTransformRequestArgs) => Promise<HandleTransformRequestResult>;
|
|
14
|
+
//# sourceMappingURL=handleRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handleRequest.d.ts","sourceRoot":"","sources":["../src/handleRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAGvF,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAOvE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,eAAe,EACf,eAAe,GAChB,EAAE;IACD,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IAC/C,eAAe,EAAE,eAAe,CAAA;CACjC,GAAG,CAAC,IAAI,EAAE,0BAA0B,KAAK,OAAO,CAAC,4BAA4B,CAAC,CA2G9E"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { parseDynamicResize } from './parseDynamicResize.js';
|
|
2
|
+
// Must match generateFileData.ts's allow-list — the only MIME types Sharp auto-detects multi-frame animation for.
|
|
3
|
+
const ANIMATED_MIME_TYPES = [
|
|
4
|
+
'image/avif',
|
|
5
|
+
'image/gif',
|
|
6
|
+
'image/webp'
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Builds the request-time `handleRequest` stage: parses and validates the v1
|
|
10
|
+
* dynamic parameters (`width`, `height`, `withoutEnlargement`), retrieves the
|
|
11
|
+
* source exactly once — only after validation passes — and resizes it while
|
|
12
|
+
* preserving the source format. Unexpected Sharp errors are left to propagate
|
|
13
|
+
* uncaught; Payload's orchestrator logs and aborts the pipeline.
|
|
14
|
+
*/ export function createHandleRequest({ dynamicDefaults, sharpDependency }) {
|
|
15
|
+
return async ({ getSourceFile, mimeType, req })=>{
|
|
16
|
+
const parseResult = parseDynamicResize({
|
|
17
|
+
limits: dynamicDefaults,
|
|
18
|
+
searchParams: req.searchParams ?? new URLSearchParams()
|
|
19
|
+
});
|
|
20
|
+
if (!parseResult.isRouted) {
|
|
21
|
+
return {
|
|
22
|
+
status: 'continue'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (!parseResult.valid) {
|
|
26
|
+
return {
|
|
27
|
+
response: Response.json({
|
|
28
|
+
errors: [
|
|
29
|
+
{
|
|
30
|
+
message: parseResult.error
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}, {
|
|
34
|
+
status: 400
|
|
35
|
+
}),
|
|
36
|
+
status: 'complete'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (req.headers?.get('range')) {
|
|
40
|
+
return {
|
|
41
|
+
response: new Response(null, {
|
|
42
|
+
status: 416
|
|
43
|
+
}),
|
|
44
|
+
status: 'complete'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const source = await getSourceFile();
|
|
48
|
+
if (!source.ok) {
|
|
49
|
+
return {
|
|
50
|
+
response: source,
|
|
51
|
+
status: 'complete'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const sourceBuffer = Buffer.from(await source.arrayBuffer());
|
|
55
|
+
const isAnimated = ANIMATED_MIME_TYPES.includes(mimeType);
|
|
56
|
+
const sharpOptions = isAnimated ? {
|
|
57
|
+
animated: true
|
|
58
|
+
} : {};
|
|
59
|
+
const withoutEnlargement = parseResult.withoutEnlargement ?? dynamicDefaults.withoutEnlargement;
|
|
60
|
+
// The output size is only known once the source is probed: with a single dimension
|
|
61
|
+
// Sharp derives the other from the aspect ratio, `fit: 'outside'` can overflow the
|
|
62
|
+
// requested box along one axis, and an animated source is resized frame by frame,
|
|
63
|
+
// so its real cost is the per-frame output times the frame count.
|
|
64
|
+
if (isAnimated || parseResult.width === undefined || parseResult.height === undefined || dynamicDefaults.fit === 'outside') {
|
|
65
|
+
const metadata = await sharpDependency(sourceBuffer, sharpOptions).metadata();
|
|
66
|
+
const frameCount = isAnimated ? metadata.pages ?? 1 : 1;
|
|
67
|
+
const frameHeight = metadata.pageHeight ?? metadata.height;
|
|
68
|
+
// EXIF orientations 5-8 rotate the image by 90°, so `.rotate()` below swaps its axes.
|
|
69
|
+
const isRotatedQuarterTurn = [
|
|
70
|
+
5,
|
|
71
|
+
6,
|
|
72
|
+
7,
|
|
73
|
+
8
|
|
74
|
+
].includes(metadata.orientation);
|
|
75
|
+
const output = getOutputDimensions({
|
|
76
|
+
fit: dynamicDefaults.fit,
|
|
77
|
+
height: parseResult.height,
|
|
78
|
+
sourceHeight: isRotatedQuarterTurn ? metadata.width : frameHeight,
|
|
79
|
+
sourceWidth: isRotatedQuarterTurn ? frameHeight : metadata.width,
|
|
80
|
+
width: parseResult.width,
|
|
81
|
+
withoutEnlargement
|
|
82
|
+
});
|
|
83
|
+
if (output && (output.width > dynamicDefaults.maxWidth || output.height > dynamicDefaults.maxHeight || output.width * output.height * frameCount > dynamicDefaults.maxPixels)) {
|
|
84
|
+
const frameDescription = frameCount > 1 ? ` across ${frameCount} frames` : '';
|
|
85
|
+
return {
|
|
86
|
+
response: Response.json({
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
message: `Requested dimensions (${output.width}x${output.height}${frameDescription}) exceed the configured maximum.`
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}, {
|
|
93
|
+
status: 400
|
|
94
|
+
}),
|
|
95
|
+
status: 'complete'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Sharp drops the EXIF orientation tag on output, so apply it to the pixels first.
|
|
100
|
+
const resizedBuffer = await sharpDependency(sourceBuffer, sharpOptions).rotate().resize({
|
|
101
|
+
fit: dynamicDefaults.fit,
|
|
102
|
+
height: parseResult.height,
|
|
103
|
+
position: dynamicDefaults.position,
|
|
104
|
+
width: parseResult.width,
|
|
105
|
+
withoutEnlargement
|
|
106
|
+
}).toBuffer();
|
|
107
|
+
const headers = new Headers();
|
|
108
|
+
headers.set('Content-Type', mimeType);
|
|
109
|
+
headers.set('Content-Length', String(resizedBuffer.length));
|
|
110
|
+
return {
|
|
111
|
+
response: new Response(req.method === 'HEAD' ? null : resizedBuffer, {
|
|
112
|
+
headers,
|
|
113
|
+
status: 200
|
|
114
|
+
}),
|
|
115
|
+
status: 'continue'
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The per-frame output size. When both dimensions are requested with any `fit` other
|
|
121
|
+
* than `'outside'` this is the requested box, an upper bound (`fit: 'contain'`/`'inside'`
|
|
122
|
+
* or `withoutEnlargement` can render smaller), which is what a resource budget needs.
|
|
123
|
+
* `'outside'` scales to cover the box, so one axis can exceed it.
|
|
124
|
+
*/ function getOutputDimensions({ fit, height, sourceHeight, sourceWidth, width, withoutEnlargement }) {
|
|
125
|
+
const hasBothDimensions = width !== undefined && height !== undefined;
|
|
126
|
+
if (hasBothDimensions && fit !== 'outside') {
|
|
127
|
+
return {
|
|
128
|
+
height,
|
|
129
|
+
width
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!sourceWidth || !sourceHeight) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const scale = hasBothDimensions ? Math.max(width / sourceWidth, height / sourceHeight) : width !== undefined ? width / sourceWidth : height / sourceHeight;
|
|
136
|
+
const effectiveScale = withoutEnlargement ? Math.min(scale, 1) : scale;
|
|
137
|
+
return {
|
|
138
|
+
height: Math.round(sourceHeight * effectiveScale),
|
|
139
|
+
width: Math.round(sourceWidth * effectiveScale)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=handleRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handleRequest.ts"],"sourcesContent":["import type { HandleTransformRequestArgs, HandleTransformRequestResult } from 'payload'\nimport type { SharpOptions } from 'sharp'\n\nimport type { SharpDependency, SharpDynamicDefaults } from './types.js'\n\nimport { parseDynamicResize } from './parseDynamicResize.js'\n\n// Must match generateFileData.ts's allow-list — the only MIME types Sharp auto-detects multi-frame animation for.\nconst ANIMATED_MIME_TYPES = ['image/avif', 'image/gif', 'image/webp']\n\n/**\n * Builds the request-time `handleRequest` stage: parses and validates the v1\n * dynamic parameters (`width`, `height`, `withoutEnlargement`), retrieves the\n * source exactly once — only after validation passes — and resizes it while\n * preserving the source format. Unexpected Sharp errors are left to propagate\n * uncaught; Payload's orchestrator logs and aborts the pipeline.\n */\nexport function createHandleRequest({\n dynamicDefaults,\n sharpDependency,\n}: {\n dynamicDefaults: Required<SharpDynamicDefaults>\n sharpDependency: SharpDependency\n}): (args: HandleTransformRequestArgs) => Promise<HandleTransformRequestResult> {\n return async ({ getSourceFile, mimeType, req }) => {\n const parseResult = parseDynamicResize({\n limits: dynamicDefaults,\n searchParams: req.searchParams ?? new URLSearchParams(),\n })\n\n if (!parseResult.isRouted) {\n return { status: 'continue' }\n }\n\n if (!parseResult.valid) {\n return {\n response: Response.json({ errors: [{ message: parseResult.error }] }, { status: 400 }),\n status: 'complete',\n }\n }\n\n if (req.headers?.get('range')) {\n return { response: new Response(null, { status: 416 }), status: 'complete' }\n }\n\n const source = await getSourceFile()\n\n if (!source.ok) {\n return { response: source, status: 'complete' }\n }\n\n const sourceBuffer = Buffer.from(await source.arrayBuffer())\n\n const isAnimated = ANIMATED_MIME_TYPES.includes(mimeType)\n const sharpOptions: SharpOptions = isAnimated ? { animated: true } : {}\n\n const withoutEnlargement = parseResult.withoutEnlargement ?? dynamicDefaults.withoutEnlargement\n\n // The output size is only known once the source is probed: with a single dimension\n // Sharp derives the other from the aspect ratio, `fit: 'outside'` can overflow the\n // requested box along one axis, and an animated source is resized frame by frame,\n // so its real cost is the per-frame output times the frame count.\n if (\n isAnimated ||\n parseResult.width === undefined ||\n parseResult.height === undefined ||\n dynamicDefaults.fit === 'outside'\n ) {\n const metadata = await sharpDependency(sourceBuffer, sharpOptions).metadata()\n const frameCount = isAnimated ? (metadata.pages ?? 1) : 1\n const frameHeight = metadata.pageHeight ?? metadata.height\n // EXIF orientations 5-8 rotate the image by 90°, so `.rotate()` below swaps its axes.\n const isRotatedQuarterTurn = [5, 6, 7, 8].includes(metadata.orientation!)\n const output = getOutputDimensions({\n fit: dynamicDefaults.fit,\n height: parseResult.height,\n sourceHeight: isRotatedQuarterTurn ? metadata.width : frameHeight,\n sourceWidth: isRotatedQuarterTurn ? frameHeight : metadata.width,\n width: parseResult.width,\n withoutEnlargement,\n })\n\n if (\n output &&\n (output.width > dynamicDefaults.maxWidth ||\n output.height > dynamicDefaults.maxHeight ||\n output.width * output.height * frameCount > dynamicDefaults.maxPixels)\n ) {\n const frameDescription = frameCount > 1 ? ` across ${frameCount} frames` : ''\n\n return {\n response: Response.json(\n {\n errors: [\n {\n message: `Requested dimensions (${output.width}x${output.height}${frameDescription}) exceed the configured maximum.`,\n },\n ],\n },\n { status: 400 },\n ),\n status: 'complete',\n }\n }\n }\n\n // Sharp drops the EXIF orientation tag on output, so apply it to the pixels first.\n const resizedBuffer = await sharpDependency(sourceBuffer, sharpOptions)\n .rotate()\n .resize({\n fit: dynamicDefaults.fit,\n height: parseResult.height,\n position: dynamicDefaults.position,\n width: parseResult.width,\n withoutEnlargement,\n })\n .toBuffer()\n\n const headers = new Headers()\n headers.set('Content-Type', mimeType)\n headers.set('Content-Length', String(resizedBuffer.length))\n\n return {\n response: new Response(req.method === 'HEAD' ? null : resizedBuffer, {\n headers,\n status: 200,\n }),\n status: 'continue',\n }\n }\n}\n\n/**\n * The per-frame output size. When both dimensions are requested with any `fit` other\n * than `'outside'` this is the requested box, an upper bound (`fit: 'contain'`/`'inside'`\n * or `withoutEnlargement` can render smaller), which is what a resource budget needs.\n * `'outside'` scales to cover the box, so one axis can exceed it.\n */\nfunction getOutputDimensions({\n fit,\n height,\n sourceHeight,\n sourceWidth,\n width,\n withoutEnlargement,\n}: {\n fit: SharpDynamicDefaults['fit']\n height: number | undefined\n sourceHeight: number | undefined\n sourceWidth: number | undefined\n width: number | undefined\n withoutEnlargement: boolean\n}): { height: number; width: number } | undefined {\n const hasBothDimensions = width !== undefined && height !== undefined\n\n if (hasBothDimensions && fit !== 'outside') {\n return { height, width }\n }\n\n if (!sourceWidth || !sourceHeight) {\n return undefined\n }\n\n const scale = hasBothDimensions\n ? Math.max(width / sourceWidth, height / sourceHeight)\n : width !== undefined\n ? width / sourceWidth\n : height! / sourceHeight\n const effectiveScale = withoutEnlargement ? Math.min(scale, 1) : scale\n\n return {\n height: Math.round(sourceHeight * effectiveScale),\n width: Math.round(sourceWidth * effectiveScale),\n }\n}\n"],"names":["parseDynamicResize","ANIMATED_MIME_TYPES","createHandleRequest","dynamicDefaults","sharpDependency","getSourceFile","mimeType","req","parseResult","limits","searchParams","URLSearchParams","isRouted","status","valid","response","Response","json","errors","message","error","headers","get","source","ok","sourceBuffer","Buffer","from","arrayBuffer","isAnimated","includes","sharpOptions","animated","withoutEnlargement","width","undefined","height","fit","metadata","frameCount","pages","frameHeight","pageHeight","isRotatedQuarterTurn","orientation","output","getOutputDimensions","sourceHeight","sourceWidth","maxWidth","maxHeight","maxPixels","frameDescription","resizedBuffer","rotate","resize","position","toBuffer","Headers","set","String","length","method","hasBothDimensions","scale","Math","max","effectiveScale","min","round"],"mappings":"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"}
|