UNPKG

typography-canvas-renderer

Version:

A lightweight npm package for rendering typographic content (text and images) on HTML5 Canvas with full CSS styling support including borders, border-radius, multiple border styles, inline text rendering, auto height calculation, and image support

932 lines (924 loc) 34.2 kB
/** * Custom error class for invalid input data */ class InvalidInputError extends Error { constructor(message, field) { super(message); this.field = field; this.name = 'InvalidInputError'; } } /** * Custom error class for CSS validation errors */ class CssValidationError extends Error { constructor(message, property, value, details) { super(message); this.property = property; this.value = value; this.details = details; this.name = 'CssValidationError'; } } /** * Custom error class for image loading errors */ class ImageLoadError extends Error { constructor(message, src) { super(message); this.src = src; this.name = 'ImageLoadError'; } } /** * Validates the input object structure and values */ const validateInput = (input) => { if (!input || typeof input !== 'object') { throw new InvalidInputError('Input must be an object'); } const inputObj = input; // Validate canvas property if (!inputObj.canvas || typeof inputObj.canvas !== 'object') { throw new InvalidInputError('Input must contain a canvas property'); } const canvas = inputObj.canvas; // Validate required canvas properties if (typeof canvas.width !== 'number' || canvas.width < 1 || canvas.width > 3000) { throw new InvalidInputError('Canvas width must be a number between 1 and 3000', 'canvas.width'); } if (typeof canvas.height !== 'number' || canvas.height < 1 || canvas.height > 4244) { throw new InvalidInputError('Canvas height must be a number between 1 and 4244', 'canvas.height'); } // Validate optional canvas properties if (canvas.background !== undefined && typeof canvas.background !== 'string') { throw new InvalidInputError('Canvas background must be a string', 'canvas.background'); } if (canvas.format !== undefined && !['png', 'jpeg'].includes(canvas.format)) { throw new InvalidInputError('Canvas format must be "png" or "jpeg"', 'canvas.format'); } if (canvas.quality !== undefined) { if (typeof canvas.quality !== 'number' || canvas.quality < 0 || canvas.quality > 1) { throw new InvalidInputError('Canvas quality must be a number between 0 and 1', 'canvas.quality'); } } if (canvas.scaleFactor !== undefined) { if (typeof canvas.scaleFactor !== 'number' || canvas.scaleFactor <= 0) { throw new InvalidInputError('Canvas scaleFactor must be a positive number', 'canvas.scaleFactor'); } } // Validate texts array if (!Array.isArray(inputObj.texts)) { throw new InvalidInputError('Texts must be an array'); } for (let i = 0; i < inputObj.texts.length; i++) { const text = inputObj.texts[i]; if (!text || typeof text !== 'object') { throw new InvalidInputError(`Text element at index ${i} must be an object`); } const textObj = text; if (typeof textObj.text !== 'string') { throw new InvalidInputError(`Text element at index ${i} must have a text property`); } if (!textObj.css || typeof textObj.css !== 'object') { throw new InvalidInputError(`Text element at index ${i} must have a css property`); } } // Validate images array if (!Array.isArray(inputObj.images)) { throw new InvalidInputError('Images must be an array'); } for (let i = 0; i < inputObj.images.length; i++) { const image = inputObj.images[i]; if (!image || typeof image !== 'object') { throw new InvalidInputError(`Image element at index ${i} must be an object`); } const imageObj = image; if (typeof imageObj.src !== 'string') { throw new InvalidInputError(`Image element at index ${i} must have a src property`); } if (!imageObj.css || typeof imageObj.css !== 'object') { throw new InvalidInputError(`Image element at index ${i} must have a css property`); } } return input; }; /** * Supported CSS properties for the canvas renderer */ const SUPPORTED_PROPERTIES = new Set([ 'position', 'left', 'top', 'width', 'height', 'font-size', 'font-family', 'color', 'background-color', 'opacity', 'text-align', 'vertical-align', 'line-height', 'padding', 'border', 'border-radius', 'z-index' ]); /** * Validates CSS properties and returns parsed values */ const validateAndParseCSS = (css, _elementType) => { const parsed = { zIndex: 0 }; for (const [property, value] of Object.entries(css)) { // Check if property is supported if (!SUPPORTED_PROPERTIES.has(property)) { console.warn(`Unsupported CSS property '${property}' will be ignored`); continue; } // Skip CSS validation for now - csstree-validator is too strict // TODO: Implement custom validation or use a more lenient validator // Parse and validate specific properties try { switch (property) { case 'position': if (value !== 'absolute') { console.warn(`Only 'position: absolute' is supported, ignoring 'position: ${value}'`); } else { parsed.position = 'absolute'; } break; case 'left': case 'top': case 'width': case 'height': case 'font-size': case 'line-height': case 'border-radius': const numValue = parsePixelValue(value); if (numValue < 0) { throw new CssValidationError(`Negative values not allowed for '${property}': ${value}`, property, value); } if (property === 'left') parsed.left = numValue; else if (property === 'top') parsed.top = numValue; else if (property === 'width') parsed.width = numValue; else if (property === 'height') parsed.height = numValue; else if (property === 'font-size') parsed.fontSize = numValue; else if (property === 'line-height') parsed.lineHeight = numValue; else if (property === 'border-radius') parsed.borderRadius = numValue; break; case 'font-family': parsed.fontFamily = value; break; case 'color': case 'background-color': if (!isValidColor(value)) { throw new CssValidationError(`Invalid color value for '${property}': ${value}`, property, value); } if (property === 'color') parsed.color = value; else parsed.backgroundColor = value; break; case 'opacity': const opacity = parseFloat(value); if (isNaN(opacity) || opacity < 0 || opacity > 1) { throw new CssValidationError(`Invalid opacity value: ${value}. Must be between 0 and 1`, property, value); } parsed.opacity = opacity; break; case 'text-align': if (!['left', 'center', 'right'].includes(value)) { throw new CssValidationError(`Invalid text-align value: ${value}. Must be 'left', 'center', or 'right'`, property, value); } parsed.textAlign = value; break; case 'vertical-align': if (!['top', 'middle', 'center', 'bottom'].includes(value)) { throw new CssValidationError(`Invalid vertical-align value: ${value}. Must be 'top', 'middle', 'center', or 'bottom'`, property, value); } parsed.verticalAlign = value; break; case 'padding': const padding = parseInt(value, 10); if (isNaN(padding) || padding < 0) { throw new CssValidationError(`Invalid padding value: ${value}. Must be a non-negative number`, property, value); } parsed.padding = padding; break; case 'border': parsed.border = parseBorder(value); break; case 'z-index': const zIndex = parseInt(value, 10); if (isNaN(zIndex)) { throw new CssValidationError(`Invalid z-index value: ${value}. Must be a number`, property, value); } parsed.zIndex = zIndex; break; } } catch (error) { if (error instanceof CssValidationError) { throw error; } throw new CssValidationError(`Error parsing CSS property '${property}': ${error instanceof Error ? error.message : 'Unknown error'}`, property, value); } } return parsed; }; /** * Parses pixel values from CSS strings */ const parsePixelValue = (value) => { const match = value.match(/^(-?\d+(?:\.\d+)?)px$/); if (!match) { throw new CssValidationError(`Invalid pixel value: ${value}. Must be a number followed by 'px'`, 'pixel-value', value); } return parseFloat(match[1]); }; /** * Validates color values */ const isValidColor = (color) => { // Basic color validation - can be extended for more complex color formats const colorRegex = /^(#[0-9a-fA-F]{3,6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)|hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)|hsla\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*[\d.]+\s*\)|[a-zA-Z]+)$/; return colorRegex.test(color); }; /** * Parses border CSS value */ const parseBorder = (value) => { const parts = value.trim().split(/\s+/); if (parts.length < 3) { throw new CssValidationError(`Invalid border value: ${value}. Must be in format 'width style color'`, 'border', value); } const width = parsePixelValue(parts[0]); const style = parts[1]; const color = parts.slice(2).join(' '); if (!['solid', 'dashed', 'dotted', 'double'].includes(style)) { throw new CssValidationError(`Unsupported border style: ${style}. Supported styles: solid, dashed, dotted, double`, 'border', value); } if (!isValidColor(color)) { throw new CssValidationError(`Invalid border color: ${color}`, 'border', value); } return { width, style, color }; }; /** * Image cache to store loaded images during rendering */ const imageCache = new Map(); /** * Loads an image from URL or base64 string */ const loadImage = (src) => { return new Promise((resolve, reject) => { // Check cache first const cachedImage = imageCache.get(src); if (cachedImage) { resolve({ element: cachedImage, src }); return; } const img = new Image(); img.onload = () => { // Cache the loaded image imageCache.set(src, img); resolve({ element: img, src }); }; img.onerror = () => { const errorMessage = `Failed to load image: ${src}`; reject(new ImageLoadError(errorMessage, src)); }; // Set crossOrigin for external URLs to avoid CORS issues if (isExternalUrl(src)) { img.crossOrigin = 'anonymous'; } img.src = src; }); }; /** * Loads multiple images concurrently */ const loadImages = async (srcs) => { const uniqueSrcs = [...new Set(srcs)]; // Remove duplicates const loadPromises = uniqueSrcs.map(src => loadImage(src)); try { return await Promise.all(loadPromises); } catch (error) { throw new ImageLoadError(`Failed to load one or more images: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Clears the image cache */ const clearImageCache = () => { imageCache.clear(); }; /** * Checks if a URL is external (not data URI) */ const isExternalUrl = (src) => { return !src.startsWith('data:') && !src.startsWith('blob:'); }; /** * Validates if a string is a valid base64 image */ const isValidBase64Image = (src) => { const base64Regex = /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,/; return base64Regex.test(src); }; /** * Validates if a string is a valid image URL */ const isValidImageUrl = (src) => { try { const url = new URL(src); const validProtocols = ['http:', 'https:', 'blob:']; return validProtocols.includes(url.protocol); } catch { return false; } }; /** * Validates image source format */ const validateImageSrc = (src) => { if (!src || typeof src !== 'string') { throw new ImageLoadError('Image source must be a non-empty string'); } if (!isValidBase64Image(src) && !isValidImageUrl(src)) { throw new ImageLoadError(`Invalid image source format: ${src}. Must be a valid URL or base64 data URI`, src); } }; /** * Processes and sorts elements by z-index for rendering */ const processElements = (input) => { const elements = []; // Process text elements for (const textElement of input.texts) { const parsedCSS = validateAndParseCSS(textElement.css); elements.push({ type: 'text', content: textElement.text, css: textElement.css, zIndex: parsedCSS.zIndex }); } // Process image elements for (const imageElement of input.images) { validateImageSrc(imageElement.src); const parsedCSS = validateAndParseCSS(imageElement.css); elements.push({ type: 'image', content: imageElement.src, css: imageElement.css, zIndex: parsedCSS.zIndex }); } // Sort by z-index (ascending - lower layers first) return elements.sort((a, b) => a.zIndex - b.zIndex); }; /** * Determines if scaling should be applied based on canvas dimensions */ const shouldApplyScaling = (width, height) => { return width > 2000 || height > 2000; }; /** * Gets the appropriate scale factor for the canvas */ const getScaleFactor = (input) => { const { width, height, scaleFactor = 1 } = input.canvas; if (shouldApplyScaling(width, height)) { return scaleFactor; } return 1; }; /** * Renders elements on a canvas context */ const renderElements = async (ctx, elements, scaleFactor) => { // Load all images first const imageSrcs = elements .filter(el => el.type === 'image') .map(el => el.content); const loadedImages = await loadImages(imageSrcs); const imageMap = new Map(loadedImages.map(img => [img.src, img.element])); // Render each element for (const element of elements) { const scaledCSS = applyScalingToCSS(element.css, scaleFactor); const parsedCSS = validateAndParseCSS(scaledCSS, element.type); if (element.type === 'text') { await renderText(ctx, element.content, parsedCSS); } else if (element.type === 'image') { const image = imageMap.get(element.content); if (image) { await renderImage(ctx, image, parsedCSS); } } } }; /** * Wraps text to fit within specified width */ const wrapText = (ctx, text, maxWidth) => { const words = text.split(' '); const lines = []; let currentLine = ''; for (let i = 0; i < words.length; i++) { const word = words[i]; const testLine = currentLine + (currentLine ? ' ' : '') + word; const testWidth = ctx.measureText(testLine).width; if (testWidth > maxWidth && currentLine) { // Current line is too long, start a new line lines.push(currentLine); currentLine = word; } else { // Add word to current line currentLine = testLine; } } // Add the last line if it's not empty if (currentLine) { lines.push(currentLine); } return lines; }; /** * Renders text on canvas */ const renderText = async (ctx, text, css) => { if (!css.left || !css.top) { console.warn('Text element missing required position properties (left, top)'); return; } // Set font properties const fontSize = css.fontSize || 16; const fontFamily = css.fontFamily || 'Arial, sans-serif'; ctx.font = `${fontSize}px ${fontFamily}`; ctx.fillStyle = css.color || '#000000'; ctx.textAlign = css.textAlign || 'left'; ctx.textBaseline = 'top'; // Save canvas state for opacity ctx.save(); // Set opacity if (css.opacity !== undefined) { ctx.globalAlpha = css.opacity; } // Check if width is set to determine rendering mode const hasWidth = css.width !== undefined; if (!hasWidth) { // Inline text rendering - no width constraint // Calculate padding for inline text (considering border width) const borderWidth = css.border ? css.border.width : 0; const customPadding = css.padding; const padding = customPadding !== undefined ? customPadding : Math.max(borderWidth, 2); // Use custom padding or border width, minimum 2px // Handle multiline text for inline rendering const lineHeight = css.lineHeight || fontSize * 1.2; const lines = text.split('\n'); // Calculate dimensions for all lines let maxWidth = 0; lines.forEach(line => { const lineWidth = ctx.measureText(line).width; maxWidth = Math.max(maxWidth, lineWidth); }); const textWidth = maxWidth; // Calculate actual text height more accurately // For single line, use fontSize; for multiple lines, use (lines.length - 1) * lineHeight + fontSize const textHeight = lines.length === 1 ? fontSize : (lines.length - 1) * lineHeight + fontSize; // Calculate total dimensions including padding const totalWidth = textWidth + (padding * 2); const totalHeight = textHeight + (padding * 2); // Position text inside the padded area let textX = css.left + padding; const textY = css.top + padding; // Adjust textX for center alignment if (css.textAlign === 'center') { textX = css.left + totalWidth / 2; } else if (css.textAlign === 'right') { textX = css.left + totalWidth - padding; } // Draw background if specified (for inline text, use total dimensions) if (css.backgroundColor) { ctx.fillStyle = css.backgroundColor; if (css.borderRadius && css.borderRadius > 0) { // Draw rounded background drawRoundedRect(ctx, css.left, css.top, totalWidth, totalHeight, css.borderRadius, true); } else { ctx.fillRect(css.left, css.top, totalWidth, totalHeight); } } // Draw text - render each line ctx.fillStyle = css.color || '#000000'; lines.forEach((line, index) => { const lineY = textY + (index * lineHeight); ctx.fillText(line, textX, lineY); }); // Draw border if specified (for inline text, use total dimensions) if (css.border) { drawBorder(ctx, css.left, css.top, totalWidth, totalHeight, css.border, css.borderRadius); } } else { // Block text rendering - width constraint applied const lineHeight = css.lineHeight || fontSize * 1.2; // Calculate available width for text (considering padding) const borderWidth = css.border ? css.border.width : 0; const customPadding = css.padding; const padding = customPadding !== undefined ? customPadding : Math.max(borderWidth, 4); // Use custom padding or minimum 4px const availableWidth = css.width - (padding * 2); // Process text: split by \n and wrap long lines const allLines = []; const manualLines = text.split('\n'); manualLines.forEach(line => { if (ctx.measureText(line).width > availableWidth) { // Wrap this line to fit within available width const wrappedLines = wrapText(ctx, line, availableWidth); allLines.push(...wrappedLines); } else { allLines.push(line); } }); // Calculate dimensions for all lines let maxWidth = 0; allLines.forEach(line => { const lineWidth = ctx.measureText(line).width; maxWidth = Math.max(maxWidth, lineWidth); }); const measuredWidth = maxWidth; // Calculate actual text height based on the final wrapped lines count // Use the actual line height for each line (this accounts for the increased line count due to padding) const measuredHeight = allLines.length * lineHeight; // Use specified dimensions if available, otherwise use measured text dimensions const elementWidth = css.width || measuredWidth; const elementHeight = css.height || (measuredHeight + (padding * 2)); // Calculate text position with padding and vertical alignment const textX = css.left + padding; // Handle vertical alignment let textY = css.top + padding; // Default to top alignment if (css.verticalAlign) { const totalTextHeight = allLines.length * lineHeight; // Use the same calculation as measuredHeight const availableHeight = elementHeight - (padding * 2); if (css.verticalAlign === 'middle' || css.verticalAlign === 'center') { textY = css.top + padding + (availableHeight - totalTextHeight) / 2; } else if (css.verticalAlign === 'bottom') { textY = css.top + elementHeight - padding - totalTextHeight; } } // Draw background if specified if (css.backgroundColor) { ctx.fillStyle = css.backgroundColor; if (css.borderRadius && css.borderRadius > 0) { // Draw rounded background drawRoundedRect(ctx, css.left, css.top, elementWidth, elementHeight, css.borderRadius, true); } else { ctx.fillRect(css.left, css.top, elementWidth, elementHeight); } } // Draw text with proper positioning ctx.fillStyle = css.color || '#000000'; // Render each line allLines.forEach((line, index) => { const lineY = textY + (index * lineHeight); // Handle text alignment within the element if (css.textAlign === 'center') { ctx.textAlign = 'center'; ctx.fillText(line, (css.left || 0) + elementWidth / 2, lineY); } else if (css.textAlign === 'right') { ctx.textAlign = 'right'; ctx.fillText(line, (css.left || 0) + elementWidth - padding, lineY); } else { ctx.textAlign = 'left'; ctx.fillText(line, textX, lineY); } }); // Draw border if specified if (css.border) { drawBorder(ctx, css.left, css.top, elementWidth, elementHeight, css.border, css.borderRadius); } } // Restore canvas state (resets opacity) ctx.restore(); }; /** * Renders image on canvas */ const renderImage = async (ctx, image, css) => { if (!css.left || !css.top) { console.warn('Image element missing required position properties (left, top)'); return; } const width = css.width || image.naturalWidth; const height = css.height || image.naturalHeight; // Save canvas state for opacity ctx.save(); // Set opacity if (css.opacity !== undefined) { ctx.globalAlpha = css.opacity; } // Draw background if specified if (css.backgroundColor) { ctx.fillStyle = css.backgroundColor; if (css.borderRadius && css.borderRadius > 0) { // Draw rounded background drawRoundedRect(ctx, css.left, css.top, width, height, css.borderRadius, true); } else { ctx.fillRect(css.left, css.top, width, height); } } // Draw image if (css.borderRadius) { // Draw image with rounded corners drawRoundedImage(ctx, image, css.left, css.top, width, height, css.borderRadius); } else { ctx.drawImage(image, css.left, css.top, width, height); } // Draw border if specified if (css.border) { drawBorder(ctx, css.left, css.top, width, height, css.border, css.borderRadius); } // Restore canvas state (resets opacity) ctx.restore(); }; /** * Draws a border around an element */ const drawBorder = (ctx, x, y, width, height, border, borderRadius) => { ctx.strokeStyle = border.color; ctx.lineWidth = border.width; // Handle double border specially if (border.style === 'double') { const outerWidth = border.width; const innerWidth = Math.max(1, Math.floor(outerWidth / 3)); const gap = Math.max(1, Math.floor(outerWidth / 3)); // Draw outer border ctx.lineWidth = outerWidth; ctx.setLineDash([]); if (borderRadius && borderRadius > 0) { drawRoundedRect(ctx, x, y, width, height, borderRadius, false); } else { ctx.strokeRect(x, y, width, height); } // Draw inner border ctx.lineWidth = innerWidth; const innerX = x + gap; const innerY = y + gap; const innerWidthRect = width - 2 * gap; const innerHeightRect = height - 2 * gap; const innerRadius = Math.max(0, (borderRadius || 0) - gap); if (innerRadius > 0) { drawRoundedRect(ctx, innerX, innerY, innerWidthRect, innerHeightRect, innerRadius, false); } else { ctx.strokeRect(innerX, innerY, innerWidthRect, innerHeightRect); } } else { // Handle other border styles ctx.setLineDash(getLineDash(border.style)); if (borderRadius && borderRadius > 0) { // Draw rounded rectangle border drawRoundedRect(ctx, x, y, width, height, borderRadius, false); } else { // Draw regular rectangle border ctx.strokeRect(x, y, width, height); } ctx.setLineDash([]); } }; /** * Draws a rounded rectangle */ const drawRoundedRect = (ctx, x, y, width, height, radius, fill) => { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); if (fill) { ctx.fill(); } else { ctx.stroke(); } }; /** * Draws an image with rounded corners */ const drawRoundedImage = (ctx, image, x, y, width, height, radius) => { ctx.save(); // Create clipping path first ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); ctx.clip(); // Draw the image ctx.drawImage(image, x, y, width, height); ctx.restore(); }; /** * Gets line dash pattern for border styles */ const getLineDash = (style) => { switch (style) { case 'dashed': return [5, 5]; case 'dotted': return [2, 2]; case 'double': return [0]; // Double border would need special handling default: return []; // solid } }; /** * Applies scaling to CSS properties */ const applyScalingToCSS = (css, scaleFactor) => { if (scaleFactor === 1) { return css; } const scaledCss = { ...css }; const propertiesToScale = [ 'left', 'top', 'width', 'height', 'font-size', 'border-width', 'border-radius' ]; for (const property of propertiesToScale) { if (scaledCss[property]) { const value = scaledCss[property]; const pixelMatch = value.match(/^(-?\d+(?:\.\d+)?)px$/); if (pixelMatch) { const numericValue = parseFloat(pixelMatch[1]); const scaledValue = numericValue * scaleFactor; scaledCss[property] = `${scaledValue}px`; } } } // Handle border property scaling if (scaledCss.border) { scaledCss.border = scaleBorderProperty(scaledCss.border, scaleFactor); } return scaledCss; }; /** * Scales border property values */ const scaleBorderProperty = (borderValue, scaleFactor) => { const parts = borderValue.trim().split(/\s+/); if (parts.length >= 1) { const widthMatch = parts[0].match(/^(-?\d+(?:\.\d+)?)px$/); if (widthMatch) { const numericWidth = parseFloat(widthMatch[1]); const scaledWidth = numericWidth * scaleFactor; parts[0] = `${scaledWidth}px`; } } return parts.join(' '); }; // Re-export types for convenience /** * Main function to render typography content on canvas * @param input - Input object containing canvas configuration and elements * @param canvasContext - Optional existing canvas context to use * @returns Promise that resolves to a Blob containing the rendered image */ const renderCanvas = async (input, canvasContext) => { // Validate input const validatedInput = validateInput(input); // Get scale factor const scaleFactor = getScaleFactor(validatedInput); // Process elements (validate CSS and sort by z-index) const elements = processElements(validatedInput); // Create or use provided canvas let canvas; let ctx; if (canvasContext) { canvas = canvasContext.canvas; ctx = canvasContext; } else { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); } // Set canvas dimensions canvas.width = validatedInput.canvas.width; canvas.height = validatedInput.canvas.height; // Clear canvas and set background ctx.clearRect(0, 0, canvas.width, canvas.height); const backgroundColor = validatedInput.canvas.background || 'white'; ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); // Render elements await renderElements(ctx, elements, scaleFactor); // Export as Blob const format = validatedInput.canvas.format || 'png'; const quality = validatedInput.canvas.quality || 0.9; return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Failed to create blob from canvas')); } }, `image/${format}`, format === 'jpeg' ? quality : undefined); }); }; /** * Renders canvas and returns data URL instead of Blob * @param input - Input object containing canvas configuration and elements * @param canvasContext - Optional existing canvas context to use * @returns Promise that resolves to a data URL string */ const renderCanvasAsDataURL = async (input, canvasContext) => { // Validate input const validatedInput = validateInput(input); // Get scale factor const scaleFactor = getScaleFactor(validatedInput); // Process elements (validate CSS and sort by z-index) const elements = processElements(validatedInput); // Create or use provided canvas let canvas; let ctx; if (canvasContext) { canvas = canvasContext.canvas; ctx = canvasContext; } else { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); } // Set canvas dimensions canvas.width = validatedInput.canvas.width; canvas.height = validatedInput.canvas.height; // Clear canvas and set background ctx.clearRect(0, 0, canvas.width, canvas.height); const backgroundColor = validatedInput.canvas.background || 'white'; ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvas.width, canvas.height); // Render elements await renderElements(ctx, elements, scaleFactor); // Export as data URL const format = validatedInput.canvas.format || 'png'; const quality = validatedInput.canvas.quality || 0.9; return canvas.toDataURL(`image/${format}`, format === 'jpeg' ? quality : undefined); }; /** * Clears the internal image cache */ const clearCache = () => { clearImageCache(); }; export { CssValidationError, ImageLoadError, InvalidInputError, clearCache, renderCanvas, renderCanvasAsDataURL }; //# sourceMappingURL=index.esm.js.map