← ./articles-ja

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しようとする場合です。

参考