UNPKG

@nanggo/social-preview

Version:

Generate beautiful social media preview images from any URL

323 lines (317 loc) 12.8 kB
"use strict"; /** * Image Generator Module * Handles image processing and generation using Sharp */ 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_DIMENSIONS = void 0; exports.generateImage = generateImage; exports.createBlankCanvas = createBlankCanvas; exports.createFallbackImage = createFallbackImage; const image_security_1 = require("../utils/image-security"); const types_1 = require("../types"); const utils_1 = require("../utils"); const sharp_cache_1 = require("../utils/sharp-cache"); const fonts_1 = require("../constants/fonts"); // Pre-load image security module at module level for performance const imageSecurityPromise = Promise.resolve().then(() => __importStar(require('../utils/image-security'))); const metadata_extractor_1 = require("./metadata-extractor"); const validators_1 = require("../utils/validators"); /** * Default dimensions for social media preview images */ exports.DEFAULT_DIMENSIONS = { width: 1200, height: 630, }; /** * Generate image buffer from metadata and template */ async function generateImage(metadata, template, options = {}) { try { const width = options.width || exports.DEFAULT_DIMENSIONS.width; const height = options.height || exports.DEFAULT_DIMENSIONS.height; const quality = options.quality || 90; // Create base image or use existing image let baseImage; if (metadata.image) { // Use existing image as background const imageBuffer = await (0, metadata_extractor_1.fetchImage)(metadata.image); baseImage = await processBackgroundImage(imageBuffer, width, height, template); } else { // Create blank canvas with gradient background or transparent canvas based on template settings if (template.imageProcessing?.requiresTransparentCanvas) { baseImage = (0, validators_1.createTransparentCanvas)(width, height); } else { baseImage = await createBlankCanvas(width, height, options); } } // Generate text overlay SVG const overlayBuffer = await generateTextOverlay(metadata, template, width, height, options); // Composite text overlay on base image const finalImage = await baseImage .composite([ { input: overlayBuffer, top: 0, left: 0, }, ]) .jpeg({ quality, progressive: true, mozjpeg: true }) .toBuffer(); return finalImage; } catch (error) { if (error instanceof types_1.PreviewGeneratorError) { throw error; } throw new types_1.PreviewGeneratorError(types_1.ErrorType.IMAGE_ERROR, 'Failed to generate image', error); } } /** * Process background image to fit dimensions with template-specific processing */ async function processBackgroundImage(imageBuffer, width, height, template) { try { // Use withSecureSharp for automatic pool management const { withSecureSharp } = await imageSecurityPromise; return await withSecureSharp(imageBuffer, async (image) => { await image.metadata(); // Apply template-specific image processing settings const imageProcessing = template.imageProcessing || {}; // Use secure resize function let processedImage = (0, image_security_1.secureResize)(image, width, height, { fit: 'cover', position: 'center', }); // Apply template-specific blur if specified const blurRadius = imageProcessing.blur || template.effects?.blur?.radius || 2; if (blurRadius > 0) { processedImage = processedImage.blur(blurRadius); } // Apply template-specific brightness and saturation together for efficiency const brightness = imageProcessing.brightness !== undefined ? imageProcessing.brightness : 0.7; const saturation = imageProcessing.saturation; // Apply modulation only once with all necessary changes if (brightness !== 1 || saturation !== undefined) { const modulateOptions = {}; if (brightness !== 1) { modulateOptions.brightness = brightness; } if (saturation !== undefined) { modulateOptions.saturation = saturation; } processedImage = processedImage.modulate(modulateOptions); } return processedImage; }); } catch (error) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.IMAGE_ERROR, 'Failed to process background image', error); } } /** * Create blank canvas with gradient background * Uses caching for better performance with repeated requests */ async function createBlankCanvas(width, height, options) { // Validate colors before using them in SVG const backgroundColor = (0, validators_1.validateColor)(options.colors?.background || '#1a1a2e'); const accentColor = (0, validators_1.validateColor)(options.colors?.accent || '#16213e'); // Use cached canvas creation for performance return (0, sharp_cache_1.createCachedCanvas)(width, height, { colors: { background: backgroundColor, accent: accentColor } }); } /** * Generate text overlay SVG */ async function generateTextOverlay(metadata, template, width, height, options) { const padding = template.layout.padding || 60; const textColor = (0, validators_1.validateColor)(options.colors?.text || '#ffffff'); // Calculate text dimensions const maxTitleWidth = width - padding * 2; const maxDescWidth = width - padding * 2; // Typography settings const titleFontSize = template.typography.title.fontSize || 48; const titleLineHeight = template.typography.title.lineHeight || 1.2; const descFontSize = template.typography.description?.fontSize || 24; const descLineHeight = template.typography.description?.lineHeight || 1.4; const siteNameFontSize = template.typography.siteName?.fontSize || 20; // Truncate and wrap text const titleLines = (0, utils_1.wrapText)(metadata.title, maxTitleWidth, titleFontSize, template.typography.title.maxLines || 2); const descLines = metadata.description ? (0, utils_1.wrapText)(metadata.description, maxDescWidth, descFontSize, template.typography.description?.maxLines || 2) : []; // Calculate positions const titleY = calculateTitlePosition(height, padding, titleLines.length, titleFontSize, titleLineHeight, template.layout.titlePosition); const descY = titleY + titleLines.length * titleFontSize * titleLineHeight + 20; const siteNameY = height - padding - 10; // Create overlay SVG const overlaySvg = ` <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> <defs> <style> .title { font-family: ${fonts_1.SYSTEM_FONT_STACK}; font-size: ${titleFontSize}px; font-weight: 700; fill: ${textColor}; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); } .description { font-family: ${fonts_1.SYSTEM_FONT_STACK}; font-size: ${descFontSize}px; font-weight: 400; fill: ${textColor}; opacity: 0.9; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } .siteName { font-family: ${fonts_1.SYSTEM_FONT_STACK}; font-size: ${siteNameFontSize}px; font-weight: 600; fill: ${textColor}; opacity: 0.8; } </style> <linearGradient id="overlayGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" style="stop-color:rgba(0,0,0,0.6);stop-opacity:1" /> <stop offset="100%" style="stop-color:rgba(0,0,0,0.2);stop-opacity:1" /> </linearGradient> </defs> <!-- Semi-transparent overlay for better text readability --> ${metadata.image ? `<rect width="${width}" height="${height}" fill="url(#overlayGradient)"/>` : ''} <!-- Title --> ${titleLines .map((line, index) => ` <text x="${padding}" y="${titleY + index * titleFontSize * titleLineHeight}" class="title"> ${(0, utils_1.escapeXml)(line)} </text> `) .join('')} <!-- Description --> ${descLines .map((line, index) => ` <text x="${padding}" y="${descY + index * descFontSize * descLineHeight}" class="description"> ${(0, utils_1.escapeXml)(line)} </text> `) .join('')} <!-- Site name / Domain --> ${metadata.siteName ? ` <text x="${padding}" y="${siteNameY}" class="siteName"> ${(0, utils_1.escapeXml)(metadata.siteName.toUpperCase())} </text> ` : ''} <!-- Decorative elements --> <rect x="${padding}" y="${titleY - titleFontSize - 10}" width="60" height="4" fill="${(0, validators_1.validateColor)(options.colors?.accent || '#4a9eff')}" rx="2"/> </svg> `; // Use cached SVG creation for better performance const cachedSVG = await (0, sharp_cache_1.createCachedSVG)(overlaySvg); return cachedSVG.toBuffer(); } /** * Calculate title position based on layout configuration */ function calculateTitlePosition(height, padding, lineCount, fontSize, lineHeight, position) { const totalTextHeight = lineCount * fontSize * lineHeight; switch (position) { case 'top': return padding + fontSize; case 'bottom': return height - padding - totalTextHeight; case 'left': case 'right': case 'center': default: return (height - totalTextHeight) / 2 + fontSize; } } /** * Create fallback image when no metadata is available */ async function createFallbackImage(url, options = {}) { try { const urlObj = new URL(url); const domain = urlObj.hostname; // Create simple fallback metadata const fallbackMetadata = { title: options.fallback?.text || domain, description: `Visit ${domain} for more information`, url, domain, siteName: domain.replace('www.', ''), }; // Use a simple template for fallback const fallbackTemplate = { name: 'fallback', layout: { padding: 60, titlePosition: 'center', }, typography: { title: { fontSize: 42, fontWeight: '600', lineHeight: 1.3, maxLines: 2, }, description: { fontSize: 22, lineHeight: 1.4, maxLines: 1, }, }, }; return await generateImage(fallbackMetadata, fallbackTemplate, options); } catch (error) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.IMAGE_ERROR, 'Failed to create fallback image', error); } }