来自 SVG 的 ImageBitmap - 某些字体大小的锯齿状文本
ImageBitmap from SVG - Jagged Text for some Font Sizes
我正在动态创建包含透明背景文本的 SVG 代码。 SVG 应绘制在 canvas 上,字体应来自 Google 字体。
问题:
虽然该方法基本上有效,但某些字体大小显然会产生错误的 alpha 通道 createImageBitmap()
,导致文本出现可怕的锯齿状。
我在 Windows 10 和 Ubuntu 的最新版本 Chrome 上遇到了问题。停用 Chrome 的硬件加速不会改变任何东西。
输出图像:Text with jagged edges
简而言之,这就是代码的作用:
- 生成在透明背景上显示一些文本的 SVG 源代码。
- 在 SVG 代码中,将指向外部内容(字体)的链接替换为相应的 base64 内容。
- 使用
createImageBitmap()
从该 SVG 创建 ImageBitmap。
- 在 canvas.
上绘制该 ImageBitmap
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];
/**
* @uses
* @license CC BY-SA 4.0.
*/
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 Whosebug
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>
因为 canvas 上下文也接受 HTMLImageElement
作为输入,所以在这里使用 createImageBitmap()
是多余的。相反,我们 return DOM 加载了 <img>
本身,从而绕过了 createImageBitmap()
,这显然导致了锯齿状的边缘。感谢@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;
});
};
我正在动态创建包含透明背景文本的 SVG 代码。 SVG 应绘制在 canvas 上,字体应来自 Google 字体。
问题:
虽然该方法基本上有效,但某些字体大小显然会产生错误的 alpha 通道 createImageBitmap()
,导致文本出现可怕的锯齿状。
我在 Windows 10 和 Ubuntu 的最新版本 Chrome 上遇到了问题。停用 Chrome 的硬件加速不会改变任何东西。
输出图像:Text with jagged edges
简而言之,这就是代码的作用:
- 生成在透明背景上显示一些文本的 SVG 源代码。
- 在 SVG 代码中,将指向外部内容(字体)的链接替换为相应的 base64 内容。
- 使用
createImageBitmap()
从该 SVG 创建 ImageBitmap。 - 在 canvas. 上绘制该 ImageBitmap
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];
/**
* @uses
* @license CC BY-SA 4.0.
*/
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 Whosebug
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>
因为 canvas 上下文也接受 HTMLImageElement
作为输入,所以在这里使用 createImageBitmap()
是多余的。相反,我们 return DOM 加载了 <img>
本身,从而绕过了 createImageBitmap()
,这显然导致了锯齿状的边缘。感谢@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;
});
};