0

I'm trying make my action icon dynamic by manipulating it with offscreenCanvas():

service_worker.js

const iconFile = `icon.png`;
const canvas = new OffscreenCanvas(24, 24),
      ctx = canvas.getContext("2d");

fetch(iconFile)
.then(r => r.blob())
.then(createImageBitmap)
.then(img => ctx.drawImage(img, 0, 0))
.then(() =>
{
  ctx.fillStyle = 'lightgreen';
  ctx.fillRect(0, canvas.height-9, canvas.width, 9);
  chrome.action.setIcon({imageData: ctx.getImageData(0, 0, 24, 24)});
});

This works fine, however when I attempt use a SVG image instead of PNG, the createImageBitmap() returns error:

DOMException: The source image could not be decoded

manifest.json

{
  "manifest_version": 3,
  "name": "Test extension",
  "author": "test",
  "description": "Test",
  "version": "0.0.1",

  "permissions":[],
  "action": {},
  "background": {
    "service_worker": "service_worker.js"
  }
}

icon.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#00864b" d="M8.1 18.8.7 11.4l2.1-2.2 5.3 5.3L21.4 1.2l2.2 2.1z"/></svg>

Any suggestions how to use SVG image in offscreenCanvas in service workers?

Kaiido
  • 123,334
  • 13
  • 219
  • 285
vanowm
  • 9,466
  • 2
  • 21
  • 37
  • 1
    createImageBitmap doesn't support SVG images. Use a PNG and standard canvas drawing commands. – wOxxOm Aug 21 '22 at 09:21
  • @wOxxOm that sucks...back in a day we had tools to build skyscrappers out extensions (XUL), now we can't even make a sandcastle - the sand is so dry... – vanowm Aug 21 '22 at 16:00
  • Does this answer your question? [Is there a way to render an SVG string to an OffscreenCanvas in a web worker?](https://stackoverflow.com/questions/70071803/is-there-a-way-to-render-an-svg-string-to-an-offscreencanvas-in-a-web-worker) – Kaiido Aug 22 '22 at 02:24
  • If it has absolute width and height, you can fetch your SVG image into an (or use [my polyfill](https://github.com/Kaiido/createImageBitmap)) **on the document's thread**, (is script injection still possible with v3?) create your ImageBitmap from there and then pass that ImageBitmap to your SW. But using a full canvas renderer is probably still the easiest. – Kaiido Aug 22 '22 at 02:27
  • And if your icons are really that simple that wouldn't be too hard to parse them yourself: https://jsfiddle.net/qdv2ywax/ And if they're used solely for this purpose you could even consider storing their color and path-data as JSON instead of SVG: https://jsfiddle.net/qdv2ywax/1/ – Kaiido Aug 22 '22 at 02:46
  • @Kaiido there is no DOM available in service workers, hens no or any other elements can be used. – vanowm Aug 22 '22 at 03:59
  • That's why I said, in bold, "**on the document's thread**". I checked since and you can still inject scripts in the current tab from your SW. So that's still an option. And anyway neither using canvg nor making your own parser or using JSON instead require an access to the DOM. – Kaiido Aug 22 '22 at 04:54

1 Answers1

0

If your the SVG images in your Blobs have absolute width and height, they should be supported by createImageBitmap natively. However, currently no browser supports this feature, and while I made a polyfill for the main thread, it won't work in a Worker where implementers are very reluctant to add an SVG parser.

So one could have hoped that injecting some scripts into the current tab would have made it.

async function execScript() {
  const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
  const bmp = await chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: grabImageBitmap,
    args: [the_url]
  });
  const canvas = new OffscreenCanvas(bmp.width, bmp.height);
  // ... do whatever with the bitmap
}

function grabImageBitmap(url) {
  const img = new Image();
  img.src = url;
  // we can't use img.decode() here, doesn't work with SVG...
  return new Promise((res, rej) => {
    img.onload = async (evt) => {
      const bmp = await createImageBitmap(img);
      res(bmp);
    };
    img.onerror = rej;
  });
}

But Chrome extensions still use JSON serialization for their various messaging APIs (BUG 248548) and there is thus no way (that I found) of transferring the ImageBitmap from one context to the other in MV3.


So you will have to parse the SVG image yourself and convert it to Canvas2D commands.
For this task, a library like canvg may come handy. They're in the area for a long time, seem to do a pretty good job, and do work in Workers... after some tweaks.
They only provide an UMD version of their library, so you'd have to build it to ESM yourself, and also bring a DOMParser implementation (canvg uses xmldom, you can make your build export it too).
Then, you have to declare your background script as a module and you can import it all in there:

import { Canvg, presets, DOMParser } from "your_custom_build.js";
const preset = presets.offscreen({ DOMParser });

async function execScript() {
  const req = await fetch(the_url);
  const markup = req.ok && await req.text();
  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext("2d");

  const renderer = await Canvg.from(ctx, markup, preset);
  renderer.render();
  // The SVG image has been drawn on your context
}

But if you don't want to do all these builds, and add the 300KB of lib necessary for all this, AND if you have only icons as simple as the one in your example, which consist of only <path> elements, you can certainly come up with your own parser, or even avoid the SVG format entirely and instead store the data as JSON. The Path2D constructor can take an SVG path declaration string as input, which allows us to generate simple assets in JSON format:

(async () => {
  // You'd use the OffscreenCanvas() constructor in background.js
  const ctx = document.querySelector("canvas").getContext("2d");
  // You'd use an actual URL to a JSON file
  const JSONURL = `data:application/json,${
    encodeURIComponent(JSON.stringify(
      [
        {
          "d": "M8.1 18.8.7 11.4l2.1-2.2 5.3 5.3L21.4 1.2l2.2 2.1z",
          "fill": "#00864b",
          "transform": "matrix(2, 0, 0, 2, 20, 20)",
          "stroke": "red",
          "strokeWidth": 3
        },
        // Add more if needed
      ]
    ))
}`;

  const resp = await fetch(JSONURL);
  const pathes = resp.ok && await resp.json();
  for (const path of pathes) {
    const pathObject = new Path2D(path.d);
    if (path.transform) { // as a CSSMatrixFunction
      ctx.setTransform(new DOMMatrix(path.transform));
    }
    if (path.stroke) {
      ctx.lineWidth = path.strokeWidth ?? 1;
      ctx.strokeStyle = path.stroke;
      ctx.stroke(pathObject);
    }
    if (path.fill) {
      ctx.fillStyle = path.fill;
      ctx.fill(pathObject);
    }
    // Add more features if wanted
  }
})().catch(console.error);
<canvas></canvas>
Kaiido
  • 123,334
  • 13
  • 219
  • 285