首页 > 解决方案 > 为什么将 blob 转换为数据 URI 会导致与直接数据 URI 方法不同的 URI?

问题描述

我正在做一个实验,我发现将 Canvas 转换为 blob 然后转换为数据 URI 会导致与直接从画布获取数据 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 内部结构,找不到任何可以解释这种变化的东西。

画布元素源代码 - 闪烁

标签: javascripthtml5-canvas

解决方案


当前的区别在于颜色空间转换的方式toBlob和处理方式不同。toDataURL

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

这是他们为toDataURL进行转换的地方 ,这里是toBlob


有趣的是,Chrome 会在再次将它们重新绘制到画布上时从这两个文件生成相同的位图,但是如果您将两个结果保存到磁盘并在像 Firefox 这样检查源颜色空间的浏览器中绘制它们,您将看到它们实际上是不同的。


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

例如,我还注意到toBlob会受到画布加速与否的影响(下面的演示需要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);


而且我应该注意,您不应该期望这些方法无论如何都会返回相同的值。规范中没有任何要求,恰恰相反,由于toBlob它是异步的并且并行执行其编码,因此它的编码器将使用比同步更慢但质量更好的选项toDataURL

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


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

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

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

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>

也可作为小故障使用,因为它可能更清晰。


推荐阅读