@inoo-ch/payload-image-optimizer 1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/dist/components/ImageBox.d.ts +22 -0
  4. package/dist/components/ImageBox.js +51 -0
  5. package/dist/components/ImageBox.js.map +1 -0
  6. package/dist/components/OptimizationStatus.d.ts +4 -0
  7. package/dist/components/OptimizationStatus.js +196 -0
  8. package/dist/components/OptimizationStatus.js.map +1 -0
  9. package/dist/components/RegenerationButton.d.ts +2 -0
  10. package/dist/components/RegenerationButton.js +212 -0
  11. package/dist/components/RegenerationButton.js.map +1 -0
  12. package/dist/defaults.d.ts +3 -0
  13. package/dist/defaults.js +35 -0
  14. package/dist/defaults.js.map +1 -0
  15. package/dist/endpoints/regenerate.d.ts +4 -0
  16. package/dist/endpoints/regenerate.js +144 -0
  17. package/dist/endpoints/regenerate.js.map +1 -0
  18. package/dist/exports/client.d.ts +6 -0
  19. package/dist/exports/client.js +6 -0
  20. package/dist/exports/client.js.map +1 -0
  21. package/dist/exports/rsc.d.ts +1 -0
  22. package/dist/exports/rsc.js +3 -0
  23. package/dist/exports/rsc.js.map +1 -0
  24. package/dist/fields/imageOptimizerField.d.ts +2 -0
  25. package/dist/fields/imageOptimizerField.js +75 -0
  26. package/dist/fields/imageOptimizerField.js.map +1 -0
  27. package/dist/hooks/afterChange.d.ts +3 -0
  28. package/dist/hooks/afterChange.js +40 -0
  29. package/dist/hooks/afterChange.js.map +1 -0
  30. package/dist/hooks/beforeChange.d.ts +3 -0
  31. package/dist/hooks/beforeChange.js +26 -0
  32. package/dist/hooks/beforeChange.js.map +1 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +127 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/next-image.d.js +2 -0
  37. package/dist/next-image.d.js.map +1 -0
  38. package/dist/processing/index.d.ts +17 -0
  39. package/dist/processing/index.js +46 -0
  40. package/dist/processing/index.js.map +1 -0
  41. package/dist/tasks/convertFormats.d.ts +12 -0
  42. package/dist/tasks/convertFormats.js +84 -0
  43. package/dist/tasks/convertFormats.js.map +1 -0
  44. package/dist/tasks/regenerateDocument.d.ts +18 -0
  45. package/dist/tasks/regenerateDocument.js +121 -0
  46. package/dist/tasks/regenerateDocument.js.map +1 -0
  47. package/dist/types.d.ts +35 -0
  48. package/dist/types.js +3 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/utilities/getImageOptimizerProps.d.ts +32 -0
  51. package/dist/utilities/getImageOptimizerProps.js +55 -0
  52. package/dist/utilities/getImageOptimizerProps.js.map +1 -0
  53. package/dist/utilities/thumbhash.d.ts +2 -0
  54. package/dist/utilities/thumbhash.js +11 -0
  55. package/dist/utilities/thumbhash.js.map +1 -0
  56. package/package.json +141 -0
