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
JavaScript
/**
* 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