@cibule/image-sharp 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @cibule/image-sharp
2
+
3
+ Sharp-based image transformation for Node.js with FileStorage caching.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @cibule/image-sharp @cibule/image @cibule/storage
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { SharpImageTransformer } from '@cibule/image-sharp';
15
+ import { LocalFileStorage } from '@cibule/storage';
16
+
17
+ const source = new LocalFileStorage({ basePath: './uploads' });
18
+ const cache = new LocalFileStorage({ basePath: './cache' });
19
+
20
+ const transformer = new SharpImageTransformer({
21
+ source,
22
+ cache,
23
+ cachePrefix: 'thumbnails', // default: 'thumbnails'
24
+ defaultFormat: 'avif', // default: 'avif'
25
+ });
26
+
27
+ // Transform an image — resizes and converts to AVIF
28
+ const result = await transformer.transform('photos/hero.jpg', {
29
+ width: 800,
30
+ quality: 80, // default: 80
31
+ format: 'webp', // overrides defaultFormat
32
+ fit: 'cover', // default: 'cover'
33
+ });
34
+
35
+ // result.body — ReadableStream<Uint8Array>
36
+ // result.contentType — 'image/webp'
37
+ // result.width — 800
38
+ // result.height — computed by Sharp
39
+ // result.size — byte size of transformed image
40
+
41
+ // Subsequent calls with same key + options serve from cache
42
+ const cached = await transformer.transform('photos/hero.jpg', { width: 800 });
43
+
44
+ // Purge all cached thumbnails
45
+ const purgeResult = await transformer.purgeCache();
46
+ // purgeResult.purgedCount — number of deleted cache entries
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ | Option | Type | Default | Description |
52
+ | --------------- | ------------- | -------------- | ------------------------------------ |
53
+ | `source` | `FileStorage` | _(required)_ | Storage driver for source images |
54
+ | `cache` | `FileStorage` | _(required)_ | Storage driver for cached thumbnails |
55
+ | `cachePrefix` | `string` | `'thumbnails'` | Key prefix for cached images |
56
+ | `defaultFormat` | `ImageFormat` | `'avif'` | Output format when not specified |
57
+
58
+ ## Supported Formats
59
+
60
+ - `avif` — best compression, modern browsers
61
+ - `webp` — good compression, wide support
62
+ - `jpeg` — universal compatibility
63
+ - `png` — lossless, transparency support
64
+
65
+ ## Caching
66
+
67
+ Transformed images are stored in the `cache` FileStorage under a deterministic key:
68
+
69
+ ```
70
+ {cachePrefix}/{sourceKey}/w{width}_q{quality}_{format}_{fit}
71
+ ```
72
+
73
+ Cache hits skip the Sharp transformation entirely and serve directly from storage.
74
+
75
+ ## API
76
+
77
+ ### `SharpImageTransformer`
78
+
79
+ Extends `ImageTransformer` from `@cibule/image`.
80
+
81
+ #### `transform(key: string, options: TransformOptions): Promise<TransformResult>`
82
+
83
+ Downloads the source image, resizes and converts it using Sharp, caches the result, and returns a `TransformResult` with the transformed image body and metadata.
84
+
85
+ #### `purgeCache(): Promise<PurgeCacheResult>`
86
+
87
+ Deletes all cached images under the configured `cachePrefix`. Returns the number of purged entries.
88
+
89
+ ## Requirements
90
+
91
+ - Node.js (Sharp requires native binaries — not compatible with Cloudflare Workers)
92
+ - For CF Workers, use `@cibule/image-cf` instead
93
+
94
+ ## License
95
+
96
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SharpImageTransformer: () => SharpImageTransformer
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // ../image/src/lib/constants.ts
38
+ var DEFAULT_QUALITY = 80;
39
+ var DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;
40
+
41
+ // ../image/src/lib/content-type-map.ts
42
+ var FORMAT_TO_CONTENT_TYPE = {
43
+ avif: "image/avif",
44
+ webp: "image/webp",
45
+ jpeg: "image/jpeg",
46
+ png: "image/png"
47
+ };
48
+ function getContentType(format) {
49
+ return FORMAT_TO_CONTENT_TYPE[format];
50
+ }
51
+
52
+ // ../image/src/lib/image-transformer.ts
53
+ var ImageTransformer = class {
54
+ };
55
+
56
+ // ../storage/src/lib/create-readable-stream.ts
57
+ function createReadableStream(data) {
58
+ return new ReadableStream({
59
+ start(controller) {
60
+ controller.enqueue(new Uint8Array(data));
61
+ controller.close();
62
+ }
63
+ });
64
+ }
65
+
66
+ // ../storage/src/lib/concat-uint8-arrays.ts
67
+ function concatUint8Arrays(chunks) {
68
+ if (chunks.length === 0) {
69
+ return new Uint8Array(0);
70
+ }
71
+ if (chunks.length === 1) {
72
+ return chunks[0] ?? new Uint8Array(0);
73
+ }
74
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
75
+ const result = new Uint8Array(totalLength);
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ result.set(chunk, offset);
79
+ offset += chunk.byteLength;
80
+ }
81
+ return result;
82
+ }
83
+
84
+ // ../storage/src/lib/to-uint8-array.ts
85
+ async function toUint8Array(input) {
86
+ if (input instanceof Uint8Array) {
87
+ return input;
88
+ }
89
+ if (input instanceof ArrayBuffer) {
90
+ return new Uint8Array(input);
91
+ }
92
+ const reader = input.getReader();
93
+ const chunks = [];
94
+ for (; ; ) {
95
+ const { done, value } = await reader.read();
96
+ if (done) break;
97
+ chunks.push(value);
98
+ }
99
+ return concatUint8Arrays(chunks);
100
+ }
101
+
102
+ // src/lib/sharp-image-transformer.ts
103
+ var import_sharp = __toESM(require("sharp"), 1);
104
+
105
+ // src/lib/cache-key.ts
106
+ function buildCacheKey(sourceKey, options, prefix) {
107
+ return `${prefix}/${sourceKey}/w${String(options.width)}_q${String(options.quality)}_${options.format}_${options.fit}`;
108
+ }
109
+
110
+ // src/lib/sharp-image-transformer.ts
111
+ var SharpImageTransformer = class extends ImageTransformer {
112
+ constructor(config) {
113
+ super();
114
+ this.config = config;
115
+ this.cachePrefix = config.cachePrefix ?? "thumbnails";
116
+ }
117
+ cachePrefix;
118
+ async transform(key, options) {
119
+ const format = options.format ?? this.config.defaultFormat ?? "avif";
120
+ const quality = options.quality ?? DEFAULT_QUALITY;
121
+ const fit = options.fit ?? "cover";
122
+ const cacheKey = buildCacheKey(
123
+ key,
124
+ { width: options.width, format, quality, fit },
125
+ this.cachePrefix
126
+ );
127
+ const cached = await this.config.cache.head(cacheKey);
128
+ if (cached) {
129
+ return this.serveCached(cacheKey, cached.size, cached.contentType ?? getContentType(format));
130
+ }
131
+ const sourceObject = await this.config.source.download(key);
132
+ const sourceBytes = await toUint8Array(sourceObject.body);
133
+ const { data, info } = await (0, import_sharp.default)(sourceBytes).resize({ width: options.width, fit }).toFormat(format, { quality }).toBuffer({ resolveWithObject: true });
134
+ const contentType = getContentType(format);
135
+ await this.config.cache.upload(cacheKey, data, { contentType });
136
+ return {
137
+ body: createReadableStream(data),
138
+ contentType,
139
+ width: info.width,
140
+ height: info.height,
141
+ size: info.size
142
+ };
143
+ }
144
+ async purgeCache() {
145
+ const keys = await this.config.cache.listKeys(this.cachePrefix);
146
+ await Promise.all(keys.map((k) => this.config.cache.delete(k)));
147
+ return { purgedCount: keys.length };
148
+ }
149
+ async serveCached(cacheKey, size, contentType) {
150
+ const cached = await this.config.cache.download(cacheKey);
151
+ const bytes = await toUint8Array(cached.body);
152
+ const metadata = await (0, import_sharp.default)(bytes).metadata();
153
+ return {
154
+ body: createReadableStream(bytes),
155
+ contentType,
156
+ width: metadata.width ?? 0,
157
+ height: metadata.height ?? 0,
158
+ size
159
+ };
160
+ }
161
+ };
162
+ // Annotate the CommonJS export names for ESM import in node:
163
+ 0 && (module.exports = {
164
+ SharpImageTransformer
165
+ });
166
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../../image/src/lib/constants.ts", "../../image/src/lib/content-type-map.ts", "../../image/src/lib/image-transformer.ts", "../../storage/src/lib/create-readable-stream.ts", "../../storage/src/lib/concat-uint8-arrays.ts", "../../storage/src/lib/to-uint8-array.ts", "../src/lib/sharp-image-transformer.ts", "../src/lib/cache-key.ts"],
4
+ "sourcesContent": ["export { SharpImageTransformer } from './lib/sharp-image-transformer';\nexport type { SharpImageTransformerConfig } from './lib/sharp-image-transformer-config';\n", "export const ALLOWED_WIDTHS = [384, 640, 750, 828, 1080, 1200, 1920] as const;\n\nexport const ALLOWED_PLACEHOLDER_WIDTHS = [20, 30, 50, 80] as const;\n\nexport const DEFAULT_QUALITY = 80;\n\nexport const DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;\n", "import type { ImageFormat } from './transform-options';\n\nconst FORMAT_TO_CONTENT_TYPE: Record<ImageFormat, string> = {\n avif: 'image/avif',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n png: 'image/png',\n};\n\nexport function getContentType(format: ImageFormat): string {\n return FORMAT_TO_CONTENT_TYPE[format];\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from './transform-options';\n\nexport abstract class ImageTransformer {\n abstract transform(key: string, options: TransformOptions): Promise<TransformResult>;\n abstract purgeCache(): Promise<PurgeCacheResult>;\n}\n", "export function createReadableStream(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller): void {\n controller.enqueue(new Uint8Array(data));\n controller.close();\n },\n });\n}\n", "export function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) {\n return new Uint8Array(0);\n }\n\n if (chunks.length === 1) {\n return chunks[0] ?? new Uint8Array(0);\n }\n\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return result;\n}\n", "import { concatUint8Arrays } from './concat-uint8-arrays';\n\nexport async function toUint8Array(\n input: ReadableStream<Uint8Array> | ArrayBuffer | Uint8Array,\n): Promise<Uint8Array> {\n if (input instanceof Uint8Array) {\n return input;\n }\n\n if (input instanceof ArrayBuffer) {\n return new Uint8Array(input);\n }\n\n const reader = input.getReader();\n const chunks: Uint8Array[] = [];\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n return concatUint8Arrays(chunks);\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';\nimport { DEFAULT_QUALITY, getContentType, ImageTransformer } from '@cibule/image';\nimport { createReadableStream, toUint8Array } from '@cibule/storage';\nimport sharp from 'sharp';\n\nimport { buildCacheKey } from './cache-key';\nimport type { SharpImageTransformerConfig } from './sharp-image-transformer-config';\n\nexport class SharpImageTransformer extends ImageTransformer {\n private readonly cachePrefix: string;\n\n public constructor(private readonly config: SharpImageTransformerConfig) {\n super();\n this.cachePrefix = config.cachePrefix ?? 'thumbnails';\n }\n\n public async transform(key: string, options: TransformOptions): Promise<TransformResult> {\n const format = options.format ?? this.config.defaultFormat ?? 'avif';\n const quality = options.quality ?? DEFAULT_QUALITY;\n const fit = options.fit ?? 'cover';\n const cacheKey = buildCacheKey(\n key,\n { width: options.width, format, quality, fit },\n this.cachePrefix,\n );\n\n const cached = await this.config.cache.head(cacheKey);\n if (cached) {\n return this.serveCached(cacheKey, cached.size, cached.contentType ?? getContentType(format));\n }\n\n const sourceObject = await this.config.source.download(key);\n const sourceBytes = await toUint8Array(sourceObject.body);\n\n const { data, info } = await sharp(sourceBytes)\n .resize({ width: options.width, fit })\n .toFormat(format, { quality })\n .toBuffer({ resolveWithObject: true });\n\n const contentType = getContentType(format);\n await this.config.cache.upload(cacheKey, data, { contentType });\n\n return {\n body: createReadableStream(data),\n contentType,\n width: info.width,\n height: info.height,\n size: info.size,\n };\n }\n\n public async purgeCache(): Promise<PurgeCacheResult> {\n const keys = await this.config.cache.listKeys(this.cachePrefix);\n await Promise.all(keys.map(k => this.config.cache.delete(k)));\n return { purgedCount: keys.length };\n }\n\n private async serveCached(\n cacheKey: string,\n size: number,\n contentType: string,\n ): Promise<TransformResult> {\n const cached = await this.config.cache.download(cacheKey);\n const bytes = await toUint8Array(cached.body);\n const metadata = await sharp(bytes).metadata();\n\n return {\n body: createReadableStream(bytes),\n contentType,\n width: metadata.width ?? 0,\n height: metadata.height ?? 0,\n size,\n };\n }\n}\n", "import type { ImageFit, ImageFormat } from '@cibule/image';\n\nexport interface ResolvedTransformOptions {\n readonly width: number;\n readonly format: ImageFormat;\n readonly quality: number;\n readonly fit: ImageFit;\n}\n\nexport function buildCacheKey(\n sourceKey: string,\n options: ResolvedTransformOptions,\n prefix: string,\n): string {\n return `${prefix}/${sourceKey}/w${String(options.width)}_q${String(options.quality)}_${options.format}_${options.fit}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["sharp"]
7
+ }
@@ -0,0 +1,3 @@
1
+ export { SharpImageTransformer } from './lib/sharp-image-transformer';
2
+ export type { SharpImageTransformerConfig } from './lib/sharp-image-transformer-config';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ // ../image/src/lib/constants.ts
2
+ var DEFAULT_QUALITY = 80;
3
+ var DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;
4
+
5
+ // ../image/src/lib/content-type-map.ts
6
+ var FORMAT_TO_CONTENT_TYPE = {
7
+ avif: "image/avif",
8
+ webp: "image/webp",
9
+ jpeg: "image/jpeg",
10
+ png: "image/png"
11
+ };
12
+ function getContentType(format) {
13
+ return FORMAT_TO_CONTENT_TYPE[format];
14
+ }
15
+
16
+ // ../image/src/lib/image-transformer.ts
17
+ var ImageTransformer = class {
18
+ };
19
+
20
+ // ../storage/src/lib/create-readable-stream.ts
21
+ function createReadableStream(data) {
22
+ return new ReadableStream({
23
+ start(controller) {
24
+ controller.enqueue(new Uint8Array(data));
25
+ controller.close();
26
+ }
27
+ });
28
+ }
29
+
30
+ // ../storage/src/lib/concat-uint8-arrays.ts
31
+ function concatUint8Arrays(chunks) {
32
+ if (chunks.length === 0) {
33
+ return new Uint8Array(0);
34
+ }
35
+ if (chunks.length === 1) {
36
+ return chunks[0] ?? new Uint8Array(0);
37
+ }
38
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
39
+ const result = new Uint8Array(totalLength);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ result.set(chunk, offset);
43
+ offset += chunk.byteLength;
44
+ }
45
+ return result;
46
+ }
47
+
48
+ // ../storage/src/lib/to-uint8-array.ts
49
+ async function toUint8Array(input) {
50
+ if (input instanceof Uint8Array) {
51
+ return input;
52
+ }
53
+ if (input instanceof ArrayBuffer) {
54
+ return new Uint8Array(input);
55
+ }
56
+ const reader = input.getReader();
57
+ const chunks = [];
58
+ for (; ; ) {
59
+ const { done, value } = await reader.read();
60
+ if (done) break;
61
+ chunks.push(value);
62
+ }
63
+ return concatUint8Arrays(chunks);
64
+ }
65
+
66
+ // src/lib/sharp-image-transformer.ts
67
+ import sharp from "sharp";
68
+
69
+ // src/lib/cache-key.ts
70
+ function buildCacheKey(sourceKey, options, prefix) {
71
+ return `${prefix}/${sourceKey}/w${String(options.width)}_q${String(options.quality)}_${options.format}_${options.fit}`;
72
+ }
73
+
74
+ // src/lib/sharp-image-transformer.ts
75
+ var SharpImageTransformer = class extends ImageTransformer {
76
+ constructor(config) {
77
+ super();
78
+ this.config = config;
79
+ this.cachePrefix = config.cachePrefix ?? "thumbnails";
80
+ }
81
+ cachePrefix;
82
+ async transform(key, options) {
83
+ const format = options.format ?? this.config.defaultFormat ?? "avif";
84
+ const quality = options.quality ?? DEFAULT_QUALITY;
85
+ const fit = options.fit ?? "cover";
86
+ const cacheKey = buildCacheKey(
87
+ key,
88
+ { width: options.width, format, quality, fit },
89
+ this.cachePrefix
90
+ );
91
+ const cached = await this.config.cache.head(cacheKey);
92
+ if (cached) {
93
+ return this.serveCached(cacheKey, cached.size, cached.contentType ?? getContentType(format));
94
+ }
95
+ const sourceObject = await this.config.source.download(key);
96
+ const sourceBytes = await toUint8Array(sourceObject.body);
97
+ const { data, info } = await sharp(sourceBytes).resize({ width: options.width, fit }).toFormat(format, { quality }).toBuffer({ resolveWithObject: true });
98
+ const contentType = getContentType(format);
99
+ await this.config.cache.upload(cacheKey, data, { contentType });
100
+ return {
101
+ body: createReadableStream(data),
102
+ contentType,
103
+ width: info.width,
104
+ height: info.height,
105
+ size: info.size
106
+ };
107
+ }
108
+ async purgeCache() {
109
+ const keys = await this.config.cache.listKeys(this.cachePrefix);
110
+ await Promise.all(keys.map((k) => this.config.cache.delete(k)));
111
+ return { purgedCount: keys.length };
112
+ }
113
+ async serveCached(cacheKey, size, contentType) {
114
+ const cached = await this.config.cache.download(cacheKey);
115
+ const bytes = await toUint8Array(cached.body);
116
+ const metadata = await sharp(bytes).metadata();
117
+ return {
118
+ body: createReadableStream(bytes),
119
+ contentType,
120
+ width: metadata.width ?? 0,
121
+ height: metadata.height ?? 0,
122
+ size
123
+ };
124
+ }
125
+ };
126
+ export {
127
+ SharpImageTransformer
128
+ };
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../image/src/lib/constants.ts", "../../image/src/lib/content-type-map.ts", "../../image/src/lib/image-transformer.ts", "../../storage/src/lib/create-readable-stream.ts", "../../storage/src/lib/concat-uint8-arrays.ts", "../../storage/src/lib/to-uint8-array.ts", "../src/lib/sharp-image-transformer.ts", "../src/lib/cache-key.ts"],
4
+ "sourcesContent": ["export const ALLOWED_WIDTHS = [384, 640, 750, 828, 1080, 1200, 1920] as const;\n\nexport const ALLOWED_PLACEHOLDER_WIDTHS = [20, 30, 50, 80] as const;\n\nexport const DEFAULT_QUALITY = 80;\n\nexport const DEFAULT_MAX_SOURCE_SIZE = 60 * 1024 * 1024;\n", "import type { ImageFormat } from './transform-options';\n\nconst FORMAT_TO_CONTENT_TYPE: Record<ImageFormat, string> = {\n avif: 'image/avif',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n png: 'image/png',\n};\n\nexport function getContentType(format: ImageFormat): string {\n return FORMAT_TO_CONTENT_TYPE[format];\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from './transform-options';\n\nexport abstract class ImageTransformer {\n abstract transform(key: string, options: TransformOptions): Promise<TransformResult>;\n abstract purgeCache(): Promise<PurgeCacheResult>;\n}\n", "export function createReadableStream(data: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller): void {\n controller.enqueue(new Uint8Array(data));\n controller.close();\n },\n });\n}\n", "export function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {\n if (chunks.length === 0) {\n return new Uint8Array(0);\n }\n\n if (chunks.length === 1) {\n return chunks[0] ?? new Uint8Array(0);\n }\n\n const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return result;\n}\n", "import { concatUint8Arrays } from './concat-uint8-arrays';\n\nexport async function toUint8Array(\n input: ReadableStream<Uint8Array> | ArrayBuffer | Uint8Array,\n): Promise<Uint8Array> {\n if (input instanceof Uint8Array) {\n return input;\n }\n\n if (input instanceof ArrayBuffer) {\n return new Uint8Array(input);\n }\n\n const reader = input.getReader();\n const chunks: Uint8Array[] = [];\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n return concatUint8Arrays(chunks);\n}\n", "import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';\nimport { DEFAULT_QUALITY, getContentType, ImageTransformer } from '@cibule/image';\nimport { createReadableStream, toUint8Array } from '@cibule/storage';\nimport sharp from 'sharp';\n\nimport { buildCacheKey } from './cache-key';\nimport type { SharpImageTransformerConfig } from './sharp-image-transformer-config';\n\nexport class SharpImageTransformer extends ImageTransformer {\n private readonly cachePrefix: string;\n\n public constructor(private readonly config: SharpImageTransformerConfig) {\n super();\n this.cachePrefix = config.cachePrefix ?? 'thumbnails';\n }\n\n public async transform(key: string, options: TransformOptions): Promise<TransformResult> {\n const format = options.format ?? this.config.defaultFormat ?? 'avif';\n const quality = options.quality ?? DEFAULT_QUALITY;\n const fit = options.fit ?? 'cover';\n const cacheKey = buildCacheKey(\n key,\n { width: options.width, format, quality, fit },\n this.cachePrefix,\n );\n\n const cached = await this.config.cache.head(cacheKey);\n if (cached) {\n return this.serveCached(cacheKey, cached.size, cached.contentType ?? getContentType(format));\n }\n\n const sourceObject = await this.config.source.download(key);\n const sourceBytes = await toUint8Array(sourceObject.body);\n\n const { data, info } = await sharp(sourceBytes)\n .resize({ width: options.width, fit })\n .toFormat(format, { quality })\n .toBuffer({ resolveWithObject: true });\n\n const contentType = getContentType(format);\n await this.config.cache.upload(cacheKey, data, { contentType });\n\n return {\n body: createReadableStream(data),\n contentType,\n width: info.width,\n height: info.height,\n size: info.size,\n };\n }\n\n public async purgeCache(): Promise<PurgeCacheResult> {\n const keys = await this.config.cache.listKeys(this.cachePrefix);\n await Promise.all(keys.map(k => this.config.cache.delete(k)));\n return { purgedCount: keys.length };\n }\n\n private async serveCached(\n cacheKey: string,\n size: number,\n contentType: string,\n ): Promise<TransformResult> {\n const cached = await this.config.cache.download(cacheKey);\n const bytes = await toUint8Array(cached.body);\n const metadata = await sharp(bytes).metadata();\n\n return {\n body: createReadableStream(bytes),\n contentType,\n width: metadata.width ?? 0,\n height: metadata.height ?? 0,\n size,\n };\n }\n}\n", "import type { ImageFit, ImageFormat } from '@cibule/image';\n\nexport interface ResolvedTransformOptions {\n readonly width: number;\n readonly format: ImageFormat;\n readonly quality: number;\n readonly fit: ImageFit;\n}\n\nexport function buildCacheKey(\n sourceKey: string,\n options: ResolvedTransformOptions,\n prefix: string,\n): string {\n return `${prefix}/${sourceKey}/w${String(options.width)}_q${String(options.quality)}_${options.format}_${options.fit}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { ImageFit, ImageFormat } from '@cibule/image';
2
+ export interface ResolvedTransformOptions {
3
+ readonly width: number;
4
+ readonly format: ImageFormat;
5
+ readonly quality: number;
6
+ readonly fit: ImageFit;
7
+ }
8
+ export declare function buildCacheKey(sourceKey: string, options: ResolvedTransformOptions, prefix: string): string;
9
+ //# sourceMappingURL=cache-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/cache-key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;CACxB;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,MAAM,GACb,MAAM,CAER"}
@@ -0,0 +1,9 @@
1
+ import type { ImageFormat } from '@cibule/image';
2
+ import type { FileStorage } from '@cibule/storage';
3
+ export interface SharpImageTransformerConfig {
4
+ readonly source: FileStorage;
5
+ readonly cache: FileStorage;
6
+ readonly cachePrefix?: string;
7
+ readonly defaultFormat?: ImageFormat;
8
+ }
9
+ //# sourceMappingURL=sharp-image-transformer-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharp-image-transformer-config.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/sharp-image-transformer-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC;CACtC"}
@@ -0,0 +1,12 @@
1
+ import type { PurgeCacheResult, TransformOptions, TransformResult } from '@cibule/image';
2
+ import { ImageTransformer } from '@cibule/image';
3
+ import type { SharpImageTransformerConfig } from './sharp-image-transformer-config';
4
+ export declare class SharpImageTransformer extends ImageTransformer {
5
+ private readonly config;
6
+ private readonly cachePrefix;
7
+ constructor(config: SharpImageTransformerConfig);
8
+ transform(key: string, options: TransformOptions): Promise<TransformResult>;
9
+ purgeCache(): Promise<PurgeCacheResult>;
10
+ private serveCached;
11
+ }
12
+ //# sourceMappingURL=sharp-image-transformer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharp-image-transformer.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/sharp-image-transformer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACzF,OAAO,EAAmC,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAKlF,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAEpF,qBAAa,qBAAsB,SAAQ,gBAAgB;IAGtC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAF1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAED,MAAM,EAAE,2BAA2B;IAK1D,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAmC3E,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;YAMtC,WAAW;CAiB1B"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@cibule/image-sharp",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://gitlab.com/LadaBr/cibule",
28
+ "directory": "packages/image-sharp"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@cibule/image": "workspace:*",
35
+ "@cibule/storage": "workspace:*",
36
+ "sharp": "^0.33.0"
37
+ }
38
+ }