simple-photo-gallery 2.0.11-rc.1 → 2.0.11-rc.3
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/dist/index.cjs +1257 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.js +2 -86
- package/dist/index.js.map +1 -1
- package/dist/lib/index.cjs +152 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +92 -0
- package/dist/lib/index.d.ts +92 -0
- package/dist/lib/index.js +136 -0
- package/dist/lib/index.js.map +1 -0
- package/package.json +16 -4
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var blurhash = require('blurhash');
|
|
4
|
+
var ExifReader = require('exifreader');
|
|
5
|
+
var sharp = require('sharp');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var ffprobe = require('node-ffprobe');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var ExifReader__default = /*#__PURE__*/_interopDefault(ExifReader);
|
|
13
|
+
var sharp__default = /*#__PURE__*/_interopDefault(sharp);
|
|
14
|
+
var ffprobe__default = /*#__PURE__*/_interopDefault(ffprobe);
|
|
15
|
+
|
|
16
|
+
// src/utils/blurhash.ts
|
|
17
|
+
async function loadImage(imagePath) {
|
|
18
|
+
return sharp__default.default(imagePath).rotate();
|
|
19
|
+
}
|
|
20
|
+
async function loadImageWithMetadata(imagePath) {
|
|
21
|
+
const image = sharp__default.default(imagePath);
|
|
22
|
+
const metadata = await image.metadata();
|
|
23
|
+
image.rotate();
|
|
24
|
+
const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;
|
|
25
|
+
if (needsDimensionSwap) {
|
|
26
|
+
const originalWidth = metadata.width;
|
|
27
|
+
metadata.width = metadata.height;
|
|
28
|
+
metadata.height = originalWidth;
|
|
29
|
+
}
|
|
30
|
+
return { image, metadata };
|
|
31
|
+
}
|
|
32
|
+
async function resizeImage(image, outputPath, width, height, format = "avif") {
|
|
33
|
+
await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);
|
|
34
|
+
}
|
|
35
|
+
async function cropAndResizeImage(image, outputPath, width, height, format = "avif") {
|
|
36
|
+
await image.resize(width, height, {
|
|
37
|
+
fit: "cover",
|
|
38
|
+
withoutEnlargement: true
|
|
39
|
+
}).toFormat(format).toFile(outputPath);
|
|
40
|
+
}
|
|
41
|
+
async function getImageDescription(imagePath) {
|
|
42
|
+
try {
|
|
43
|
+
const tags = await ExifReader__default.default.load(imagePath);
|
|
44
|
+
if (tags.description?.description) return tags.description.description;
|
|
45
|
+
if (tags.ImageDescription?.description) return tags.ImageDescription.description;
|
|
46
|
+
if (tags.UserComment && typeof tags.UserComment === "object" && tags.UserComment !== null && "description" in tags.UserComment) {
|
|
47
|
+
return tags.UserComment.description;
|
|
48
|
+
}
|
|
49
|
+
if (tags.ExtDescrAccessibility?.description) return tags.ExtDescrAccessibility.description;
|
|
50
|
+
if (tags["Caption/Abstract"]?.description) return tags["Caption/Abstract"].description;
|
|
51
|
+
if (tags.XPTitle?.description) return tags.XPTitle.description;
|
|
52
|
+
if (tags.XPComment?.description) return tags.XPComment.description;
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
|
|
58
|
+
const originalWidth = metadata.width || 0;
|
|
59
|
+
const originalHeight = metadata.height || 0;
|
|
60
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
61
|
+
throw new Error("Invalid image dimensions");
|
|
62
|
+
}
|
|
63
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
64
|
+
let width;
|
|
65
|
+
let height;
|
|
66
|
+
if (originalWidth > originalHeight) {
|
|
67
|
+
width = size;
|
|
68
|
+
height = Math.round(size / aspectRatio);
|
|
69
|
+
} else {
|
|
70
|
+
width = Math.round(size * aspectRatio);
|
|
71
|
+
height = size;
|
|
72
|
+
}
|
|
73
|
+
await resizeImage(image, outputPath, width, height);
|
|
74
|
+
await resizeImage(image, outputPathRetina, width * 2, height * 2);
|
|
75
|
+
return { width, height };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/utils/blurhash.ts
|
|
79
|
+
async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
|
|
80
|
+
const image = await loadImage(imagePath);
|
|
81
|
+
const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
82
|
+
const pixels = new Uint8ClampedArray(data.buffer);
|
|
83
|
+
return blurhash.encode(pixels, info.width, info.height, componentX, componentY);
|
|
84
|
+
}
|
|
85
|
+
async function getVideoDimensions(filePath) {
|
|
86
|
+
const data = await ffprobe__default.default(filePath);
|
|
87
|
+
const videoStream = data.streams.find((stream) => stream.codec_type === "video");
|
|
88
|
+
if (!videoStream) {
|
|
89
|
+
throw new Error("No video stream found");
|
|
90
|
+
}
|
|
91
|
+
const dimensions = {
|
|
92
|
+
width: videoStream.width || 0,
|
|
93
|
+
height: videoStream.height || 0
|
|
94
|
+
};
|
|
95
|
+
if (dimensions.width === 0 || dimensions.height === 0) {
|
|
96
|
+
throw new Error("Invalid video dimensions");
|
|
97
|
+
}
|
|
98
|
+
return dimensions;
|
|
99
|
+
}
|
|
100
|
+
async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
|
|
101
|
+
const aspectRatio = videoDimensions.width / videoDimensions.height;
|
|
102
|
+
const width = Math.round(height * aspectRatio);
|
|
103
|
+
const tempFramePath = `${outputPath}.temp.png`;
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const ffmpeg = child_process.spawn("ffmpeg", [
|
|
106
|
+
"-i",
|
|
107
|
+
inputPath,
|
|
108
|
+
"-vframes",
|
|
109
|
+
"1",
|
|
110
|
+
"-y",
|
|
111
|
+
"-loglevel",
|
|
112
|
+
verbose ? "error" : "quiet",
|
|
113
|
+
tempFramePath
|
|
114
|
+
]);
|
|
115
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
116
|
+
console.log(`ffmpeg: ${data.toString()}`);
|
|
117
|
+
});
|
|
118
|
+
ffmpeg.on("close", async (code) => {
|
|
119
|
+
if (code === 0) {
|
|
120
|
+
try {
|
|
121
|
+
const frameImage = sharp__default.default(tempFramePath);
|
|
122
|
+
await resizeImage(frameImage, outputPath, width, height);
|
|
123
|
+
await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);
|
|
124
|
+
try {
|
|
125
|
+
await fs.promises.unlink(tempFramePath);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
resolve({ width, height });
|
|
129
|
+
} catch (sharpError) {
|
|
130
|
+
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
ffmpeg.on("error", (error) => {
|
|
137
|
+
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.createImageThumbnails = createImageThumbnails;
|
|
143
|
+
exports.createVideoThumbnails = createVideoThumbnails;
|
|
144
|
+
exports.cropAndResizeImage = cropAndResizeImage;
|
|
145
|
+
exports.generateBlurHash = generateBlurHash;
|
|
146
|
+
exports.getImageDescription = getImageDescription;
|
|
147
|
+
exports.getVideoDimensions = getVideoDimensions;
|
|
148
|
+
exports.loadImage = loadImage;
|
|
149
|
+
exports.loadImageWithMetadata = loadImageWithMetadata;
|
|
150
|
+
exports.resizeImage = resizeImage;
|
|
151
|
+
//# sourceMappingURL=index.cjs.map
|
|
152
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts"],"names":["sharp","ExifReader","encode","ffprobe","spawn","fs"],"mappings":"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","file":"index.cjs","sourcesContent":["import ExifReader from 'exifreader';\nimport sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Extracts description from image EXIF data\n * @param metadata - Sharp metadata object containing EXIF data\n * @returns Promise resolving to image description or undefined if not found\n */\nexport async function getImageDescription(imagePath: string): Promise<string | undefined> {\n try {\n const tags = await ExifReader.load(imagePath);\n\n // Description\n if (tags.description?.description) return tags.description.description;\n\n // ImageDescription\n if (tags.ImageDescription?.description) return tags.ImageDescription.description;\n\n // UserComment\n if (\n tags.UserComment &&\n typeof tags.UserComment === 'object' &&\n tags.UserComment !== null &&\n 'description' in tags.UserComment\n ) {\n return (tags.UserComment as { description: string }).description;\n }\n\n // ExtDescrAccessibility\n if (tags.ExtDescrAccessibility?.description) return tags.ExtDescrAccessibility.description;\n\n // Caption/Abstract\n if (tags['Caption/Abstract']?.description) return tags['Caption/Abstract'].description;\n\n // XP Title\n if (tags.XPTitle?.description) return tags.XPTitle.description;\n\n // XP Comment\n if (tags.XPComment?.description) return tags.XPComment.description;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export { GalleryData, GalleryMetadata, GallerySection, MediaFile, MediaFileWithPath, SubGallery, Thumbnail } from '@simple-photo-gallery/common/src/gallery';
|
|
2
|
+
import { Sharp, Metadata, FormatEnum } from 'sharp';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a BlurHash from an image file or Sharp instance
|
|
6
|
+
* @param imagePath - Path to image file or Sharp instance
|
|
7
|
+
* @param componentX - Number of x components (default: 4)
|
|
8
|
+
* @param componentY - Number of y components (default: 3)
|
|
9
|
+
* @returns Promise resolving to BlurHash string
|
|
10
|
+
*/
|
|
11
|
+
declare function generateBlurHash(imagePath: string, componentX?: number, componentY?: number): Promise<string>;
|
|
12
|
+
|
|
13
|
+
/** Represents width and height dimensions */
|
|
14
|
+
interface Dimensions {
|
|
15
|
+
/** Width in pixels */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Height in pixels */
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
/** Represents an image with metadata */
|
|
21
|
+
interface ImageWithMetadata {
|
|
22
|
+
/** The image */
|
|
23
|
+
image: Sharp;
|
|
24
|
+
/** The metadata */
|
|
25
|
+
metadata: Metadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loads an image and auto-rotates it based on EXIF orientation.
|
|
30
|
+
* @param imagePath - Path to the image file
|
|
31
|
+
* @returns Promise resolving to Sharp image instance
|
|
32
|
+
*/
|
|
33
|
+
declare function loadImage(imagePath: string): Promise<Sharp>;
|
|
34
|
+
/**
|
|
35
|
+
* Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.
|
|
36
|
+
* @param imagePath - Path to the image file
|
|
37
|
+
* @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata
|
|
38
|
+
*/
|
|
39
|
+
declare function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata>;
|
|
40
|
+
/**
|
|
41
|
+
* Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.
|
|
42
|
+
* @param image - Sharp image instance
|
|
43
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
44
|
+
* @param width - Target width for thumbnail
|
|
45
|
+
* @param height - Target height for thumbnail
|
|
46
|
+
*/
|
|
47
|
+
declare function resizeImage(image: Sharp, outputPath: string, width: number, height: number, format?: keyof FormatEnum): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.
|
|
50
|
+
* @param image - Sharp image instance
|
|
51
|
+
* @param outputPath - Path where the image should be saved
|
|
52
|
+
* @param width - Target width for the image
|
|
53
|
+
* @param height - Target height for the image
|
|
54
|
+
*/
|
|
55
|
+
declare function cropAndResizeImage(image: Sharp, outputPath: string, width: number, height: number, format?: keyof FormatEnum): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Extracts description from image EXIF data
|
|
58
|
+
* @param metadata - Sharp metadata object containing EXIF data
|
|
59
|
+
* @returns Promise resolving to image description or undefined if not found
|
|
60
|
+
*/
|
|
61
|
+
declare function getImageDescription(imagePath: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Creates regular and retina thumbnails for an image while maintaining aspect ratio
|
|
64
|
+
* @param image - Sharp image instance
|
|
65
|
+
* @param metadata - Image metadata containing dimensions
|
|
66
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
67
|
+
* @param outputPathRetina - Path where retina thumbnail should be saved
|
|
68
|
+
* @param size - Target size of the longer side of the thumbnail
|
|
69
|
+
* @returns Promise resolving to thumbnail dimensions
|
|
70
|
+
*/
|
|
71
|
+
declare function createImageThumbnails(image: Sharp, metadata: Metadata, outputPath: string, outputPathRetina: string, size: number): Promise<Dimensions>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets video dimensions using ffprobe
|
|
75
|
+
* @param filePath - Path to the video file
|
|
76
|
+
* @returns Promise resolving to video dimensions
|
|
77
|
+
* @throws Error if no video stream found or invalid dimensions
|
|
78
|
+
*/
|
|
79
|
+
declare function getVideoDimensions(filePath: string): Promise<Dimensions>;
|
|
80
|
+
/**
|
|
81
|
+
* Creates regular and retina thumbnails for a video by extracting the first frame
|
|
82
|
+
* @param inputPath - Path to the video file
|
|
83
|
+
* @param videoDimensions - Original video dimensions
|
|
84
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
85
|
+
* @param outputPathRetina - Path where retina thumbnail should be saved
|
|
86
|
+
* @param height - Target height for thumbnail
|
|
87
|
+
* @param verbose - Whether to enable verbose ffmpeg output
|
|
88
|
+
* @returns Promise resolving to thumbnail dimensions
|
|
89
|
+
*/
|
|
90
|
+
declare function createVideoThumbnails(inputPath: string, videoDimensions: Dimensions, outputPath: string, outputPathRetina: string, height: number, verbose?: boolean): Promise<Dimensions>;
|
|
91
|
+
|
|
92
|
+
export { type Dimensions, type ImageWithMetadata, createImageThumbnails, createVideoThumbnails, cropAndResizeImage, generateBlurHash, getImageDescription, getVideoDimensions, loadImage, loadImageWithMetadata, resizeImage };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export { GalleryData, GalleryMetadata, GallerySection, MediaFile, MediaFileWithPath, SubGallery, Thumbnail } from '@simple-photo-gallery/common/src/gallery';
|
|
2
|
+
import { Sharp, Metadata, FormatEnum } from 'sharp';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a BlurHash from an image file or Sharp instance
|
|
6
|
+
* @param imagePath - Path to image file or Sharp instance
|
|
7
|
+
* @param componentX - Number of x components (default: 4)
|
|
8
|
+
* @param componentY - Number of y components (default: 3)
|
|
9
|
+
* @returns Promise resolving to BlurHash string
|
|
10
|
+
*/
|
|
11
|
+
declare function generateBlurHash(imagePath: string, componentX?: number, componentY?: number): Promise<string>;
|
|
12
|
+
|
|
13
|
+
/** Represents width and height dimensions */
|
|
14
|
+
interface Dimensions {
|
|
15
|
+
/** Width in pixels */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Height in pixels */
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
/** Represents an image with metadata */
|
|
21
|
+
interface ImageWithMetadata {
|
|
22
|
+
/** The image */
|
|
23
|
+
image: Sharp;
|
|
24
|
+
/** The metadata */
|
|
25
|
+
metadata: Metadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loads an image and auto-rotates it based on EXIF orientation.
|
|
30
|
+
* @param imagePath - Path to the image file
|
|
31
|
+
* @returns Promise resolving to Sharp image instance
|
|
32
|
+
*/
|
|
33
|
+
declare function loadImage(imagePath: string): Promise<Sharp>;
|
|
34
|
+
/**
|
|
35
|
+
* Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.
|
|
36
|
+
* @param imagePath - Path to the image file
|
|
37
|
+
* @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata
|
|
38
|
+
*/
|
|
39
|
+
declare function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata>;
|
|
40
|
+
/**
|
|
41
|
+
* Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.
|
|
42
|
+
* @param image - Sharp image instance
|
|
43
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
44
|
+
* @param width - Target width for thumbnail
|
|
45
|
+
* @param height - Target height for thumbnail
|
|
46
|
+
*/
|
|
47
|
+
declare function resizeImage(image: Sharp, outputPath: string, width: number, height: number, format?: keyof FormatEnum): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.
|
|
50
|
+
* @param image - Sharp image instance
|
|
51
|
+
* @param outputPath - Path where the image should be saved
|
|
52
|
+
* @param width - Target width for the image
|
|
53
|
+
* @param height - Target height for the image
|
|
54
|
+
*/
|
|
55
|
+
declare function cropAndResizeImage(image: Sharp, outputPath: string, width: number, height: number, format?: keyof FormatEnum): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Extracts description from image EXIF data
|
|
58
|
+
* @param metadata - Sharp metadata object containing EXIF data
|
|
59
|
+
* @returns Promise resolving to image description or undefined if not found
|
|
60
|
+
*/
|
|
61
|
+
declare function getImageDescription(imagePath: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Creates regular and retina thumbnails for an image while maintaining aspect ratio
|
|
64
|
+
* @param image - Sharp image instance
|
|
65
|
+
* @param metadata - Image metadata containing dimensions
|
|
66
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
67
|
+
* @param outputPathRetina - Path where retina thumbnail should be saved
|
|
68
|
+
* @param size - Target size of the longer side of the thumbnail
|
|
69
|
+
* @returns Promise resolving to thumbnail dimensions
|
|
70
|
+
*/
|
|
71
|
+
declare function createImageThumbnails(image: Sharp, metadata: Metadata, outputPath: string, outputPathRetina: string, size: number): Promise<Dimensions>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets video dimensions using ffprobe
|
|
75
|
+
* @param filePath - Path to the video file
|
|
76
|
+
* @returns Promise resolving to video dimensions
|
|
77
|
+
* @throws Error if no video stream found or invalid dimensions
|
|
78
|
+
*/
|
|
79
|
+
declare function getVideoDimensions(filePath: string): Promise<Dimensions>;
|
|
80
|
+
/**
|
|
81
|
+
* Creates regular and retina thumbnails for a video by extracting the first frame
|
|
82
|
+
* @param inputPath - Path to the video file
|
|
83
|
+
* @param videoDimensions - Original video dimensions
|
|
84
|
+
* @param outputPath - Path where thumbnail should be saved
|
|
85
|
+
* @param outputPathRetina - Path where retina thumbnail should be saved
|
|
86
|
+
* @param height - Target height for thumbnail
|
|
87
|
+
* @param verbose - Whether to enable verbose ffmpeg output
|
|
88
|
+
* @returns Promise resolving to thumbnail dimensions
|
|
89
|
+
*/
|
|
90
|
+
declare function createVideoThumbnails(inputPath: string, videoDimensions: Dimensions, outputPath: string, outputPathRetina: string, height: number, verbose?: boolean): Promise<Dimensions>;
|
|
91
|
+
|
|
92
|
+
export { type Dimensions, type ImageWithMetadata, createImageThumbnails, createVideoThumbnails, cropAndResizeImage, generateBlurHash, getImageDescription, getVideoDimensions, loadImage, loadImageWithMetadata, resizeImage };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { encode } from 'blurhash';
|
|
2
|
+
import ExifReader from 'exifreader';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { promises } from 'fs';
|
|
6
|
+
import ffprobe from 'node-ffprobe';
|
|
7
|
+
|
|
8
|
+
// src/utils/blurhash.ts
|
|
9
|
+
async function loadImage(imagePath) {
|
|
10
|
+
return sharp(imagePath).rotate();
|
|
11
|
+
}
|
|
12
|
+
async function loadImageWithMetadata(imagePath) {
|
|
13
|
+
const image = sharp(imagePath);
|
|
14
|
+
const metadata = await image.metadata();
|
|
15
|
+
image.rotate();
|
|
16
|
+
const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;
|
|
17
|
+
if (needsDimensionSwap) {
|
|
18
|
+
const originalWidth = metadata.width;
|
|
19
|
+
metadata.width = metadata.height;
|
|
20
|
+
metadata.height = originalWidth;
|
|
21
|
+
}
|
|
22
|
+
return { image, metadata };
|
|
23
|
+
}
|
|
24
|
+
async function resizeImage(image, outputPath, width, height, format = "avif") {
|
|
25
|
+
await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);
|
|
26
|
+
}
|
|
27
|
+
async function cropAndResizeImage(image, outputPath, width, height, format = "avif") {
|
|
28
|
+
await image.resize(width, height, {
|
|
29
|
+
fit: "cover",
|
|
30
|
+
withoutEnlargement: true
|
|
31
|
+
}).toFormat(format).toFile(outputPath);
|
|
32
|
+
}
|
|
33
|
+
async function getImageDescription(imagePath) {
|
|
34
|
+
try {
|
|
35
|
+
const tags = await ExifReader.load(imagePath);
|
|
36
|
+
if (tags.description?.description) return tags.description.description;
|
|
37
|
+
if (tags.ImageDescription?.description) return tags.ImageDescription.description;
|
|
38
|
+
if (tags.UserComment && typeof tags.UserComment === "object" && tags.UserComment !== null && "description" in tags.UserComment) {
|
|
39
|
+
return tags.UserComment.description;
|
|
40
|
+
}
|
|
41
|
+
if (tags.ExtDescrAccessibility?.description) return tags.ExtDescrAccessibility.description;
|
|
42
|
+
if (tags["Caption/Abstract"]?.description) return tags["Caption/Abstract"].description;
|
|
43
|
+
if (tags.XPTitle?.description) return tags.XPTitle.description;
|
|
44
|
+
if (tags.XPComment?.description) return tags.XPComment.description;
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
|
|
50
|
+
const originalWidth = metadata.width || 0;
|
|
51
|
+
const originalHeight = metadata.height || 0;
|
|
52
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
53
|
+
throw new Error("Invalid image dimensions");
|
|
54
|
+
}
|
|
55
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
56
|
+
let width;
|
|
57
|
+
let height;
|
|
58
|
+
if (originalWidth > originalHeight) {
|
|
59
|
+
width = size;
|
|
60
|
+
height = Math.round(size / aspectRatio);
|
|
61
|
+
} else {
|
|
62
|
+
width = Math.round(size * aspectRatio);
|
|
63
|
+
height = size;
|
|
64
|
+
}
|
|
65
|
+
await resizeImage(image, outputPath, width, height);
|
|
66
|
+
await resizeImage(image, outputPathRetina, width * 2, height * 2);
|
|
67
|
+
return { width, height };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/utils/blurhash.ts
|
|
71
|
+
async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
|
|
72
|
+
const image = await loadImage(imagePath);
|
|
73
|
+
const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
74
|
+
const pixels = new Uint8ClampedArray(data.buffer);
|
|
75
|
+
return encode(pixels, info.width, info.height, componentX, componentY);
|
|
76
|
+
}
|
|
77
|
+
async function getVideoDimensions(filePath) {
|
|
78
|
+
const data = await ffprobe(filePath);
|
|
79
|
+
const videoStream = data.streams.find((stream) => stream.codec_type === "video");
|
|
80
|
+
if (!videoStream) {
|
|
81
|
+
throw new Error("No video stream found");
|
|
82
|
+
}
|
|
83
|
+
const dimensions = {
|
|
84
|
+
width: videoStream.width || 0,
|
|
85
|
+
height: videoStream.height || 0
|
|
86
|
+
};
|
|
87
|
+
if (dimensions.width === 0 || dimensions.height === 0) {
|
|
88
|
+
throw new Error("Invalid video dimensions");
|
|
89
|
+
}
|
|
90
|
+
return dimensions;
|
|
91
|
+
}
|
|
92
|
+
async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
|
|
93
|
+
const aspectRatio = videoDimensions.width / videoDimensions.height;
|
|
94
|
+
const width = Math.round(height * aspectRatio);
|
|
95
|
+
const tempFramePath = `${outputPath}.temp.png`;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const ffmpeg = spawn("ffmpeg", [
|
|
98
|
+
"-i",
|
|
99
|
+
inputPath,
|
|
100
|
+
"-vframes",
|
|
101
|
+
"1",
|
|
102
|
+
"-y",
|
|
103
|
+
"-loglevel",
|
|
104
|
+
verbose ? "error" : "quiet",
|
|
105
|
+
tempFramePath
|
|
106
|
+
]);
|
|
107
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
108
|
+
console.log(`ffmpeg: ${data.toString()}`);
|
|
109
|
+
});
|
|
110
|
+
ffmpeg.on("close", async (code) => {
|
|
111
|
+
if (code === 0) {
|
|
112
|
+
try {
|
|
113
|
+
const frameImage = sharp(tempFramePath);
|
|
114
|
+
await resizeImage(frameImage, outputPath, width, height);
|
|
115
|
+
await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);
|
|
116
|
+
try {
|
|
117
|
+
await promises.unlink(tempFramePath);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
resolve({ width, height });
|
|
121
|
+
} catch (sharpError) {
|
|
122
|
+
reject(new Error(`Failed to process extracted frame: ${sharpError}`));
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
ffmpeg.on("error", (error) => {
|
|
129
|
+
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { createImageThumbnails, createVideoThumbnails, cropAndResizeImage, generateBlurHash, getImageDescription, getVideoDimensions, loadImage, loadImageWithMetadata, resizeImage };
|
|
135
|
+
//# sourceMappingURL=index.js.map
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts"],"names":["sharp","fs"],"mappings":"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","file":"index.js","sourcesContent":["import ExifReader from 'exifreader';\nimport sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Extracts description from image EXIF data\n * @param metadata - Sharp metadata object containing EXIF data\n * @returns Promise resolving to image description or undefined if not found\n */\nexport async function getImageDescription(imagePath: string): Promise<string | undefined> {\n try {\n const tags = await ExifReader.load(imagePath);\n\n // Description\n if (tags.description?.description) return tags.description.description;\n\n // ImageDescription\n if (tags.ImageDescription?.description) return tags.ImageDescription.description;\n\n // UserComment\n if (\n tags.UserComment &&\n typeof tags.UserComment === 'object' &&\n tags.UserComment !== null &&\n 'description' in tags.UserComment\n ) {\n return (tags.UserComment as { description: string }).description;\n }\n\n // ExtDescrAccessibility\n if (tags.ExtDescrAccessibility?.description) return tags.ExtDescrAccessibility.description;\n\n // Caption/Abstract\n if (tags['Caption/Abstract']?.description) return tags['Caption/Abstract'].description;\n\n // XP Title\n if (tags.XPTitle?.description) return tags.XPTitle.description;\n\n // XP Comment\n if (tags.XPComment?.description) return tags.XPComment.description;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-photo-gallery",
|
|
3
|
-
"version": "2.0.11-rc.
|
|
3
|
+
"version": "2.0.11-rc.3",
|
|
4
4
|
"description": "Simple Photo Gallery CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vladimir Haltakov, Tomasz Rusin",
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
"simple-photo-gallery": "./dist/index.js",
|
|
17
17
|
"spg": "./dist/index.js"
|
|
18
18
|
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./lib": {
|
|
27
|
+
"types": "./dist/lib/index.d.ts",
|
|
28
|
+
"default": "./dist/lib/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
19
31
|
"scripts": {
|
|
20
32
|
"gallery": "SPG_TELEMETRY_PROVIDER=none tsx src/index.ts",
|
|
21
33
|
"clean": "rm -rf dist",
|
|
@@ -28,10 +40,11 @@
|
|
|
28
40
|
"format:fix": "prettier --write .",
|
|
29
41
|
"test": "jest",
|
|
30
42
|
"test:coverage": "jest --coverage",
|
|
31
|
-
"prepublish": "
|
|
43
|
+
"prepublish": "yarn build"
|
|
32
44
|
},
|
|
33
45
|
"dependencies": {
|
|
34
|
-
"@simple-photo-gallery/
|
|
46
|
+
"@simple-photo-gallery/common": "1.0.1",
|
|
47
|
+
"@simple-photo-gallery/theme-modern": "2.0.11-rc.3",
|
|
35
48
|
"axios": "^1.12.2",
|
|
36
49
|
"blurhash": "^2.0.5",
|
|
37
50
|
"commander": "^12.0.0",
|
|
@@ -46,7 +59,6 @@
|
|
|
46
59
|
"devDependencies": {
|
|
47
60
|
"@eslint/eslintrc": "^3.3.1",
|
|
48
61
|
"@eslint/js": "^9.30.1",
|
|
49
|
-
"@simple-photo-gallery/common": "1.0.0",
|
|
50
62
|
"@types/fs-extra": "^11.0.4",
|
|
51
63
|
"@types/jest": "^30.0.0",
|
|
52
64
|
"@types/node": "^24.0.10",
|