Chrome extension with SVG icons (chrome.browserAction.setIcon)

Tags: ,



I am using scalable SVG icons in my Chrome extension.

chrome.browserAction.setIcon({
  tabId: tabId,
  path: '../icons/' + icon + '/scalable.svg'
});

I want to switch icons based on some parameters, so I have visual feedback.

What I have realized is that when I am switching icons very quickly, Chrome is messing up and often I end up seeing the wrong icon. I added console.log prints to code to ensure I am switching icons properly and I see that my code has no errors.

It looks like Chrome executes such change of icon requests asynchronously and conversion of SVG to pixels takes sometimes longer than usual. That leads to an execution in the wrong order.

So for example if I switch icons from A to B, then to C; then to D, … at the end I may see C, although the last request for change was to switch it to D.

Any ideas on how to fix this annoying problem?

Answer

  1. Chain the calls to the API using Promise
  2. If you call setIcon often, create a cache of imageData yourself and use it instead of path because the API re-reads the source icon each time and re-creates imageData.

Here’s a generic example, not tested:

const queue = {};
const cache = {};
// auto-clean the queue so it doesn't grow infinitely
chrome.tabs.onRemoved.addListener(tabId => delete queue[tabId]);

async function setIcon(tabId, icon) {
  const url = '../icons/' + icon + '/scalable.svg';
  const imageData = await (cache[url] || (cache[url] = loadImageData(url)));
  queue[tabId] = (queue[tabId] || Promise.resolve()).then(() =>
    new Promise(resolve =>
      chrome.browserAction.setIcon({tabId, imageData}, resolve)));
}

function loadImageData(url) {
  return new Promise((resolve, reject) => {
    const data = {};
    const img = new Image();
    img.src = url;
    img.onload = () => {
      for (const size of [16, 32]) {
        const canvas = document.createElement('canvas');
        document.documentElement.appendChild(canvas);
        canvas.width = canvas.height = size;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        data[size] = ctx.getImageData(0, 0, size, size);
        canvas.remove();
      }
      resolve(data);
    };
    img.onerror = reject;
  });
}


Source: stackoverflow