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

3 lines (2 loc) 12.3 kB
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TypographyCanvasRenderer={})}(this,function(e){"use strict";class t extends Error{constructor(e,t){super(e),this.field=t,this.name="InvalidInputError"}}class o extends Error{constructor(e,t,o,r){super(e),this.property=t,this.value=o,this.details=r,this.name="CssValidationError"}}class r extends Error{constructor(e,t){super(e),this.src=t,this.name="ImageLoadError"}}const a=e=>{if(!e||"object"!=typeof e)throw new t("Input must be an object");const o=e;if(!o.canvas||"object"!=typeof o.canvas)throw new t("Input must contain a canvas property");const r=o.canvas;if("number"!=typeof r.width||r.width<1||r.width>3e3)throw new t("Canvas width must be a number between 1 and 3000","canvas.width");if("number"!=typeof r.height||r.height<1||r.height>4244)throw new t("Canvas height must be a number between 1 and 4244","canvas.height");if(void 0!==r.background&&"string"!=typeof r.background)throw new t("Canvas background must be a string","canvas.background");if(void 0!==r.format&&!["png","jpeg"].includes(r.format))throw new t('Canvas format must be "png" or "jpeg"',"canvas.format");if(void 0!==r.quality&&("number"!=typeof r.quality||r.quality<0||r.quality>1))throw new t("Canvas quality must be a number between 0 and 1","canvas.quality");if(void 0!==r.scaleFactor&&("number"!=typeof r.scaleFactor||r.scaleFactor<=0))throw new t("Canvas scaleFactor must be a positive number","canvas.scaleFactor");if(!Array.isArray(o.texts))throw new t("Texts must be an array");for(let e=0;e<o.texts.length;e++){const r=o.texts[e];if(!r||"object"!=typeof r)throw new t(`Text element at index ${e} must be an object`);const a=r;if("string"!=typeof a.text)throw new t(`Text element at index ${e} must have a text property`);if(!a.css||"object"!=typeof a.css)throw new t(`Text element at index ${e} must have a css property`)}if(!Array.isArray(o.images))throw new t("Images must be an array");for(let e=0;e<o.images.length;e++){const r=o.images[e];if(!r||"object"!=typeof r)throw new t(`Image element at index ${e} must be an object`);const a=r;if("string"!=typeof a.src)throw new t(`Image element at index ${e} must have a src property`);if(!a.css||"object"!=typeof a.css)throw new t(`Image element at index ${e} must have a css property`)}return e},n=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"]),s=(e,t)=>{const r={zIndex:0};for(const[t,a]of Object.entries(e))if(n.has(t))try{switch(t){case"position":"absolute"!==a?console.warn(`Only 'position: absolute' is supported, ignoring 'position: ${a}'`):r.position="absolute";break;case"left":case"top":case"width":case"height":case"font-size":case"line-height":case"border-radius":const e=i(a);if(e<0)throw new o(`Negative values not allowed for '${t}': ${a}`,t,a);"left"===t?r.left=e:"top"===t?r.top=e:"width"===t?r.width=e:"height"===t?r.height=e:"font-size"===t?r.fontSize=e:"line-height"===t?r.lineHeight=e:"border-radius"===t&&(r.borderRadius=e);break;case"font-family":r.fontFamily=a;break;case"color":case"background-color":if(!l(a))throw new o(`Invalid color value for '${t}': ${a}`,t,a);"color"===t?r.color=a:r.backgroundColor=a;break;case"opacity":const n=parseFloat(a);if(isNaN(n)||n<0||n>1)throw new o(`Invalid opacity value: ${a}. Must be between 0 and 1`,t,a);r.opacity=n;break;case"text-align":if(!["left","center","right"].includes(a))throw new o(`Invalid text-align value: ${a}. Must be 'left', 'center', or 'right'`,t,a);r.textAlign=a;break;case"vertical-align":if(!["top","middle","center","bottom"].includes(a))throw new o(`Invalid vertical-align value: ${a}. Must be 'top', 'middle', 'center', or 'bottom'`,t,a);r.verticalAlign=a;break;case"padding":const s=parseInt(a,10);if(isNaN(s)||s<0)throw new o(`Invalid padding value: ${a}. Must be a non-negative number`,t,a);r.padding=s;break;case"border":r.border=c(a);break;case"z-index":const d=parseInt(a,10);if(isNaN(d))throw new o(`Invalid z-index value: ${a}. Must be a number`,t,a);r.zIndex=d}}catch(e){if(e instanceof o)throw e;throw new o(`Error parsing CSS property '${t}': ${e instanceof Error?e.message:"Unknown error"}`,t,a)}else console.warn(`Unsupported CSS property '${t}' will be ignored`);return r},i=e=>{const t=e.match(/^(-?\d+(?:\.\d+)?)px$/);if(!t)throw new o(`Invalid pixel value: ${e}. Must be a number followed by 'px'`,"pixel-value",e);return parseFloat(t[1])},l=e=>/^(#[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]+)$/.test(e),c=e=>{const t=e.trim().split(/\s+/);if(t.length<3)throw new o(`Invalid border value: ${e}. Must be in format 'width style color'`,"border",e);const r=i(t[0]),a=t[1],n=t.slice(2).join(" ");if(!["solid","dashed","dotted","double"].includes(a))throw new o(`Unsupported border style: ${a}. Supported styles: solid, dashed, dotted, double`,"border",e);if(!l(n))throw new o(`Invalid border color: ${n}`,"border",e);return{width:r,style:a,color:n}},d=new Map,h=async e=>{const t=[...new Set(e)].map(e=>(e=>new Promise((t,o)=>{const a=d.get(e);if(a)return void t({element:a,src:e});const n=new Image;n.onload=()=>{d.set(e,n),t({element:n,src:e})},n.onerror=()=>{o(new r(`Failed to load image: ${e}`,e))},u(e)&&(n.crossOrigin="anonymous"),n.src=e}))(e));try{return await Promise.all(t)}catch(e){throw new r(`Failed to load one or more images: ${e instanceof Error?e.message:"Unknown error"}`)}},u=e=>!e.startsWith("data:")&&!e.startsWith("blob:"),f=e=>{if(!e||"string"!=typeof e)throw new r("Image source must be a non-empty string");if(!(e=>/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,/.test(e))(e)&&!(e=>{try{const t=new URL(e);return["http:","https:","blob:"].includes(t.protocol)}catch{return!1}})(e))throw new r(`Invalid image source format: ${e}. Must be a valid URL or base64 data URI`,e)},p=e=>{const t=[];for(const o of e.texts){const e=s(o.css);t.push({type:"text",content:o.text,css:o.css,zIndex:e.zIndex})}for(const o of e.images){f(o.src);const e=s(o.css);t.push({type:"image",content:o.src,css:o.css,zIndex:e.zIndex})}return t.sort((e,t)=>e.zIndex-t.zIndex)},g=e=>{const{width:t,height:o,scaleFactor:r=1}=e.canvas;return((e,t)=>e>2e3||t>2e3)(t,o)?r:1},b=async(e,t,o)=>{const r=t.filter(e=>"image"===e.type).map(e=>e.content),a=await h(r),n=new Map(a.map(e=>[e.src,e.element]));for(const r of t){const t=$(r.css,o),a=s(t,r.type);if("text"===r.type)await w(e,r.content,a);else if("image"===r.type){const t=n.get(r.content);t&&await m(e,t,a)}}},w=async(e,t,o)=>{if(!o.left||!o.top)return void console.warn("Text element missing required position properties (left, top)");const r=o.fontSize||16,a=o.fontFamily||"Arial, sans-serif";e.font=`${r}px ${a}`,e.fillStyle=o.color||"#000000",e.textAlign=o.textAlign||"left",e.textBaseline="top",e.save(),void 0!==o.opacity&&(e.globalAlpha=o.opacity);if(void 0!==o.width){const a=o.lineHeight||1.2*r,n=o.border?o.border.width:0,s=o.padding,i=void 0!==s?s:Math.max(n,4),l=o.width-2*i,c=[];t.split("\n").forEach(t=>{if(e.measureText(t).width>l){const o=((e,t,o)=>{const r=t.split(" "),a=[];let n="";for(let t=0;t<r.length;t++){const s=r[t],i=n+(n?" ":"")+s;e.measureText(i).width>o&&n?(a.push(n),n=s):n=i}return n&&a.push(n),a})(e,t,l);c.push(...o)}else c.push(t)});let d=0;c.forEach(t=>{const o=e.measureText(t).width;d=Math.max(d,o)});const h=d,u=c.length*a,f=o.width||h,p=o.height||u+2*i,g=o.left+i;let b=o.top+i;if(o.verticalAlign){const e=c.length*a,t=p-2*i;"middle"===o.verticalAlign||"center"===o.verticalAlign?b=o.top+i+(t-e)/2:"bottom"===o.verticalAlign&&(b=o.top+p-i-e)}o.backgroundColor&&(e.fillStyle=o.backgroundColor,o.borderRadius&&o.borderRadius>0?y(e,o.left,o.top,f,p,o.borderRadius,!0):e.fillRect(o.left,o.top,f,p)),e.fillStyle=o.color||"#000000",c.forEach((t,r)=>{const n=b+r*a;"center"===o.textAlign?(e.textAlign="center",e.fillText(t,(o.left||0)+f/2,n)):"right"===o.textAlign?(e.textAlign="right",e.fillText(t,(o.left||0)+f-i,n)):(e.textAlign="left",e.fillText(t,g,n))}),o.border&&v(e,o.left,o.top,f,p,o.border,o.borderRadius)}else{const a=o.border?o.border.width:0,n=o.padding,s=void 0!==n?n:Math.max(a,2),i=o.lineHeight||1.2*r,l=t.split("\n");let c=0;l.forEach(t=>{const o=e.measureText(t).width;c=Math.max(c,o)});const d=c+2*s,h=(1===l.length?r:(l.length-1)*i+r)+2*s;let u=o.left+s;const f=o.top+s;"center"===o.textAlign?u=o.left+d/2:"right"===o.textAlign&&(u=o.left+d-s),o.backgroundColor&&(e.fillStyle=o.backgroundColor,o.borderRadius&&o.borderRadius>0?y(e,o.left,o.top,d,h,o.borderRadius,!0):e.fillRect(o.left,o.top,d,h)),e.fillStyle=o.color||"#000000",l.forEach((t,o)=>{const r=f+o*i;e.fillText(t,u,r)}),o.border&&v(e,o.left,o.top,d,h,o.border,o.borderRadius)}e.restore()},m=async(e,t,o)=>{if(!o.left||!o.top)return void console.warn("Image element missing required position properties (left, top)");const r=o.width||t.naturalWidth,a=o.height||t.naturalHeight;e.save(),void 0!==o.opacity&&(e.globalAlpha=o.opacity),o.backgroundColor&&(e.fillStyle=o.backgroundColor,o.borderRadius&&o.borderRadius>0?y(e,o.left,o.top,r,a,o.borderRadius,!0):e.fillRect(o.left,o.top,r,a)),o.borderRadius?x(e,t,o.left,o.top,r,a,o.borderRadius):e.drawImage(t,o.left,o.top,r,a),o.border&&v(e,o.left,o.top,r,a,o.border,o.borderRadius),e.restore()},v=(e,t,o,r,a,n,s)=>{if(e.strokeStyle=n.color,e.lineWidth=n.width,"double"===n.style){const i=n.width,l=Math.max(1,Math.floor(i/3)),c=Math.max(1,Math.floor(i/3));e.lineWidth=i,e.setLineDash([]),s&&s>0?y(e,t,o,r,a,s,!1):e.strokeRect(t,o,r,a),e.lineWidth=l;const d=t+c,h=o+c,u=r-2*c,f=a-2*c,p=Math.max(0,(s||0)-c);p>0?y(e,d,h,u,f,p,!1):e.strokeRect(d,h,u,f)}else e.setLineDash(I(n.style)),s&&s>0?y(e,t,o,r,a,s,!1):e.strokeRect(t,o,r,a),e.setLineDash([])},y=(e,t,o,r,a,n,s)=>{e.beginPath(),e.moveTo(t+n,o),e.lineTo(t+r-n,o),e.quadraticCurveTo(t+r,o,t+r,o+n),e.lineTo(t+r,o+a-n),e.quadraticCurveTo(t+r,o+a,t+r-n,o+a),e.lineTo(t+n,o+a),e.quadraticCurveTo(t,o+a,t,o+a-n),e.lineTo(t,o+n),e.quadraticCurveTo(t,o,t+n,o),e.closePath(),s?e.fill():e.stroke()},x=(e,t,o,r,a,n,s)=>{e.save(),e.beginPath(),e.moveTo(o+s,r),e.lineTo(o+a-s,r),e.quadraticCurveTo(o+a,r,o+a,r+s),e.lineTo(o+a,r+n-s),e.quadraticCurveTo(o+a,r+n,o+a-s,r+n),e.lineTo(o+s,r+n),e.quadraticCurveTo(o,r+n,o,r+n-s),e.lineTo(o,r+s),e.quadraticCurveTo(o,r,o+s,r),e.closePath(),e.clip(),e.drawImage(t,o,r,a,n),e.restore()},I=e=>{switch(e){case"dashed":return[5,5];case"dotted":return[2,2];case"double":return[0];default:return[]}},$=(e,t)=>{if(1===t)return e;const o={...e},r=["left","top","width","height","font-size","border-width","border-radius"];for(const e of r)if(o[e]){const r=o[e].match(/^(-?\d+(?:\.\d+)?)px$/);if(r){const a=parseFloat(r[1])*t;o[e]=`${a}px`}}return o.border&&(o.border=T(o.border,t)),o},T=(e,t)=>{const o=e.trim().split(/\s+/);if(o.length>=1){const e=o[0].match(/^(-?\d+(?:\.\d+)?)px$/);if(e){const r=parseFloat(e[1])*t;o[0]=`${r}px`}}return o.join(" ")};e.CssValidationError=o,e.ImageLoadError=r,e.InvalidInputError=t,e.clearCache=()=>{d.clear()},e.renderCanvas=async(e,t)=>{const o=a(e),r=g(o),n=p(o);let s,i;t?(s=t.canvas,i=t):(s=document.createElement("canvas"),i=s.getContext("2d")),s.width=o.canvas.width,s.height=o.canvas.height,i.clearRect(0,0,s.width,s.height);const l=o.canvas.background||"white";i.fillStyle=l,i.fillRect(0,0,s.width,s.height),await b(i,n,r);const c=o.canvas.format||"png",d=o.canvas.quality||.9;return new Promise((e,t)=>{s.toBlob(o=>{o?e(o):t(new Error("Failed to create blob from canvas"))},`image/${c}`,"jpeg"===c?d:void 0)})},e.renderCanvasAsDataURL=async(e,t)=>{const o=a(e),r=g(o),n=p(o);let s,i;t?(s=t.canvas,i=t):(s=document.createElement("canvas"),i=s.getContext("2d")),s.width=o.canvas.width,s.height=o.canvas.height,i.clearRect(0,0,s.width,s.height);const l=o.canvas.background||"white";i.fillStyle=l,i.fillRect(0,0,s.width,s.height),await b(i,n,r);const c=o.canvas.format||"png",d=o.canvas.quality||.9;return s.toDataURL(`image/${c}`,"jpeg"===c?d:void 0)}}); //# sourceMappingURL=index.umd.min.js.map