UNPKG

thumbkit

Version:

A comprehensive TypeScript library for generating thumbnails from images, PDFs, videos, office documents, and archives.

480 lines 21.1 kB
"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