为什么将 blob 转换为数据 URI 会产生与直接数据 URI 方法不同的 URI?

Why does converting a blob to a data URI result in a different URI than the direct data URI method?

我正在做一个实验,我发现将 Canvas 转换为 blob,然后再转换为数据 URI 会产生与直接从 canvas 获取数据 URI 不同的 URI。打开后的内容在两个 URI 上几乎相同。

使用blob方法时,如何获得与直接数据URI方法相同的URI结果?

let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let text = "Bufferoverrun";

ctx.textBaseline = "top";
ctx.font = "16px 'Arial'";
ctx.textBaseline = "alphabetic";
ctx.rotate(.05);
ctx.fillStyle = "#f60";
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = "#069";
ctx.fillText(text, 2, 15);
ctx.fillStyle = "rgba(102, 200, 0, 0.7)";
ctx.fillText(text, 4, 17);
ctx.shadowBlur = 10;
ctx.shadowColor = "blue";
ctx.fillRect(-20, 10, 234, 5);

const blobToBase64 = blob => {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    return new Promise(resolve => {
        reader.onloadend = () => {
            resolve(reader.result);
        };
    });
};

canvas.toBlob(async function (result) {
    let blobToURL = await blobToBase64(result)
    if (blobToURL != canvas.toDataURL()) {
        console.log("Data mismatch");
    } else {
        console.log("Match")
    }
})

我查看了 Chrome 的 Blink 内部结构,但找不到任何可以解释这一变化的内容。

Canvas Element Source Code - Blink

当前的区别在于 toBlobtoDataURL 处理颜色-space 转换的方式不同。

toBlob 在某些情况下可能会保留当前颜色 - space 并将其包含在生成的 Blob 中,toDataURL 永远不会,而当 toBlob 会,它使用其他路径进行转换。

这是他们为 toDataURL and here is for toBlob.

进行转换的地方

有趣的是,Chrome 会在将两个文件重新绘制回 canvas 时从这两个文件生成相同的位图,但是如果您将两个结果都保存到磁盘并在浏览器中绘制它们查看来源 color-space 就像 Firefox,你会发现它们实际上是不同的。


请注意,这可能不是您会发现两种方法之间差异的唯一地方。

例如,我还注意到toBlob会受到canvas加速与否的影响(下面的演示需要chrome://flags/#enable-experimental-web-platform-features):

const test = (accelerated) => {
  const HW = accelerated ? "accelerated" : "not-accelerated"
  let canvas = document.createElement('canvas');
  let ctx = canvas.getContext('2d', {
    willReadFrequently: !accelerated
  });
  let text = "Bufferoverrun";

  ctx.fillStyle = "#f60";
  ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = "#069";
  ctx.fillText(text, 2, 15);
  ctx.fillStyle = "rgba(102, 200, 0, 0.7)";
  ctx.shadowBlur = 10;
  ctx.shadowColor = "blue";
  ctx.fillRect(-20, 10, 234, 5);

  canvas.toBlob(async function(result) {
    let blobToURL = await blobToBase64(result)
    const data = canvas.toDataURL();
    if (blobToURL != data) {
      console.log(HW, "Data mismatch");
    } else {
      console.log(HW, "Match")
    }
  });

  function blobToBase64(blob) {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    return new Promise(resolve => {
      reader.onloadend = () => {
        resolve(reader.result);
      };
    });
  }
};
test(true);
test(false);


而且我应该指出,无论如何,您不应该期望这些方法return 具有相同的值。规范中没有任何要求,恰恰相反,人们可以预期,由于 toBlob 是异步的并并行执行其编码 ,因此它会使用速度较慢但质量更好的选项编码器比同步 toDataURL.

但是如果对于您自己的情况,您希望通过两种方法获得相同的结果,您可以通过禁用硬件加速 (chrome://settings/?search=hardware acceleration) 强制软件渲染,它们将产生相同的结果。


从评论中 OP 解释说它们实际上是在一个网络扩展中,并且区域实际上处理一个没有 toDataURL 方法的 OffscreenCanvas。

所以首先,在 Firefox 中有一个 demote method 可用于 ChromeContexts(网络扩展)的二维上下文,它应该允许强制使用软件渲染器,但Chrome 没有实现此功能

现在关于工作案例中的 OffscreenCanvas,一种解决方法是从我们从中获取 OffscreenCanvas 的 HTMLCanvasElement 上的主线程调用 toDataURL()

const worker = new Worker( worker_url );
const workercanvas = document.createElement( "canvas" );
const offscreen = workercanvas.transferControlToOffscreen();
worker.postMessage(offscreen, [offscreen]);
worker.onmessage = (evt) => {
  const fromworker = workercanvas.toDataURL();
  const frommain = getDataURLFromMain();
  console.log( fromworker === frommain ? "Match" : "Data mismatch" );
};
<script>
// boiler plate prepare scripts content for SO's StackSnippet
const drawing_ops = `
  const ctx = canvas.getContext("2d");
  const text = "Bufferoverrun";

  ctx.textBaseline = "top";
  ctx.font = "16px 'Arial'";
  ctx.textBaseline = "alphabetic";
  ctx.rotate(.05);
  ctx.fillStyle = "#f60";
  ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = "#069";
  ctx.fillText(text, 2, 15);
  ctx.fillStyle = "rgba(102, 200, 0, 0.7)";
  ctx.fillText(text, 4, 17);
  ctx.shadowBlur = 10;
  ctx.shadowColor = "blue";
  ctx.fillRect(-20, 10, 234, 5);
`;
const getDataURLFromMain = new Function(`
  const canvas = document.createElement("canvas");
${ drawing_ops }
  return canvas.toDataURL();
`);
const worker_script = new Blob( [ `
onmessage = (evt) => {
  const canvas = evt.data;
${ drawing_ops }
  if( ctx.commit ) { // might require WebPlatformFeatures flags
    ctx.commit();
    postMessage("");
  }
  else {
    requestAnimationFrame(() => postMessage(""));  
  }
};` ] );
const worker_url = URL.createObjectURL( worker_script );
</script>

Also available as a glitch 因为它可能更清楚。