thumbkit
Version:
A comprehensive TypeScript library for generating thumbnails from images, PDFs, videos, office documents, and archives.
480 lines • 21.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThumbnailGenerator = void 0;
const sharp_1 = __importDefault(require("sharp"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const canvas_1 = require("canvas");
const fs = __importStar(require("fs/promises"));
const config_1 = require("./config");
class ThumbnailGenerator {
constructor(options = {}) {
this.supportedTypes = config_1.SUPPORTED_TYPES;
this.config = { ...config_1.DEFAULT_CONFIG, ...options };
}
/**
* Generates a standardized thumbnail name based on the original file name.
*
* @param originalFileName - The original file name from which to generate the thumbnail name.
* @returns The generated thumbnail file name with the configured suffix and format.
*/
generateThumbnailName(originalFileName) {
const ext = path.extname(originalFileName);
const nameWithoutExt = path.basename(originalFileName, ext);
const outputFormat = this.config.format === "jpeg" ? "jpg" : this.config.format;
return `${nameWithoutExt}${this.config.suffix}.${outputFormat}`;
}
/**
* Creates a ThumbnailResult object from a generated thumbnail buffer.
*
* @param thumbnailBuffer Thumbnail buffer
* @param originalFileName Original filename
* @param fileType File type (image, document, video, or office)
* @param metadata Additional metadata
* @returns ThumbnailResult object
*/
createThumbnailObject(thumbnailBuffer, originalFileName, fileType, metadata) {
const thumbnailFileName = this.generateThumbnailName(originalFileName);
const md5Hash = crypto
.createHash("md5")
.update(thumbnailBuffer)
.digest("hex");
return {
fileName: thumbnailFileName,
originalFileName,
fileBuffer: thumbnailBuffer,
fileSizeInBytes: thumbnailBuffer.length,
mimeType: `image/${this.config.format}`,
md5Hash,
fileType,
dimensions: {
width: this.config.width,
height: this.config.height,
},
quality: this.config.quality,
createdAt: new Date().toISOString(),
isThumbnail: true,
metadata: metadata ?? {},
};
}
/**
* Determines the file type category based on the file extension.
*
* @param fileName The name of the file with its extension.
* @returns The file type category as 'image', 'document', 'video', 'office', 'archive', or 'unsupported'.
*/
getFileType(fileName) {
const ext = path.extname(fileName).toLowerCase();
if (this.supportedTypes.images.includes(ext))
return "image";
if (this.supportedTypes.documents.includes(ext))
return "document";
if (this.supportedTypes.videos.includes(ext))
return "video";
if (this.supportedTypes.office.includes(ext))
return "office";
if (this.supportedTypes.archives.includes(ext))
return "archive";
return "unsupported";
}
/**
* Generates a thumbnail for an image file using sharp.
* @param fileBuffer The image file buffer.
* @param fileName The original file name.
* @returns A promise that resolves with a ThumbnailResult object.
* @throws An Error if the thumbnail generation fails.
*/
async generateImageThumbnail(fileBuffer, fileName) {
try {
const sharpInstance = (0, sharp_1.default)(fileBuffer);
const metadata = await sharpInstance.metadata();
const thumbnailBuffer = await sharpInstance
.resize(this.config.width, this.config.height, {
fit: this.config.fit,
withoutEnlargement: this.config.withoutEnlargement,
background: this.config.background,
position: this.config.position,
})
.toFormat(this.config.format, {
quality: this.config.quality,
})
.toBuffer();
return this.createThumbnailObject(thumbnailBuffer, fileName, "image", {
originalDimensions: {
width: metadata.width,
height: metadata.height,
},
format: metadata.format,
hasAlpha: metadata.hasAlpha,
channels: metadata.channels,
});
}
catch (error) {
throw new Error(`Image thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generates a thumbnail for a PDF file using PDF.js.
* @param fileBuffer The PDF file buffer.
* @param fileName The original file name.
* @returns A promise that resolves with a ThumbnailResult object containing the thumbnail image buffer and additional PDF metadata.
* @throws An Error if the thumbnail generation fails.
*/
async generatePDFThumbnail(fileBuffer, fileName) {
try {
// Dynamic import for PDF.js
const pdfjsLib = await Promise.resolve().then(() => __importStar(require("pdfjs-dist/legacy/build/pdf.mjs")));
const uint8Array = new Uint8Array(fileBuffer);
const loadingTask = pdfjsLib.getDocument({ data: uint8Array });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 2 }); // Higher scale for better quality
const canvas = (0, canvas_1.createCanvas)(viewport.width, viewport.height);
const context = canvas.getContext("2d");
await page.render({ canvasContext: context, viewport }).promise;
// Convert canvas to buffer and resize
const canvasBuffer = canvas.toBuffer("image/png");
const thumbnailBuffer = await (0, sharp_1.default)(canvasBuffer)
.resize(this.config.width, this.config.height, {
fit: this.config.fit,
withoutEnlargement: this.config.withoutEnlargement,
background: this.config.background,
position: this.config.position,
})
.toFormat(this.config.format, {
quality: this.config.quality,
})
.toBuffer();
return this.createThumbnailObject(thumbnailBuffer, fileName, "document", {
totalPages: pdf.numPages,
pdfVersion: pdf._pdfInfo?.PDFFormatVersion,
pageSize: {
width: viewport.width,
height: viewport.height,
},
});
}
catch (error) {
throw new Error(`PDF thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generate thumbnail for video files
* @param fileBuffer The video file buffer
* @param fileName The original file name
* @returns A promise that resolves with a ThumbnailResult object
* @throws An Error if the thumbnail generation fails
* @note Video thumbnails currently generate a placeholder thumbnail
* requiring ffmpeg integration for frame extraction
*/
async generateVideoThumbnail(fileBuffer, fileName) {
try {
// This would require ffmpeg integration
// For now, we'll create a placeholder thumbnail
const placeholderSvg = `
<svg width="${this.config.width}" height="${this.config.height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="14" fill="#666">
Video File
</text>
<polygon points="40,30 40,70 70,50" fill="#666"/>
</svg>
`;
const thumbnailBuffer = await (0, sharp_1.default)(Buffer.from(placeholderSvg))
.resize(this.config.width, this.config.height)
.toFormat(this.config.format, { quality: this.config.quality })
.toBuffer();
return this.createThumbnailObject(thumbnailBuffer, fileName, "video", {
isPlaceholder: true,
note: "Video thumbnails require ffmpeg integration for frame extraction",
});
}
catch (error) {
throw new Error(`Video thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generates a thumbnail for office documents.
*
* This method creates a placeholder thumbnail for office documents based on
* the file extension. It generates an SVG icon specific to the document type
* (e.g., DOC, XLS, PPT) and converts it to a thumbnail image.
*
* @param fileBuffer - The buffer of the office document file.
* @param fileName - The original file name of the office document.
* @returns A promise that resolves with a ThumbnailResult object containing
* the generated thumbnail image buffer and additional metadata.
* @throws An Error if the thumbnail generation fails.
*/
async generateOfficeThumbnail(fileBuffer, fileName) {
try {
const ext = path.extname(fileName).toLowerCase();
let iconText = "DOC";
let iconColor = "#2b579a";
if ([".xls", ".xlsx"].includes(ext)) {
iconText = "XLS";
iconColor = "#217346";
}
else if ([".ppt", ".pptx"].includes(ext)) {
iconText = "PPT";
iconColor = "#d24726";
}
const placeholderSvg = `
<svg width="${this.config.width}" height="${this.config.height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<rect x="20" y="20" width="${this.config.width - 40}" height="${this.config.height - 40}" fill="${iconColor}" rx="4"/>
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="16" fill="white" font-weight="bold">
${iconText}
</text>
</svg>
`;
const thumbnailBuffer = await (0, sharp_1.default)(Buffer.from(placeholderSvg))
.resize(this.config.width, this.config.height)
.toFormat(this.config.format, { quality: this.config.quality })
.toBuffer();
return this.createThumbnailObject(thumbnailBuffer, fileName, "office", {
fileExtension: ext,
isPlaceholder: true,
note: "Office document thumbnails show file type icons",
});
}
catch (error) {
throw new Error(`Office document thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generates a thumbnail for archive files.
*
* This method creates a placeholder thumbnail for archive files using an SVG representation
* that visually indicates the file is an archive. The SVG is converted to a thumbnail image
* using the specified configuration settings.
*
* @param fileBuffer - The buffer of the archive file.
* @param fileName - The original file name of the archive.
* @returns A promise that resolves with a ThumbnailResult object containing
* the generated thumbnail image buffer and additional metadata.
* @throws An Error if the thumbnail generation fails.
*/
async generateArchiveThumbnail(fileBuffer, fileName) {
try {
const placeholderSvg = `
<svg width="${this.config.width}" height="${this.config.height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<rect x="20" y="30" width="${this.config.width - 40}" height="${this.config.height - 50}" fill="#795548" rx="4"/>
<rect x="30" y="20" width="${this.config.width - 60}" height="20" fill="#8d6e63" rx="2"/>
<text x="50%" y="65%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="12" fill="white">
Archive
</text>
</svg>
`;
const thumbnailBuffer = await (0, sharp_1.default)(Buffer.from(placeholderSvg))
.resize(this.config.width, this.config.height)
.toFormat(this.config.format, { quality: this.config.quality })
.toBuffer();
return this.createThumbnailObject(thumbnailBuffer, fileName, "archive", {
fileExtension: path.extname(fileName).toLowerCase(),
isPlaceholder: true,
});
}
catch (error) {
throw new Error(`Archive thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generates a thumbnail for a given file or buffer.
*
* This method takes various types of input (Buffer, file path string, or FileInput object)
* and generates a thumbnail image based on the file type. It supports images, PDF documents,
* videos, office documents, and archives. The generated thumbnail is returned as a ThumbnailResult
* object containing the image buffer, file name, and additional metadata.
*
* @param input - The file or buffer to generate a thumbnail for.
* @param fileName? - The original file name, required when input is a Buffer.
* @param options? - Optional configuration overrides for thumbnail generation.
* @returns A promise that resolves with a ThumbnailResult object containing the generated thumbnail
* image buffer and additional metadata.
* @throws An Error if the thumbnail generation fails.
*/
async generate(input, fileName, options) {
try {
let fileBuffer;
let actualFileName;
// Handle different input types
if (Buffer.isBuffer(input)) {
if (!fileName) {
throw new Error("fileName is required when input is a Buffer");
}
fileBuffer = input;
actualFileName = fileName;
}
else if (typeof input === "string") {
// File path
fileBuffer = await fs.readFile(input);
actualFileName = fileName || path.basename(input);
}
else if (input &&
typeof input === "object" &&
"buffer" in input &&
"fileName" in input) {
// FileInput object
fileBuffer = input.buffer;
actualFileName = input.fileName;
}
else {
throw new Error("Invalid input type. Expected Buffer, file path string, or FileInput object");
}
// Temporarily override config if options provided
const originalConfig = { ...this.config };
if (options) {
this.config = { ...this.config, ...options };
}
const fileType = this.getFileType(actualFileName);
let result;
switch (fileType) {
case "image":
result = await this.generateImageThumbnail(fileBuffer, actualFileName);
break;
case "document":
result = await this.generatePDFThumbnail(fileBuffer, actualFileName);
break;
case "video":
result = await this.generateVideoThumbnail(fileBuffer, actualFileName);
break;
case "office":
result = await this.generateOfficeThumbnail(fileBuffer, actualFileName);
break;
case "archive":
result = await this.generateArchiveThumbnail(fileBuffer, actualFileName);
break;
default:
throw new Error(`Unsupported file type: ${path.extname(actualFileName)}`);
}
// Restore original config
this.config = originalConfig;
return result;
}
catch (error) {
throw new Error(`Thumbnail generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Generates thumbnails for multiple files in a single call.
* @param files An array of file paths or FileInput objects
* @param options Optional configuration object to override the default config
* @returns A promise that resolves with an object containing two arrays: results and errors.
* results contains ThumbnailResult objects for the successfully generated thumbnails.
* errors contains objects with index, fileName, and error properties for each failed thumbnail generation.
*/
async generateBatch(files, options) {
const results = [];
const errors = [];
for (let i = 0; i < files.length; i++) {
try {
const file = files[i];
let result;
if (typeof file === "string") {
result = await this.generate(file, undefined, options);
}
else if (file &&
typeof file === "object" &&
"buffer" in file &&
"fileName" in file) {
result = await this.generate(file, undefined, options);
}
else {
throw new Error("Invalid file format in batch");
}
results.push(result);
}
catch (error) {
const fileName = typeof files[i] === "string"
? files[i]
: files[i]?.fileName || "unknown";
errors.push({
index: i,
fileName: fileName,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
return { results, errors };
}
/**
* Checks if a given file type is supported by the generator.
*
* @param fileName - The name of the file, including its extension.
* @returns A boolean indicating whether the file type is supported.
*/
isSupported(fileName) {
return this.getFileType(fileName) !== "unsupported";
}
/**
* Get list of all supported file extensions
*/
getSupportedTypes() {
return { ...this.supportedTypes };
}
/**
* Get list of supported extensions as flat array
*/
getSupportedExtensions() {
return Object.values(this.supportedTypes).flat();
}
/**
* Update the configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Reset configuration to defaults
*/
resetConfig() {
this.config = { ...config_1.DEFAULT_CONFIG };
}
}
exports.ThumbnailGenerator = ThumbnailGenerator;
exports.default = ThumbnailGenerator;
//# sourceMappingURL=index.js.map