@@ -0,0 +1,26 @@
1
+ import { resolveCollectionConfig } from '../defaults.js';
2
+ import { generateThumbHash, stripAndResize } from '../processing/index.js';
3
+ export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
4
+ return async ({ context, data, req })=>{
5
+ if (context?.imageOptimizer_skip) return data;
6
+ if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data;
7
+ const originalSize = req.file.data.length;
8
+ const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
9
+ // Process in memory: strip EXIF, resize, generate blur
10
+ const processed = await stripAndResize(req.file.data, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
11
+ data.imageOptimizer = {
12
+ originalSize,
13
+ optimizedSize: processed.size,
14
+ status: 'pending'
15
+ };
16
+ if (resolvedConfig.generateThumbHash) {
17
+ data.imageOptimizer.thumbHash = await generateThumbHash(processed.buffer);
18
+ }
19
+ // Store processed buffer in context for afterChange to write to disk
20
+ // (Payload 3.0 does not use modified req.file.data for the disk write)
21
+ context.imageOptimizer_processedBuffer = processed.buffer;
22
+ return data;
23
+ };
24
+ };
25
+
26
+ //# sourceMappingURL=beforeChange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/beforeChange.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { generateThumbHash, stripAndResize } from '../processing/index.js'\n\nexport const createBeforeChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionBeforeChangeHook => {\n return async ({ context, data, req }) => {\n if (context?.imageOptimizer_skip) return data\n\n if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data\n\n const originalSize = req.file.data.length\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // Process in memory: strip EXIF, resize, generate blur\n const processed = await stripAndResize(\n req.file.data,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n data.imageOptimizer = {\n originalSize,\n optimizedSize: processed.size,\n status: 'pending',\n }\n\n if (resolvedConfig.generateThumbHash) {\n data.imageOptimizer.thumbHash = await generateThumbHash(processed.buffer)\n }\n\n // Store processed buffer in context for afterChange to write to disk\n // (Payload 3.0 does not use modified req.file.data for the disk write)\n context.imageOptimizer_processedBuffer = processed.buffer\n\n return data\n }\n}\n"],"names":["resolveCollectionConfig","generateThumbHash","stripAndResize","createBeforeChangeHook","resolvedConfig","collectionSlug","context","data","req","imageOptimizer_skip","file","mimetype","startsWith","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","imageOptimizer","optimizedSize","size","status","thumbHash","buffer","imageOptimizer_processedBuffer"],"mappings":"AAGA,SAASA,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,iBAAiB,EAAEC,cAAc,QAAQ,yBAAwB;AAE1E,OAAO,MAAMC,yBAAyB,CACpCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAClC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,IAAI,CAACC,IAAIE,IAAI,IAAI,CAACF,IAAIE,IAAI,CAACH,IAAI,IAAI,CAACC,IAAIE,IAAI,CAACC,QAAQ,EAAEC,WAAW,WAAW,OAAOL;QAEpF,MAAMM,eAAeL,IAAIE,IAAI,CAACH,IAAI,CAACO,MAAM;QAEzC,MAAMC,sBAAsBf,wBAAwBI,gBAAgBC;QAEpE,uDAAuD;QACvD,MAAMW,YAAY,MAAMd,eACtBM,IAAIE,IAAI,CAACH,IAAI,EACbQ,oBAAoBE,aAAa,EACjCb,eAAec,aAAa;QAG9BX,KAAKY,cAAc,GAAG;YACpBN;YACAO,eAAeJ,UAAUK,IAAI;YAC7BC,QAAQ;QACV;QAEA,IAAIlB,eAAeH,iBAAiB,EAAE;YACpCM,KAAKY,cAAc,CAACI,SAAS,GAAG,MAAMtB,kBAAkBe,UAAUQ,MAAM;QAC1E;QAEA,qEAAqE;QACrE,uEAAuE;QACvElB,QAAQmB,8BAA8B,GAAGT,UAAUQ,MAAM;QAEzD,OAAOjB;IACT;AACF,EAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Config } from 'payload';
2
+ import type { ImageOptimizerConfig } from './types.js';
3
+ export type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig } from './types.js';
4
+ export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
5
+ export declare const imageOptimizer: (pluginOptions: ImageOptimizerConfig) => (config: Config) => Config;
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { resolveConfig } from './defaults.js';
2
+ import { getImageOptimizerField } from './fields/imageOptimizerField.js';
3
+ import { createBeforeChangeHook } from './hooks/beforeChange.js';
4
+ import { createAfterChangeHook } from './hooks/afterChange.js';
5
+ import { createConvertFormatsHandler } from './tasks/convertFormats.js';
6
+ import { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js';
7
+ import { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js';
8
+ export { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js';
9
+ export const imageOptimizer = (pluginOptions)=>(config)=>{
10
+ const resolvedConfig = resolveConfig(pluginOptions);
11
+ if (!config.collections) {
12
+ config.collections = [];
13
+ }
14
+ // Inject imageOptimizer fields into targeted upload collections
15
+ for(const collectionSlug in resolvedConfig.collections){
16
+ const collection = config.collections.find((c)=>c.slug === collectionSlug);
17
+ if (collection) {
18
+ collection.fields.push(getImageOptimizerField());
19
+ }
20
+ }
21
+ // If disabled, keep fields for schema consistency but skip hooks/tasks
22
+ if (resolvedConfig.disabled) {
23
+ return config;
24
+ }
25
+ // Inject hooks into targeted upload collections
26
+ for(const collectionSlug in resolvedConfig.collections){
27
+ const collection = config.collections.find((c)=>c.slug === collectionSlug);
28
+ if (collection) {
29
+ if (!collection.hooks) {
30
+ collection.hooks = {};
31
+ }
32
+ if (!collection.hooks.beforeChange) {
33
+ collection.hooks.beforeChange = [];
34
+ }
35
+ collection.hooks.beforeChange.push(createBeforeChangeHook(resolvedConfig, collectionSlug));
36
+ if (!collection.hooks.afterChange) {
37
+ collection.hooks.afterChange = [];
38
+ }
39
+ collection.hooks.afterChange.push(createAfterChangeHook(resolvedConfig, collectionSlug));
40
+ // Add RegenerationButton to the collection list view
41
+ if (!collection.admin) {
42
+ collection.admin = {};
43
+ }
44
+ if (!collection.admin.components) {
45
+ collection.admin.components = {};
46
+ }
47
+ if (!collection.admin.components.beforeListTable) {
48
+ collection.admin.components.beforeListTable = [];
49
+ }
50
+ collection.admin.components.beforeListTable.push('@inoo-ch/payload-image-optimizer/client#RegenerationButton');
51
+ }
52
+ }
53
+ // Register async format conversion job task
54
+ if (!config.jobs) {
55
+ config.jobs = {
56
+ tasks: []
57
+ };
58
+ }
59
+ if (!config.jobs.tasks) {
60
+ config.jobs.tasks = [];
61
+ }
62
+ config.jobs.tasks.push({
63
+ slug: 'imageOptimizer_convertFormats',
64
+ inputSchema: [
65
+ {
66
+ name: 'collectionSlug',
67
+ type: 'text',
68
+ required: true
69
+ },
70
+ {
71
+ name: 'docId',
72
+ type: 'text',
73
+ required: true
74
+ }
75
+ ],
76
+ outputSchema: [
77
+ {
78
+ name: 'variantsGenerated',
79
+ type: 'number'
80
+ }
81
+ ],
82
+ retries: 2,
83
+ handler: createConvertFormatsHandler(resolvedConfig)
84
+ });
85
+ config.jobs.tasks.push({
86
+ slug: 'imageOptimizer_regenerateDocument',
87
+ inputSchema: [
88
+ {
89
+ name: 'collectionSlug',
90
+ type: 'text',
91
+ required: true
92
+ },
93
+ {
94
+ name: 'docId',
95
+ type: 'text',
96
+ required: true
97
+ }
98
+ ],
99
+ outputSchema: [
100
+ {
101
+ name: 'status',
102
+ type: 'text'
103
+ },
104
+ {
105
+ name: 'reason',
106
+ type: 'text'
107
+ }
108
+ ],
109
+ retries: 2,
110
+ handler: createRegenerateDocumentHandler(resolvedConfig)
111
+ });
112
+ // Register regeneration endpoints
113
+ if (!config.endpoints) config.endpoints = [];
114
+ config.endpoints.push({
115
+ path: '/image-optimizer/regenerate',
116
+ method: 'post',
117
+ handler: createRegenerateHandler(resolvedConfig)
118
+ });
119
+ config.endpoints.push({
120
+ path: '/image-optimizer/regenerate',
121
+ method: 'get',
122
+ handler: createRegenerateStatusHandler(resolvedConfig)
123
+ });
124
+ return config;
125
+ };
126
+
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { ImageOptimizerConfig } from './types.js'\nimport { resolveConfig } from './defaults.js'\nimport { getImageOptimizerField } from './fields/imageOptimizerField.js'\nimport { createBeforeChangeHook } from './hooks/beforeChange.js'\nimport { createAfterChangeHook } from './hooks/afterChange.js'\nimport { createConvertFormatsHandler } from './tasks/convertFormats.js'\nimport { createRegenerateDocumentHandler } from './tasks/regenerateDocument.js'\nimport { createRegenerateHandler, createRegenerateStatusHandler } from './endpoints/regenerate.js'\n\nexport type { ImageOptimizerConfig, ImageFormat, FormatQuality, CollectionOptimizerConfig } from './types.js'\n\nexport { encodeImageToThumbHash, decodeThumbHashToDataURL } from './utilities/thumbhash.js'\n\nexport const imageOptimizer =\n (pluginOptions: ImageOptimizerConfig) =>\n (config: Config): Config => {\n const resolvedConfig = resolveConfig(pluginOptions)\n\n if (!config.collections) {\n config.collections = []\n }\n\n // Inject imageOptimizer fields into targeted upload collections\n for (const collectionSlug in resolvedConfig.collections) {\n const collection = config.collections.find((c) => c.slug === collectionSlug)\n\n if (collection) {\n collection.fields.push(getImageOptimizerField())\n }\n }\n\n // If disabled, keep fields for schema consistency but skip hooks/tasks\n if (resolvedConfig.disabled) {\n return config\n }\n\n // Inject hooks into targeted upload collections\n for (const collectionSlug in resolvedConfig.collections) {\n const collection = config.collections.find((c) => c.slug === collectionSlug)\n\n if (collection) {\n if (!collection.hooks) {\n collection.hooks = {}\n }\n\n if (!collection.hooks.beforeChange) {\n collection.hooks.beforeChange = []\n }\n collection.hooks.beforeChange.push(createBeforeChangeHook(resolvedConfig, collectionSlug))\n\n if (!collection.hooks.afterChange) {\n collection.hooks.afterChange = []\n }\n collection.hooks.afterChange.push(createAfterChangeHook(resolvedConfig, collectionSlug))\n\n // Add RegenerationButton to the collection list view\n if (!collection.admin) {\n collection.admin = {}\n }\n if (!collection.admin.components) {\n collection.admin.components = {}\n }\n if (!collection.admin.components.beforeListTable) {\n collection.admin.components.beforeListTable = []\n }\n collection.admin.components.beforeListTable.push(\n '@inoo-ch/payload-image-optimizer/client#RegenerationButton',\n )\n }\n }\n\n // Register async format conversion job task\n if (!config.jobs) {\n config.jobs = { tasks: [] }\n }\n if (!config.jobs!.tasks) {\n config.jobs!.tasks = []\n }\n\n config.jobs!.tasks!.push({\n slug: 'imageOptimizer_convertFormats',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'variantsGenerated', type: 'number' },\n ],\n retries: 2,\n handler: createConvertFormatsHandler(resolvedConfig),\n } as any)\n\n config.jobs!.tasks!.push({\n slug: 'imageOptimizer_regenerateDocument',\n inputSchema: [\n { name: 'collectionSlug', type: 'text', required: true },\n { name: 'docId', type: 'text', required: true },\n ],\n outputSchema: [\n { name: 'status', type: 'text' },\n { name: 'reason', type: 'text' },\n ],\n retries: 2,\n handler: createRegenerateDocumentHandler(resolvedConfig),\n } as any)\n\n // Register regeneration endpoints\n if (!config.endpoints) config.endpoints = []\n\n config.endpoints.push({\n path: '/image-optimizer/regenerate',\n method: 'post',\n handler: createRegenerateHandler(resolvedConfig),\n })\n\n config.endpoints.push({\n path: '/image-optimizer/regenerate',\n method: 'get',\n handler: createRegenerateStatusHandler(resolvedConfig),\n })\n\n return config\n }\n"],"names":["resolveConfig","getImageOptimizerField","createBeforeChangeHook","createAfterChangeHook","createConvertFormatsHandler","createRegenerateDocumentHandler","createRegenerateHandler","createRegenerateStatusHandler","encodeImageToThumbHash","decodeThumbHashToDataURL","imageOptimizer","pluginOptions","config","resolvedConfig","collections","collectionSlug","collection","find","c","slug","fields","push","disabled","hooks","beforeChange","afterChange","admin","components","beforeListTable","jobs","tasks","inputSchema","name","type","required","outputSchema","retries","handler","endpoints","path","method"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=next-image.d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next-image.d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ export declare function stripAndResize(buffer: Buffer, maxDimensions: {
2
+ width: number;
3
+ height: number;
4
+ }, stripMetadata: boolean): Promise<{
5
+ buffer: Buffer;
6
+ width: number;
7
+ height: number;
8
+ size: number;
9
+ }>;
10
+ export declare function generateThumbHash(buffer: Buffer): Promise<string>;
11
+ export declare function convertFormat(buffer: Buffer, format: 'webp' | 'avif', quality: number): Promise<{
12
+ buffer: Buffer;
13
+ width: number;
14
+ height: number;
15
+ size: number;
16
+ mimeType: string;
17
+ }>;
@@ -0,0 +1,46 @@
1
+ import sharp from 'sharp';
2
+ import { rgbaToThumbHash } from 'thumbhash';
3
+ export async function stripAndResize(buffer, maxDimensions, stripMetadata) {
4
+ let pipeline = sharp(buffer).rotate().resize(maxDimensions.width, maxDimensions.height, {
5
+ fit: 'inside',
6
+ withoutEnlargement: true
7
+ });
8
+ if (!stripMetadata) {
9
+ pipeline = pipeline.keepMetadata();
10
+ }
11
+ const { data, info } = await pipeline.toBuffer({
12
+ resolveWithObject: true
13
+ });
14
+ return {
15
+ buffer: data,
16
+ width: info.width,
17
+ height: info.height,
18
+ size: info.size
19
+ };
20
+ }
21
+ export async function generateThumbHash(buffer) {
22
+ const { data, info } = await sharp(buffer).resize(100, 100, {
23
+ fit: 'inside'
24
+ }).raw().ensureAlpha().toBuffer({
25
+ resolveWithObject: true
26
+ });
27
+ const thumbHash = rgbaToThumbHash(info.width, info.height, data);
28
+ return Buffer.from(thumbHash).toString('base64');
29
+ }
30
+ export async function convertFormat(buffer, format, quality) {
31
+ const { data, info } = await sharp(buffer).toFormat(format, {
32
+ quality
33
+ }).toBuffer({
34
+ resolveWithObject: true
35
+ });
36
+ const mimeType = format === 'webp' ? 'image/webp' : 'image/avif';
37
+ return {
38
+ buffer: data,
39
+ width: info.width,
40
+ height: info.height,
41
+ size: info.size,
42
+ mimeType
43
+ };
44
+ }
45
+
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/processing/index.ts"],"sourcesContent":["import sharp from 'sharp'\nimport { rgbaToThumbHash } from 'thumbhash'\n\nexport async function stripAndResize(\n buffer: Buffer,\n maxDimensions: { width: number; height: number },\n stripMetadata: boolean,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number }> {\n let pipeline = sharp(buffer)\n .rotate()\n .resize(maxDimensions.width, maxDimensions.height, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n\n if (!stripMetadata) {\n pipeline = pipeline.keepMetadata()\n }\n\n const { data, info } = await pipeline.toBuffer({ resolveWithObject: true })\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n }\n}\n\nexport async function generateThumbHash(buffer: Buffer): Promise<string> {\n const { data, info } = await sharp(buffer)\n .resize(100, 100, { fit: 'inside' })\n .raw()\n .ensureAlpha()\n .toBuffer({ resolveWithObject: true })\n\n const thumbHash = rgbaToThumbHash(info.width, info.height, data)\n return Buffer.from(thumbHash).toString('base64')\n}\n\nexport async function convertFormat(\n buffer: Buffer,\n format: 'webp' | 'avif',\n quality: number,\n): Promise<{ buffer: Buffer; width: number; height: number; size: number; mimeType: string }> {\n const { data, info } = await sharp(buffer)\n .toFormat(format, { quality })\n .toBuffer({ resolveWithObject: true })\n\n const mimeType = format === 'webp' ? 'image/webp' : 'image/avif'\n\n return {\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size,\n mimeType,\n }\n}\n"],"names":["sharp","rgbaToThumbHash","stripAndResize","buffer","maxDimensions","stripMetadata","pipeline","rotate","resize","width","height","fit","withoutEnlargement","keepMetadata","data","info","toBuffer","resolveWithObject","size","generateThumbHash","raw","ensureAlpha","thumbHash","Buffer","from","toString","convertFormat","format","quality","toFormat","mimeType"],"mappings":"AAAA,OAAOA,WAAW,QAAO;AACzB,SAASC,eAAe,QAAQ,YAAW;AAE3C,OAAO,eAAeC,eACpBC,MAAc,EACdC,aAAgD,EAChDC,aAAsB;IAEtB,IAAIC,WAAWN,MAAMG,QAClBI,MAAM,GACNC,MAAM,CAACJ,cAAcK,KAAK,EAAEL,cAAcM,MAAM,EAAE;QACjDC,KAAK;QACLC,oBAAoB;IACtB;IAEF,IAAI,CAACP,eAAe;QAClBC,WAAWA,SAASO,YAAY;IAClC;IAEA,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMT,SAASU,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEzE,OAAO;QACLd,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;IACjB;AACF;AAEA,OAAO,eAAeC,kBAAkBhB,MAAc;IACpD,MAAM,EAAEW,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChCK,MAAM,CAAC,KAAK,KAAK;QAAEG,KAAK;IAAS,GACjCS,GAAG,GACHC,WAAW,GACXL,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMK,YAAYrB,gBAAgBc,KAAKN,KAAK,EAAEM,KAAKL,MAAM,EAAEI;IAC3D,OAAOS,OAAOC,IAAI,CAACF,WAAWG,QAAQ,CAAC;AACzC;AAEA,OAAO,eAAeC,cACpBvB,MAAc,EACdwB,MAAuB,EACvBC,OAAe;IAEf,MAAM,EAAEd,IAAI,EAAEC,IAAI,EAAE,GAAG,MAAMf,MAAMG,QAChC0B,QAAQ,CAACF,QAAQ;QAAEC;IAAQ,GAC3BZ,QAAQ,CAAC;QAAEC,mBAAmB;IAAK;IAEtC,MAAMa,WAAWH,WAAW,SAAS,eAAe;IAEpD,OAAO;QACLxB,QAAQW;QACRL,OAAOM,KAAKN,KAAK;QACjBC,QAAQK,KAAKL,MAAM;QACnBQ,MAAMH,KAAKG,IAAI;QACfY;IACF;AACF"}
@@ -0,0 +1,12 @@
1
+ import type { ResolvedImageOptimizerConfig } from '../types.js';
2
+ export declare const createConvertFormatsHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => ({ input, req }: {
3
+ input: {
4
+ collectionSlug: string;
5
+ docId: string;
6
+ };
7
+ req: any;
8
+ }) => Promise<{
9
+ output: {
10
+ variantsGenerated: number;
11
+ };
12
+ }>;
@@ -0,0 +1,84 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { resolveCollectionConfig } from '../defaults.js';
4
+ import { convertFormat } from '../processing/index.js';
5
+ export const createConvertFormatsHandler = (resolvedConfig)=>{
6
+ return async ({ input, req })=>{
7
+ try {
8
+ const doc = await req.payload.findByID({
9
+ collection: input.collectionSlug,
10
+ id: input.docId
11
+ });
12
+ const collectionConfig = req.payload.collections[input.collectionSlug].config;
13
+ let staticDir = typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : '';
14
+ if (!staticDir) {
15
+ throw new Error(`No staticDir configured for collection "${input.collectionSlug}"`);
16
+ }
17
+ if (!path.isAbsolute(staticDir)) {
18
+ staticDir = path.resolve(process.cwd(), staticDir);
19
+ }
20
+ // Sanitize filename to prevent path traversal
21
+ const safeFilename = path.basename(doc.filename);
22
+ const filePath = path.join(staticDir, safeFilename);
23
+ const fileBuffer = await fs.readFile(filePath);
24
+ const variants = [];
25
+ const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
26
+ for (const format of perCollectionConfig.formats){
27
+ const result = await convertFormat(fileBuffer, format.format, format.quality);
28
+ const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`;
29
+ await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
30
+ variants.push({
31
+ format: format.format,
32
+ filename: variantFilename,
33
+ filesize: result.size,
34
+ width: result.width,
35
+ height: result.height,
36
+ mimeType: result.mimeType,
37
+ url: `/api/${input.collectionSlug}/file/${variantFilename}`
38
+ });
39
+ }
40
+ await req.payload.update({
41
+ collection: input.collectionSlug,
42
+ id: input.docId,
43
+ data: {
44
+ imageOptimizer: {
45
+ status: 'complete',
46
+ variants
47
+ }
48
+ },
49
+ context: {
50
+ imageOptimizer_skip: true
51
+ }
52
+ });
53
+ return {
54
+ output: {
55
+ variantsGenerated: variants.length
56
+ }
57
+ };
58
+ } catch (err) {
59
+ const errorMessage = err instanceof Error ? err.message : String(err);
60
+ try {
61
+ await req.payload.update({
62
+ collection: input.collectionSlug,
63
+ id: input.docId,
64
+ data: {
65
+ imageOptimizer: {
66
+ status: 'error',
67
+ error: errorMessage
68
+ }
69
+ },
70
+ context: {
71
+ imageOptimizer_skip: true
72
+ }
73
+ });
74
+ } catch (updateErr) {
75
+ req.payload.logger.error({
76
+ err: updateErr
77
+ }, 'Failed to persist error status for image optimizer');
78
+ }
79
+ throw err;
80
+ }
81
+ };
82
+ };
83
+
84
+ //# sourceMappingURL=convertFormats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tasks/convertFormats.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { convertFormat } from '../processing/index.js'\n\nexport const createConvertFormatsHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n const fileBuffer = await fs.readFile(filePath)\n\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(fileBuffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'complete',\n variants,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { variantsGenerated: variants.length } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","convertFormat","createConvertFormatsHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","variants","perCollectionConfig","format","formats","result","quality","variantFilename","parse","name","writeFile","buffer","push","filesize","size","width","height","mimeType","url","update","data","imageOptimizer","status","context","imageOptimizer_skip","output","variantsGenerated","length","err","errorMessage","message","String","error","updateErr","logger"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { ResolvedImageOptimizerConfig } from '../types.js';
2
+ export declare const createRegenerateDocumentHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => ({ input, req }: {
3
+ input: {
4
+ collectionSlug: string;
5
+ docId: string;
6
+ };
7
+ req: any;
8
+ }) => Promise<{
9
+ output: {
10
+ status: string;
11
+ reason: string;
12
+ };
13
+ } | {
14
+ output: {
15
+ status: string;
16
+ reason?: undefined;
17
+ };
18
+ }>;
@@ -0,0 +1,121 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { resolveCollectionConfig } from '../defaults.js';
4
+ import { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js';
5
+ export const createRegenerateDocumentHandler = (resolvedConfig)=>{
6
+ return async ({ input, req })=>{
7
+ try {
8
+ const doc = await req.payload.findByID({
9
+ collection: input.collectionSlug,
10
+ id: input.docId
11
+ });
12
+ // Skip non-image documents
13
+ if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {
14
+ return {
15
+ output: {
16
+ status: 'skipped',
17
+ reason: 'not-image'
18
+ }
19
+ };
20
+ }
21
+ const collectionConfig = req.payload.collections[input.collectionSlug].config;
22
+ let staticDir = typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : '';
23
+ if (!staticDir) {
24
+ throw new Error(`No staticDir configured for collection "${input.collectionSlug}"`);
25
+ }
26
+ if (!path.isAbsolute(staticDir)) {
27
+ staticDir = path.resolve(process.cwd(), staticDir);
28
+ }
29
+ // Sanitize filename to prevent path traversal
30
+ const safeFilename = path.basename(doc.filename);
31
+ const filePath = path.join(staticDir, safeFilename);
32
+ let fileBuffer;
33
+ try {
34
+ fileBuffer = await fs.readFile(filePath);
35
+ } catch {
36
+ // If file not on disk, try fetching from URL
37
+ if (doc.url) {
38
+ const url = doc.url.startsWith('http') ? doc.url : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`;
39
+ const response = await fetch(url);
40
+ fileBuffer = Buffer.from(await response.arrayBuffer());
41
+ } else {
42
+ throw new Error(`File not found: ${filePath}`);
43
+ }
44
+ }
45
+ const originalSize = fileBuffer.length;
46
+ const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug);
47
+ // Step 1: Strip metadata + resize
48
+ const processed = await stripAndResize(fileBuffer, perCollectionConfig.maxDimensions, resolvedConfig.stripMetadata);
49
+ // Write optimized file back to disk
50
+ await fs.writeFile(filePath, processed.buffer);
51
+ // Step 2: Generate ThumbHash
52
+ let thumbHash;
53
+ if (resolvedConfig.generateThumbHash) {
54
+ thumbHash = await generateThumbHash(processed.buffer);
55
+ }
56
+ // Step 3: Convert to all configured formats
57
+ const variants = [];
58
+ for (const format of perCollectionConfig.formats){
59
+ const result = await convertFormat(processed.buffer, format.format, format.quality);
60
+ const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`;
61
+ await fs.writeFile(path.join(staticDir, variantFilename), result.buffer);
62
+ variants.push({
63
+ format: format.format,
64
+ filename: variantFilename,
65
+ filesize: result.size,
66
+ width: result.width,
67
+ height: result.height,
68
+ mimeType: result.mimeType,
69
+ url: `/api/${input.collectionSlug}/file/${variantFilename}`
70
+ });
71
+ }
72
+ // Step 4: Update the document with all optimization data
73
+ await req.payload.update({
74
+ collection: input.collectionSlug,
75
+ id: input.docId,
76
+ data: {
77
+ imageOptimizer: {
78
+ originalSize,
79
+ optimizedSize: processed.size,
80
+ status: 'complete',
81
+ thumbHash,
82
+ variants,
83
+ error: null
84
+ }
85
+ },
86
+ context: {
87
+ imageOptimizer_skip: true
88
+ }
89
+ });
90
+ return {
91
+ output: {
92
+ status: 'complete'
93
+ }
94
+ };
95
+ } catch (err) {
96
+ const errorMessage = err instanceof Error ? err.message : String(err);
97
+ try {
98
+ await req.payload.update({
99
+ collection: input.collectionSlug,
100
+ id: input.docId,
101
+ data: {
102
+ imageOptimizer: {
103
+ status: 'error',
104
+ error: errorMessage
105
+ }
106
+ },
107
+ context: {
108
+ imageOptimizer_skip: true
109
+ }
110
+ });
111
+ } catch (updateErr) {
112
+ req.payload.logger.error({
113
+ err: updateErr
114
+ }, 'Failed to persist error status for image optimizer regeneration');
115
+ }
116
+ throw err;
117
+ }
118
+ };
119
+ };
120
+
121
+ //# sourceMappingURL=regenerateDocument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tasks/regenerateDocument.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\n\nimport type { CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { stripAndResize, generateThumbHash, convertFormat } from '../processing/index.js'\n\nexport const createRegenerateDocumentHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n return async ({ input, req }: { input: { collectionSlug: string; docId: string }; req: any }) => {\n try {\n const doc = await req.payload.findByID({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n })\n\n // Skip non-image documents\n if (!doc.mimeType || !doc.mimeType.startsWith('image/')) {\n return { output: { status: 'skipped', reason: 'not-image' } }\n }\n\n const collectionConfig = req.payload.collections[input.collectionSlug as keyof typeof req.payload.collections].config\n\n let staticDir: string =\n typeof collectionConfig.upload === 'object' ? collectionConfig.upload.staticDir || '' : ''\n if (!staticDir) {\n throw new Error(`No staticDir configured for collection \"${input.collectionSlug}\"`)\n }\n if (!path.isAbsolute(staticDir)) {\n staticDir = path.resolve(process.cwd(), staticDir)\n }\n\n // Sanitize filename to prevent path traversal\n const safeFilename = path.basename(doc.filename)\n const filePath = path.join(staticDir, safeFilename)\n\n let fileBuffer: Buffer\n try {\n fileBuffer = await fs.readFile(filePath)\n } catch {\n // If file not on disk, try fetching from URL\n if (doc.url) {\n const url = doc.url.startsWith('http')\n ? doc.url\n : `${process.env.NEXT_PUBLIC_SERVER_URL || ''}${doc.url}`\n const response = await fetch(url)\n fileBuffer = Buffer.from(await response.arrayBuffer())\n } else {\n throw new Error(`File not found: ${filePath}`)\n }\n }\n\n const originalSize = fileBuffer.length\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, input.collectionSlug)\n\n // Step 1: Strip metadata + resize\n const processed = await stripAndResize(\n fileBuffer,\n perCollectionConfig.maxDimensions,\n resolvedConfig.stripMetadata,\n )\n\n // Write optimized file back to disk\n await fs.writeFile(filePath, processed.buffer)\n\n // Step 2: Generate ThumbHash\n let thumbHash: string | undefined\n if (resolvedConfig.generateThumbHash) {\n thumbHash = await generateThumbHash(processed.buffer)\n }\n\n // Step 3: Convert to all configured formats\n const variants: Array<{\n filename: string\n filesize: number\n format: string\n height: number\n mimeType: string\n url: string\n width: number\n }> = []\n\n for (const format of perCollectionConfig.formats) {\n const result = await convertFormat(processed.buffer, format.format, format.quality)\n const variantFilename = `${path.parse(safeFilename).name}-optimized.${format.format}`\n await fs.writeFile(path.join(staticDir, variantFilename), result.buffer)\n\n variants.push({\n format: format.format,\n filename: variantFilename,\n filesize: result.size,\n width: result.width,\n height: result.height,\n mimeType: result.mimeType,\n url: `/api/${input.collectionSlug}/file/${variantFilename}`,\n })\n }\n\n // Step 4: Update the document with all optimization data\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n originalSize,\n optimizedSize: processed.size,\n status: 'complete',\n thumbHash,\n variants,\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n\n return { output: { status: 'complete' } }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err)\n\n try {\n await req.payload.update({\n collection: input.collectionSlug as CollectionSlug,\n id: input.docId,\n data: {\n imageOptimizer: {\n status: 'error',\n error: errorMessage,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n } catch (updateErr) {\n req.payload.logger.error(\n { err: updateErr },\n 'Failed to persist error status for image optimizer regeneration',\n )\n }\n\n throw err\n }\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","stripAndResize","generateThumbHash","convertFormat","createRegenerateDocumentHandler","resolvedConfig","input","req","doc","payload","findByID","collection","collectionSlug","id","docId","mimeType","startsWith","output","status","reason","collectionConfig","collections","config","staticDir","upload","Error","isAbsolute","resolve","process","cwd","safeFilename","basename","filename","filePath","join","fileBuffer","readFile","url","env","NEXT_PUBLIC_SERVER_URL","response","fetch","Buffer","from","arrayBuffer","originalSize","length","perCollectionConfig","processed","maxDimensions","stripMetadata","writeFile","buffer","thumbHash","variants","format","formats","result","quality","variantFilename","parse","name","push","filesize","size","width","height","update","data","imageOptimizer","optimizedSize","error","context","imageOptimizer_skip","err","errorMessage","message","String","updateErr","logger"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { CollectionSlug } from 'payload';
2
+ export type ImageFormat = 'webp' | 'avif';
3
+ export type FormatQuality = {
4
+ format: ImageFormat;
5
+ quality: number;
6
+ };
7
+ export type CollectionOptimizerConfig = {
8
+ formats?: FormatQuality[];
9
+ maxDimensions?: {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ };
14
+ export type ImageOptimizerConfig = {
15
+ collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>;
16
+ disabled?: boolean;
17
+ formats?: FormatQuality[];
18
+ generateThumbHash?: boolean;
19
+ maxDimensions?: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ stripMetadata?: boolean;
24
+ };
25
+ export type ResolvedCollectionOptimizerConfig = {
26
+ formats: FormatQuality[];
27
+ maxDimensions: {
28
+ width: number;
29
+ height: number;
30
+ };
31
+ };
32
+ export type ResolvedImageOptimizerConfig = Required<Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>> & {
33
+ collections: ImageOptimizerConfig['collections'];
34
+ disabled: boolean;
35
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type ImageFormat = 'webp' | 'avif'\n\nexport type FormatQuality = {\n format: ImageFormat\n quality: number // 1-100\n}\n\nexport type CollectionOptimizerConfig = {\n formats?: FormatQuality[]\n maxDimensions?: { width: number; height: number }\n}\n\nexport type ImageOptimizerConfig = {\n collections: Partial<Record<CollectionSlug, true | CollectionOptimizerConfig>>\n disabled?: boolean\n formats?: FormatQuality[]\n generateThumbHash?: boolean\n maxDimensions?: { width: number; height: number }\n stripMetadata?: boolean\n}\n\nexport type ResolvedCollectionOptimizerConfig = {\n formats: FormatQuality[]\n maxDimensions: { width: number; height: number }\n}\n\nexport type ResolvedImageOptimizerConfig = Required<\n Pick<ImageOptimizerConfig, 'formats' | 'generateThumbHash' | 'maxDimensions' | 'stripMetadata'>\n> & {\n collections: ImageOptimizerConfig['collections']\n disabled: boolean\n}\n"],"names":[],"mappings":"AA4BA,WAKC"}
@@ -0,0 +1,32 @@
1
+ type ImageOptimizerData = {
2
+ thumbHash?: string | null;
3
+ };
4
+ type ResourceWithOptimizer = {
5
+ imageOptimizer?: ImageOptimizerData | null;
6
+ focalX?: number | null;
7
+ focalY?: number | null;
8
+ };
9
+ export type ImageOptimizerProps = {
10
+ placeholder: 'blur' | 'empty';
11
+ blurDataURL?: string;
12
+ style: {
13
+ objectPosition: string;
14
+ };
15
+ };
16
+ /**
17
+ * Extracts image optimization props from a Payload media resource.
18
+ *
19
+ * Returns props that can be spread onto a Next.js `<Image>` component to add
20
+ * ThumbHash blur placeholders and focal-point-based object positioning.
21
+ *
22
+ * Works with any component — including the Payload website template's `ImageMedia`:
23
+ *
24
+ * ```tsx
25
+ * import { getImageOptimizerProps } from '@inoo-ch/payload-image-optimizer/client'
26
+ *
27
+ * const optimizerProps = getImageOptimizerProps(resource)
28
+ * <NextImage {...existingProps} {...optimizerProps} />
29
+ * ```
30
+ */
31
+ export declare function getImageOptimizerProps(resource: ResourceWithOptimizer | null | undefined): ImageOptimizerProps;
32
+ export {};