SVG foreignObjectはCanvasをtaintしてWebGL textureを壊すことがある
SVGの <foreignObject> でHTMLを描画し、そのSVGをcanvasへdrawし、さらにWebGL textureとして使うと、Chromiumでtextureがtaintedとして拒否されることがあります。
分かりにくいのは、remote image、external font、明らかなcross-origin resourceがなくても起きることです。<foreignObject> のrasterization path自体が、canvasをWebGL textureとして使えない状態にすることがあります。
症状
three.jsやWebGL appで、texture upload付近にsecurity errorが出ます。
SecurityError: Failed to execute 'texSubImage2D' on 'WebGL2RenderingContext':
Tainted canvases may not be loaded.
見た目はmissing texture、blank plane、fallback materialになります。
最小の失敗形
危険なpipelineです。
HTML layout
-> SVG <foreignObject>
-> SVG imageを<canvas>へdraw
-> THREE.CanvasTextureまたはWebGL texture upload
問題は <foreignObject> です。browserにHTML layoutをSVG rasterizationへ埋め込ませるため、localに見えるcontentでもcanvas tainting ruleに触れることがあります。
WebGL upload前にtaintを検出する
three.js texture initializationの try/catch だけに頼らないほうが安全です。library内部でerrorが遅延または捕捉されることがあります。
canvasを直接probeします。
function assertCanvasReadable(canvas: HTMLCanvasElement) {
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Canvas 2D context is not available.");
}
try {
context.getImageData(0, 0, 1, 1);
} catch (error) {
throw new Error(`Canvas is tainted and cannot be used as a WebGL texture: ${error}`);
}
}
texture作成前に呼びます。
assertCanvasReadable(canvas);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
tainted canvasはreadbackできないため、getImageData() は検出に向いています。
generated text textureはCanvas 2Dで直接描く
generated text、label、page、diagramをWebGL textureにしたいなら、SVG <foreignObject> を避けて直接描画します。
function renderLabel(text: string) {
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 512;
const context = canvas.getContext("2d");
if (!context) throw new Error("2D context unavailable.");
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "#111827";
context.font = "32px system-ui, sans-serif";
context.fillText(text, 48, 96);
assertCanvasReadable(canvas);
return canvas;
}
DOM layoutより手間は増えますが、WebGL textureとしては予測しやすくなります。
縦書きや複雑layout
日本語縦書きは <foreignObject> を使いたくなる理由の1つです。CSS writing-mode が便利だからです。
WebGL textureでは、canvas座標でlayoutするほうが安全です。
function drawVerticalText(
context: CanvasRenderingContext2D,
text: string,
startX: number,
startY: number,
lineHeight: number
) {
let y = startY;
for (const char of text) {
context.fillText(char, startX, y);
y += lineHeight;
}
}
pagination、columns、punctuation、font fallbackを自前で扱う必要はありますが、生成したcanvasはreadableなままです。
SVGが常に悪いわけではない
SVG全般を避ける話ではありません。純粋なSVG shape、path、gradient、textは安全にrasterizeできることがあります。
高リスクなのは、<foreignObject> でHTMLを埋め込み、それを通常のsame-origin bitmapのようにWebGLへuploadしようとする場合です。