@byline/core 1.2.0 → 1.3.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.
|
@@ -38,6 +38,21 @@ export interface UploadFileOptions {
|
|
|
38
38
|
* Providers may use this to namespace storage paths.
|
|
39
39
|
*/
|
|
40
40
|
collection?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Explicit, fully-qualified storage path / object key the provider must
|
|
43
|
+
* write to verbatim. When set, providers MUST place the file at exactly
|
|
44
|
+
* this path — no UUID prefix, year/month rewrite, or `pathPrefix` injection.
|
|
45
|
+
*
|
|
46
|
+
* Used by the image-variant pipeline (`generateImageVariants`) to write
|
|
47
|
+
* sibling files alongside an already-stored original (e.g. so the
|
|
48
|
+
* `thumbnail` variant of `media/2026/05/abc-photo.jpg` lands at
|
|
49
|
+
* `media/2026/05/abc-photo-thumbnail.webp`).
|
|
50
|
+
*
|
|
51
|
+
* Always POSIX-style (forward slashes), no leading slash. Callers are
|
|
52
|
+
* responsible for sanitisation and collision-avoidance — providers do
|
|
53
|
+
* not second-guess the path when this is set.
|
|
54
|
+
*/
|
|
55
|
+
targetStoragePath?: string;
|
|
41
56
|
}
|
|
42
57
|
/**
|
|
43
58
|
* The pluggable file-storage interface.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { ImageSize } from '../@types/collection-types.js';
|
|
9
|
+
import type { IStorageProvider, StoredFileLocation } from '../@types/storage-types.js';
|
|
10
|
+
import type { BylineLogger } from '../logger/index.js';
|
|
11
|
+
export interface ImageMeta {
|
|
12
|
+
width: number | null;
|
|
13
|
+
height: number | null;
|
|
14
|
+
format: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ImageVariantResult {
|
|
17
|
+
name: string;
|
|
18
|
+
storagePath: string;
|
|
19
|
+
width: number | undefined;
|
|
20
|
+
height: number | undefined;
|
|
21
|
+
format: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ProcessImageResult {
|
|
24
|
+
meta: ImageMeta;
|
|
25
|
+
variants: ImageVariantResult[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns true for MIME types that should skip Sharp processing.
|
|
29
|
+
* SVGs are vector files — Sharp cannot meaningfully resize or convert them,
|
|
30
|
+
* and they do not benefit from the responsive-image pipeline.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isBypassMimeType(mimeType: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Extract basic image metadata (dimensions + format) from a buffer using Sharp.
|
|
35
|
+
* Returns nulls for non-image or unrecognised formats.
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractImageMeta(buffer: Buffer, mimeType: string): Promise<ImageMeta>;
|
|
38
|
+
/**
|
|
39
|
+
* Generate the named image variants (sizes) defined in `UploadConfig.sizes`
|
|
40
|
+
* and persist them through the configured `IStorageProvider`.
|
|
41
|
+
*
|
|
42
|
+
* - SVG and GIF files are skipped entirely (bypass types).
|
|
43
|
+
* - Each variant is uploaded as a sibling object to `storedFile.storagePath`,
|
|
44
|
+
* using the naming convention: `<dir>/<basename>-<variantName>.<ext>`
|
|
45
|
+
* (POSIX paths — both local filesystem keys and S3 object keys).
|
|
46
|
+
* - Variant bytes are produced in-memory by Sharp and written via
|
|
47
|
+
* `storage.upload(buffer, { targetStoragePath })` — no direct filesystem
|
|
48
|
+
* access, so the function is provider-agnostic.
|
|
49
|
+
* - Returns an array of `ImageVariantResult` describing what was created.
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateImageVariants(sourceBuffer: Buffer, mimeType: string, storedFile: StoredFileLocation, storage: IStorageProvider, sizes: ImageSize[], logger?: BylineLogger): Promise<ImageVariantResult[]>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
// Sharp ships its own types; no @types/sharp needed.
|
|
10
|
+
import sharp from 'sharp';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// SVG detection
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Returns true for MIME types that should skip Sharp processing.
|
|
16
|
+
* SVGs are vector files — Sharp cannot meaningfully resize or convert them,
|
|
17
|
+
* and they do not benefit from the responsive-image pipeline.
|
|
18
|
+
*/
|
|
19
|
+
export function isBypassMimeType(mimeType) {
|
|
20
|
+
return mimeType === 'image/svg+xml' || mimeType === 'image/gif';
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Metadata extraction
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Extract basic image metadata (dimensions + format) from a buffer using Sharp.
|
|
27
|
+
* Returns nulls for non-image or unrecognised formats.
|
|
28
|
+
*/
|
|
29
|
+
export async function extractImageMeta(buffer, mimeType) {
|
|
30
|
+
if (isBypassMimeType(mimeType)) {
|
|
31
|
+
// For SVGs, attempt a lightweight XML parse for width/height attributes.
|
|
32
|
+
const svgMeta = tryParseSvgDimensions(buffer);
|
|
33
|
+
return { width: svgMeta.width, height: svgMeta.height, format: 'svg' };
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const metadata = await sharp(buffer).metadata();
|
|
37
|
+
return {
|
|
38
|
+
width: metadata.width ?? null,
|
|
39
|
+
height: metadata.height ?? null,
|
|
40
|
+
format: metadata.format ?? null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { width: null, height: null, format: null };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A lightweight SVG width/height parser — avoids pulling in a full XML library.
|
|
49
|
+
* Only reads the root `<svg>` element's `width` and `height` attributes.
|
|
50
|
+
*/
|
|
51
|
+
function tryParseSvgDimensions(buffer) {
|
|
52
|
+
try {
|
|
53
|
+
const text = buffer.toString('utf8', 0, Math.min(buffer.length, 2048));
|
|
54
|
+
const widthMatch = text.match(/<svg[^>]*\swidth=["']([0-9.]+)(?:px)?["']/);
|
|
55
|
+
const heightMatch = text.match(/<svg[^>]*\sheight=["']([0-9.]+)(?:px)?["']/);
|
|
56
|
+
return {
|
|
57
|
+
width: widthMatch ? Math.round(Number.parseFloat(widthMatch[1])) : null,
|
|
58
|
+
height: heightMatch ? Math.round(Number.parseFloat(heightMatch[1])) : null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { width: null, height: null };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Variant generation
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Map a Sharp output format keyword to the corresponding image MIME type.
|
|
70
|
+
* Unknown formats fall back to `application/octet-stream` — the storage
|
|
71
|
+
* provider will accept it; only the served `Content-Type` is affected.
|
|
72
|
+
*/
|
|
73
|
+
function mimeTypeForFormat(format) {
|
|
74
|
+
switch (format) {
|
|
75
|
+
case 'jpeg':
|
|
76
|
+
return 'image/jpeg';
|
|
77
|
+
case 'png':
|
|
78
|
+
return 'image/png';
|
|
79
|
+
case 'webp':
|
|
80
|
+
return 'image/webp';
|
|
81
|
+
case 'avif':
|
|
82
|
+
return 'image/avif';
|
|
83
|
+
default:
|
|
84
|
+
return 'application/octet-stream';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate the named image variants (sizes) defined in `UploadConfig.sizes`
|
|
89
|
+
* and persist them through the configured `IStorageProvider`.
|
|
90
|
+
*
|
|
91
|
+
* - SVG and GIF files are skipped entirely (bypass types).
|
|
92
|
+
* - Each variant is uploaded as a sibling object to `storedFile.storagePath`,
|
|
93
|
+
* using the naming convention: `<dir>/<basename>-<variantName>.<ext>`
|
|
94
|
+
* (POSIX paths — both local filesystem keys and S3 object keys).
|
|
95
|
+
* - Variant bytes are produced in-memory by Sharp and written via
|
|
96
|
+
* `storage.upload(buffer, { targetStoragePath })` — no direct filesystem
|
|
97
|
+
* access, so the function is provider-agnostic.
|
|
98
|
+
* - Returns an array of `ImageVariantResult` describing what was created.
|
|
99
|
+
*/
|
|
100
|
+
export async function generateImageVariants(sourceBuffer, mimeType, storedFile, storage, sizes, logger) {
|
|
101
|
+
if (isBypassMimeType(mimeType) || sizes.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
// Storage paths are POSIX-style across providers; use `path.posix` so
|
|
105
|
+
// Windows local-fs hosts don't introduce backslashes into S3 object keys.
|
|
106
|
+
const sourcePath = storedFile.storagePath;
|
|
107
|
+
const originalExt = path.posix.extname(sourcePath);
|
|
108
|
+
const originalBase = path.posix.basename(sourcePath, originalExt);
|
|
109
|
+
const variantDir = path.posix.dirname(sourcePath);
|
|
110
|
+
const variants = [];
|
|
111
|
+
for (const size of sizes) {
|
|
112
|
+
const outputFormat = size.format ?? 'webp';
|
|
113
|
+
const variantFilename = `${originalBase}-${size.name}.${outputFormat}`;
|
|
114
|
+
const variantStoragePath = variantDir === '.' ? variantFilename : `${variantDir}/${variantFilename}`;
|
|
115
|
+
try {
|
|
116
|
+
let pipeline = sharp(sourceBuffer);
|
|
117
|
+
const resizeOptions = {
|
|
118
|
+
width: size.width,
|
|
119
|
+
height: size.height,
|
|
120
|
+
fit: size.fit ?? 'cover',
|
|
121
|
+
withoutEnlargement: true,
|
|
122
|
+
};
|
|
123
|
+
pipeline = pipeline.resize(resizeOptions);
|
|
124
|
+
// Apply format + quality.
|
|
125
|
+
switch (outputFormat) {
|
|
126
|
+
case 'jpeg':
|
|
127
|
+
pipeline = pipeline.jpeg({ quality: size.quality ?? 85 });
|
|
128
|
+
break;
|
|
129
|
+
case 'png':
|
|
130
|
+
pipeline = pipeline.png({ quality: size.quality ?? 85 });
|
|
131
|
+
break;
|
|
132
|
+
case 'webp':
|
|
133
|
+
pipeline = pipeline.webp({ quality: size.quality ?? 85 });
|
|
134
|
+
break;
|
|
135
|
+
case 'avif':
|
|
136
|
+
pipeline = pipeline.avif({ quality: size.quality ?? 55 });
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
pipeline = pipeline.webp({ quality: size.quality ?? 85 });
|
|
140
|
+
}
|
|
141
|
+
const variantBuffer = await pipeline.toBuffer();
|
|
142
|
+
const sharpMeta = await sharp(variantBuffer).metadata();
|
|
143
|
+
await storage.upload(variantBuffer, {
|
|
144
|
+
filename: variantFilename,
|
|
145
|
+
mimeType: mimeTypeForFormat(outputFormat),
|
|
146
|
+
size: variantBuffer.byteLength,
|
|
147
|
+
targetStoragePath: variantStoragePath,
|
|
148
|
+
});
|
|
149
|
+
variants.push({
|
|
150
|
+
name: size.name,
|
|
151
|
+
storagePath: variantStoragePath,
|
|
152
|
+
width: sharpMeta.width,
|
|
153
|
+
height: sharpMeta.height,
|
|
154
|
+
format: outputFormat,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
logger?.error({ err, variant: size.name }, 'failed to generate image variant');
|
|
159
|
+
// Non-fatal: skip this variant but continue with others.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return variants;
|
|
163
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Storage-agnostic image-processing helpers (sharp-backed). Lives in the
|
|
10
|
+
* neutral `@byline/core/image` subpath so any storage provider —
|
|
11
|
+
* `@byline/storage-local`, `@byline/storage-s3`, future providers — can
|
|
12
|
+
* consume the same metadata extraction and variant generation utilities
|
|
13
|
+
* without taking a dependency on a sibling provider package.
|
|
14
|
+
*/
|
|
15
|
+
export { extractImageMeta, generateImageVariants, isBypassMimeType, } from './image-processor.js';
|
|
16
|
+
export type { ImageMeta, ImageVariantResult, ProcessImageResult, } from './image-processor.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Storage-agnostic image-processing helpers (sharp-backed). Lives in the
|
|
10
|
+
* neutral `@byline/core/image` subpath so any storage provider —
|
|
11
|
+
* `@byline/storage-local`, `@byline/storage-s3`, future providers — can
|
|
12
|
+
* consume the same metadata extraction and variant generation utilities
|
|
13
|
+
* without taking a dependency on a sibling provider package.
|
|
14
|
+
*/
|
|
15
|
+
export { extractImageMeta, generateImageVariants, isBypassMimeType, } from './image-processor.js';
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
"import": "./dist/validation/index.js",
|
|
62
62
|
"require": "./dist/validation/index.js"
|
|
63
63
|
},
|
|
64
|
+
"./image": {
|
|
65
|
+
"types": "./dist/image/index.d.ts",
|
|
66
|
+
"import": "./dist/image/index.js",
|
|
67
|
+
"require": "./dist/image/index.js"
|
|
68
|
+
},
|
|
64
69
|
"./package.json": "./package.json"
|
|
65
70
|
},
|
|
66
71
|
"files": [
|
|
@@ -71,9 +76,10 @@
|
|
|
71
76
|
"jose": "^6.2.3",
|
|
72
77
|
"npm-run-all": "^4.1.5",
|
|
73
78
|
"pino": "^10.3.1",
|
|
79
|
+
"sharp": "^0.34.5",
|
|
74
80
|
"uuid": "^14.0.0",
|
|
75
81
|
"zod": "^4.4.2",
|
|
76
|
-
"@byline/auth": "1.
|
|
82
|
+
"@byline/auth": "1.3.0"
|
|
77
83
|
},
|
|
78
84
|
"devDependencies": {
|
|
79
85
|
"@biomejs/biome": "2.4.14",
|