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.1 kB
class e extends Error{constructor(e,t){super(e),this.field=t,this.name="InvalidInputError"}}class t extends Error{constructor(e,t,o,r){super(e),this.property=t,this.value=o,this.details=r,this.name="CssValidationError"}}class o extends Error{constructor(e,t){super(e),this.src=t,this.name="ImageLoadError"}}const r=t=>{if(!t||"object"!=typeof t)throw new e("Input must be an object");const o=t;if(!o.canvas||"object"!=typeof o.canvas)throw new e("Input must contain a canvas property");const r=o.canvas;if("number"!=typeof r.width||r.width<1||r.width>3e3)throw new e("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 e("Canvas height must be a number between 1 and 4244","canvas.height");if(void 0!==r.background&&"string"!=typeof r.background)throw new e("Canvas background must be a string","canvas.background");if(void 0!==r.format&&!["png","jpeg"].includes(r.format))throw new e('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 e("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 e("Canvas scaleFactor must be a positive number","canvas.scaleFactor");if(!Array.isArray(o.texts))throw new e("Texts must be an array");for(let t=0;t<o.texts.length;t++){const r=o.texts[t];if(!r||"object"!=typeof r)throw new e(`Text element at index ${t} must be an object`);const a=r;if("string"!=typeof a.text)throw new e(`Text element at index ${t} must have a text property`);if(!a.css||"object"!=typeof a.css)throw new e(`Text element at index ${t} must have a css property`)}if(!Array.isArray(o.images))throw new e("Images must be an array");for(let t=0;t<o.images.length;t++){const r=o.images[t];if(!r||"object"!=typeof r)throw new e(`Image element at index ${t} must be an object`);const a=r;if("string"!=typeof a.src)throw new e(`Image element at index ${t} must have a src property`);if(!a.css||"object"!=typeof a.css)throw new e(`Image element at index ${t} must have a css property`)}return t},a=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"]),n=(e,o)=>{const r={zIndex:0};for(const[o,n]of Object.entries(e))if(a.has(o))try{switch(o){case"position":"absolute"!==n?console.warn(`Only 'position: absolute' is supported, ignoring 'position: ${n}'`):r.position="absolute";break;case"left":case"top":case"width":case"height":case"font-size":case"line-height":case"border-radius":const e=s(n);if(e<0)throw new t(`Negative values not allowed for '${o}': ${n}`,o,n);"left"===o?r.left=e:"top"===o?r.top=e:"width"===o?r.width=e:"height"===o?r.height=e:"font-size"===o?r.fontSize=e:"line-height"===o?r.lineHeight=e:"border-radius"===o&&(r.borderRadius=e);break;case"font-family":r.fontFamily=n;break;case"color":case"background-color":if(!i(n))throw new t(`Invalid color value for '${o}': ${n}`,o,n);"color"===o?r.color=n:r.backgroundColor=n;break;case"opacity":const a=parseFloat(n);if(isNaN(a)||a<0||a>1)throw new t(`Invalid opacity value: ${n}. Must be between 0 and 1`,o,n);r.opacity=a;break;case"text-align":if(!["left","center","right"].includes(n))throw new t(`Invalid text-align value: ${n}. Must be 'left', 'center', or 'right'`,o,n);r.textAlign=n;break;case"vertical-align":if(!["top","middle","center","bottom"].includes(n))throw new t(`Invalid vertical-align value: ${n}. Must be 'top', 'middle', 'center', or 'bottom'`,o,n);r.verticalAlign=n;break;case"padding":const c=parseInt(n,10);if(isNaN(c)||c<0)throw new t(`Invalid padding value: ${n}. Must be a non-negative number`,o,n);r.padding=c;break;case"border":r.border=l(n);break;case"z-index":const d=parseInt(n,10);if(isNaN(d))throw new t(`Invalid z-index value: ${n}. Must be a number`,o,n);r.zIndex=d}}catch(e){if(e instanceof t)throw e;throw new t(`Error parsing CSS property '${o}': ${e instanceof Error?e.message:"Unknown error"}`,o,n)}else console.warn(`Unsupported CSS property '${o}' will be ignored`);return r},s=e=>{const o=e.match(/^(-?\d+(?:\.\d+)?)px$/);if(!o)throw new t(`Invalid pixel value: ${e}. Must be a number followed by 'px'`,"pixel-value",e);return parseFloat(o[1])},i=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),l=e=>{const o=e.trim().split(/\s+/);if(o.length<3)throw new t(`Invalid border value: ${e}. Must be in format 'width style color'`,"border",e);const r=s(o[0]),a=o[1],n=o.slice(2).join(" ");if(!["solid","dashed","dotted","double"].includes(a))throw new t(`Unsupported border style: ${a}. Supported styles: solid, dashed, dotted, double`,"border",e);if(!i(n))throw new t(`Invalid border color: ${n}`,"border",e);return{width:r,style:a,color:n}},c=new Map,d=async e=>{const t=[...new Set(e)].map(e=>(e=>new Promise((t,r)=>{const a=c.get(e);if(a)return void t({element:a,src:e});const n=new Image;n.onload=()=>{c.set(e,n),t({element:n,src:e})},n.onerror=()=>{r(new o(`Failed to load image: ${e}`,e))},h(e)&&(n.crossOrigin="anonymous"),n.src=e}))(e));try{return await Promise.all(t)}catch(e){throw new o(`Failed to load one or more images: ${e instanceof Error?e.message:"Unknown error"}`)}},h=e=>!e.startsWith("data:")&&!e.startsWith("blob:"),u=e=>{if(!e||"string"!=typeof e)throw new o("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 o(`Invalid image source format: ${e}. Must be a valid URL or base64 data URI`,e)},f=e=>{const t=[];for(const o of e.texts){const e=n(o.css);t.push({type:"text",content:o.text,css:o.css,zIndex:e.zIndex})}for(const o of e.images){u(o.src);const e=n(o.css);t.push({type:"image",content:o.src,css:o.css,zIndex:e.zIndex})}return t.sort((e,t)=>e.zIndex-t.zIndex)},p=e=>{const{width:t,height:o,scaleFactor:r=1}=e.canvas;return((e,t)=>e>2e3||t>2e3)(t,o)?r:1},g=async(e,t,o)=>{const r=t.filter(e=>"image"===e.type).map(e=>e.content),a=await d(r),s=new Map(a.map(e=>[e.src,e.element]));for(const r of t){const t=$(r.css,o),a=n(t,r.type);if("text"===r.type)await b(e,r.content,a);else if("image"===r.type){const t=s.get(r.content);t&&await w(e,t,a)}}},b=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?v(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&&m(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?v(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&&m(e,o.left,o.top,d,h,o.border,o.borderRadius)}e.restore()},w=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?v(e,o.left,o.top,r,a,o.borderRadius,!0):e.fillRect(o.left,o.top,r,a)),o.borderRadius?y(e,t,o.left,o.top,r,a,o.borderRadius):e.drawImage(t,o.left,o.top,r,a),o.border&&m(e,o.left,o.top,r,a,o.border,o.borderRadius),e.restore()},m=(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?v(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?v(e,d,h,u,f,p,!1):e.strokeRect(d,h,u,f)}else e.setLineDash(x(n.style)),s&&s>0?v(e,t,o,r,a,s,!1):e.strokeRect(t,o,r,a),e.setLineDash([])},v=(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()},y=(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()},x=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=I(o.border,t)),o},I=(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(" ")},k=async(e,t)=>{const o=r(e),a=p(o),n=f(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 g(i,n,a);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)})},T=async(e,t)=>{const o=r(e),a=p(o),n=f(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 g(i,n,a);const c=o.canvas.format||"png",d=o.canvas.quality||.9;return s.toDataURL(`image/${c}`,"jpeg"===c?d:void 0)},R=()=>{c.clear()};export{t as CssValidationError,o as ImageLoadError,e as InvalidInputError,R as clearCache,k as renderCanvas,T as renderCanvasAsDataURL}; //# sourceMappingURL=index.esm.min.js.map