23
loading...
This website collects cookies to deliver better user experience
Note: Code snippets are written in TypeScript.
foreignObject
to embed the text with an HTML paragraph (<p/>
) and a graphical image
element.<text/>
elements to draw graphics instead of HTML elements, however the feature needs to support dynamic inputs that can be too long and might need to be truncated and displayed with three ending dots ...
.<text/>
are not paragraphs but lines.<svg>
{this.text && (
<foreignObject>
<p>{this.text}</p>
</foreignObject>
)}
</svg>
p {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
<image/>
element.<svg>
{this.imgBase64 && this.imgMimeType && (
<image x="500" y="1000" width="64" height="64"
href={`data:${this.imgMimeType};base64,${this.imgBase64}`} />
)}
</svg>
foreignObject
with an HTML element <img/>
would have been possible for rendering purposes but, I did not find a way to ultimately export it to the resulting image.href="https://..."
) and had to first transform it to a base64
string.export const fetchImage = async ({imgSrc}: {imgSrc: string}): Promise<string | undefined> => {
const data: Response = await fetch(imgSrc);
const blob: Blob = await data.blob();
const base64: string = await toBase64({blob});
return base64.split(',')?.[1];
};
const toBase64 = ({blob}: {blob: Blob}): Promise<string> => {
return new Promise<string>((resolve, reject) => {
try {
const reader: FileReader = new FileReader();
reader.onloadend = () => {
const {result} = reader;
resolve(result as string);
};
reader.readAsDataURL(blob);
} catch (err) {
reject(err);
}
});
};
imgSrc
is the URL to the image -- the logo -- that should be embedded. It is first fetched, then transformed to a blob
and finally converted to a base64
string.@Method()
async toBlob(type: string = 'image/webp'): Promise<Blob> {
const canvas: HTMLCanvasElement =
await svgToCanvas({svg: this.svgRef});
return canvasToBlob({canvas, type});
}
image/webp
) for the export. According to my tests, it also works for other format such as image/png
and image/jpg
.HTMLCanvasElement
.export const transformCanvas = ({index}: Frame): Promise<SvgToCanvas | undefined> => {
return new Promise<SvgToCanvas | undefined>((resolve) => {
const svg: SVGGraphicsElement | null =
document.querySelector(`div[frame="${index}"] svg`);
if (!svg) {
resolve(undefined);
return;
}
const {width, height} = svgSize(svg);
const blob: Blob =
new Blob([svg.outerHTML],
{type: 'image/svg+xml;charset=utf-8'});
const blobURL: string = URL.createObjectURL(blob);
const image = new Image();
image.onload = () => {
const canvas: HTMLCanvasElement =
document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context: CanvasRenderingContext2D | null =
canvas.getContext('2d');
context?.drawImage(image, 0, 0, width, height);
URL.revokeObjectURL(blobURL);
resolve({
canvas,
index
});
};
image.src = blobURL;
});
};
const blob: Blob = new Blob([svg.outerHTML],
{type: 'image/svg+xml;charset=utf-8'});
const blobURL: string = URL.createObjectURL(blob);
Security Error: Tainted canvases may not be exported.
Image
object which, fortunately, was possible by using another serialization method.const base64SVG: string =
window.btoa(new XMLSerializer().serializeToString(svg));
const imgSrc: string = `data:image/svg+xml;base64,${base64SVG}`;
foreignObject
content needs its CSS styles to be inlined when exporting.const inlineStyle = ({clone, style}: {clone: SVGGraphicsElement; style: CSSStyleDeclaration}) => {
const text: HTMLParagraphElement | null =
clone.querySelector('foreignObject > p');
if (!text) {
return;
}
for (const key of Object.keys(style)) {
text.style.setProperty(key, style[key]);
}
};
export const svgToCanvas = ({svg, style}: {svg: SVGGraphicsElement; style: CSSStyleDeclaration}): Promise<HTMLCanvasElement> => {
return new Promise<HTMLCanvasElement>(async (resolve) => {
const {width, height} = svgSize(svg);
const clone: SVGGraphicsElement =
svg.cloneNode(true) as SVGGraphicsElement;
inlineStyle({clone, style});
const base64SVG: string =
window.btoa(new XMLSerializer().serializeToString(clone));
const imgSrc: string = `data:image/svg+xml;base64,${base64SVG}`;
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => {
const canvas: HTMLCanvasElement =
document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context: CanvasRenderingContext2D | null =
canvas.getContext('2d');
context?.drawImage(image, 0, 0, width, height);
resolve(canvas);
};
image.src = imgSrc;
});
};
@Method()
async toBlob(type: string = 'image/webp'): Promise<Blob> {
const style: CSSStyleDeclaration | undefined =
this.textRef ? getComputedStyle(this.textRef) : undefined;
const canvas: HTMLCanvasElement =
await svgToCanvas({svg: this.svgRef, style});
return canvasToBlob({canvas, type});
}
export const canvasToBlob =
async ({canvas, type}: {canvas: HTMLCanvasElement; type: string}):
Promise<Blob> => {
const dataUrl: string = canvas.toDataURL(type);
return (await fetch(dataUrl)).blob();
};
data:
in the connect-src
rule of the content security policy (CSP) which is strongly discouraged.callback
as argument.export const canvasToBlob =
async ({canvas, type}: {canvas: HTMLCanvasElement; type: string}):
Promise<Blob> => {
return new Promise<Blob>((resolve) => canvas.toBlob((blob: Blob) => resolve(blob), type));
};