简体   繁体   中英

ImageBitmap from SVG - Jagged Text for some Font Sizes

I am dynamically creating SVG code containing text on a transparent background. The SVG should be drawn on a canvas, fonts should come from Google Fonts.

The problem:

While the approach basically works, some font sizes apparently yield bad alpha channels with createImageBitmap() , resulting in horribly jagged text.

I am experiencing the problem on the latest versions of Chrome on both Windows 10 and Ubuntu. Deactivating Chrome's hardware acceleration doesn't change anything.

Image of output: Text with jagged edges

In a nutshell, this is what the code does:

  1. Generate SVG sourcecode that displays some text on a transparent background.
  2. In SVG code, replace links to external content (fonts) with respective base64 content.
  3. Create an ImageBitmap from that SVG using createImageBitmap() .
  4. Draw that ImageBitmap on a canvas.

 function createSvg(bckgrColor1, bckgrColor2, w, h, fontSize) { return ` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" version="2" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}"> <style type="text/css"> @font-face { font-family: 'Lobster'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/lobster/v23/neILzCirqoswsqX9zoKmMw.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } </style> <text x="0" y="50" font-family="Lobster" font-size="${fontSize}"> Hello World! </text> </svg>`; } const _embedAssets = async function (svgSrc) { const _imageExtensions = ["png", "gif", "jpg", "jpeg", "svg", "bmp"]; const _fontExtensions = ["woff2"]; const _assetExtensions = [..._imageExtensions, ..._fontExtensions]; // Regex copied from https://stackoverflow.com/a/8943487/1273551, not "stress tested"... const urlRegex = /(\\bhttps?:\\/\\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi; const allUrls = svgSrc.match(urlRegex); const assetUrls = allUrls.filter((url) => _assetExtensions.some((extension) => url.toLowerCase().endsWith(`.${extension}`) ) ); const assetBase64Fetcher = assetUrls.map(_fetchBase64AssetUrl); const assetFetcherResults = await Promise.all(assetBase64Fetcher); return assetFetcherResults.reduce( (svgSrc, x) => svgSrc.replace(x.url, x.base64), svgSrc ); }; // Fetch asset (image or font) and convert it to base64 string representation. const _fetchBase64AssetUrl = async function (assetUrl) { return new Promise(async (resolve, reject) => { const resp = await fetch(assetUrl); const blob = await resp.blob(); const reader = new FileReader(); reader.onloadend = (event) => { const target = event.target; if (!target) { return reject(`Asset with URL "${assetUrl}" could not be loaded.`); } const result = target.result; if (!result) { return reject(`Asset with URL "${assetUrl}" returned an empty result.`); } resolve({ url: assetUrl, base64: result.toString() }); }; reader.readAsDataURL(blob); }); }; const createImageBitmapFromSvg = async function (svgSrc) { return new Promise(async (resolve) => { const svgWithAssetsEmbedded = await _embedAssets(svgSrc); const svgBlob = new Blob([svgWithAssetsEmbedded], { type: "image/svg+xml;charset=utf-8" }); const svgBase64 = URL.createObjectURL(svgBlob); let img = new Image(); img.onload = async () => { const imgBitmap = await createImageBitmap(img); resolve(imgBitmap); }; img.src = svgBase64; }); }; const renderCanvas = async function (canvas, svgSource, width, height, color) { canvas.width = width; canvas.height = height; let svgEmbedded = await _embedAssets(svgSource); let svgImageBitmap = await createImageBitmapFromSvg(svgEmbedded); let ctx = canvas.getContext("2d"); if (ctx) { ctx.fillStyle = color; ctx.strokeStyle = "#000000"; ctx.lineWidth = 2; ctx.fillRect(0, 0, canvas.width, canvas.height); // ctx.strokeRect(0, 0, canvas.width, canvas.height); //for white background ctx.drawImage(svgImageBitmap, 0, 0, canvas.width, canvas.height); } }; const renderCanvasAlternative = async function (canvas, svgSource, width, height, color) { // create imagebitmap from raw svg code let svgImageBitmap = await createImageBitmapFromSvg(svgSource); // temporary intermediate step as suggested on StackOverflow const osc = await new OffscreenCanvas(width, height) let oscx = osc.getContext("bitmaprenderer") oscx.transferFromImageBitmap(svgImageBitmap); const svgImageBitmapFromOffscreenCanvas = osc.transferToImageBitmap(); // const svgImageBitmapFromOffscreenCanvas2 = await createImageBitmap(osc); // results in empty bitmap // draw image bitmap on canvas canvas.width = width; canvas.height = height; let ctx = canvas.getContext("bitmaprenderer"); if (!ctx) throw new Error("Could not get context from canvas."); ctx.transferFromImageBitmap(svgImageBitmapFromOffscreenCanvas); } const bootstrap = async () => { // width and height for svg and canvases const w = "300"; const h = "80"; // create two svg sources, only difference is fontsize of embedded font const svgSourceGood = createSvg("", "", w, h, 49); const svgSourceBad = createSvg("#990000", "", w, h, 48); // draw GOOD svg in canvas renderCanvasAlternative( document.getElementById("myCanvas01"), svgSourceGood, w, h, "green" ); // draw BAD svg in canvas renderCanvasAlternative( document.getElementById("myCanvas02"), svgSourceBad, w, h, "red" ); }; bootstrap();
 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div>SVG drawn in Canvas, Fontsize 49</div> <canvas id="myCanvas01"></canvas> <div>SVG drawn in Canvas, Fontsize 48</div> <canvas id="myCanvas02"></canvas> <script src="index.js"></script> </body> </html>

Since the canvas context also accepts HTMLImageElement as an input, using createImageBitmap() is redundant here. Instead, we return the DOM loaded <img> itself, thus circumventing createImageBitmap() , which obviously caused the jagged edges. Thanks to @Kaiido.

 const createImageFromSvg = async function(svgSrc: string): Promise < HTMLImageElement > { return new Promise(async resolve => { // replace assets with their base64 versions in svg source code const svgWithAssetsEmbedded = await _embedAssets(svgSrc); // create blob from that const svgBlob = new Blob([svgWithAssetsEmbedded], { type: 'image/svg+xml;charset=utf-8' }); // create URL that can be used in HTML (?) const svgBase64 = URL.createObjectURL(svgBlob); let img = new Image(); img.onload = async() => { resolve(img); }; img.src = svgBase64; }); };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM