svgweb-workercanvgoffscreen-canvas

How can I change the color of the svg?


I am using canvg inside of a Web Worker to draw an SVG into an OffscreenCanvas.

I will draw the SVG in black. Is it possible to apply a color to the SVG without modifying the SVG itself or using CSS? If so, how?

I would like it drawn in red, for example.

const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 0c17.7 0 32 14.3 32 32l0 10.4c93.7 13.9 167.7 88 181.6 181.6l10.4 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-10.4 0c-13.9 93.7-88 167.7-181.6 181.6l0 10.4c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-10.4C130.3 455.7 56.3 381.7 42.4 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l10.4 0C56.3 130.3 130.3 56.3 224 42.4L224 32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6l0-20.6c0-17.7 14.3-32 32-32s32 14.3 32 32l0 20.6c58.3-12.5 104.1-58.4 116.6-116.6L384 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l20.6 0C392.1 165.7 346.3 119.9 288 107.4l0 20.6c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-20.6C165.7 119.9 119.9 165.7 107.4 224l20.6 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-20.6 0zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>`;

var blob = new Blob(
  Array.prototype.map.call(
    document.querySelectorAll('script[type="text/js-worker"]'),
    function (oScript) {
      return oScript.textContent;
    }
  ),
  { type: "text/javascript" }
);

const worker = new Worker(window.URL.createObjectURL(blob), {
  type: "module"
});

const img = document.querySelector('img')

worker.postMessage({
  width: 20,
  height: 20,
  svg: svg
})

worker.onmessage = (event) => {
  img.src = event.data.pngUrl
}
<script type="text/js-worker">
  import { DOMParser } from 'https://cdn.skypack.dev/@xmldom/xmldom@^0.7.5'
import {
  Canvg,
  presets
} from 'https://cdn.skypack.dev/canvg@^4.0.0'

const preset = presets.offscreen({
  DOMParser
})

self.onmessage = async (event) => {
  const {
    width,
    height,
    svg
  } = event.data
  const canvas = new OffscreenCanvas(width, height)
  const ctx = canvas.getContext('2d')
  const v = await Canvg.from(ctx, svg, preset)

  // Render only first frame, ignoring animations and mouse.
  await v.render()

  const blob = await canvas.convertToBlob()
  const pngUrl = URL.createObjectURL(blob)

  self.postMessage({
    pngUrl
  })
}
</script>

<img />


Solution

  • If your icons are monochrome, you can apply a simple compositing operation to apply the expected color over the already painted area:

    const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 0c17.7 0 32 14.3 32 32l0 10.4c93.7 13.9 167.7 88 181.6 181.6l10.4 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-10.4 0c-13.9 93.7-88 167.7-181.6 181.6l0 10.4c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-10.4C130.3 455.7 56.3 381.7 42.4 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l10.4 0C56.3 130.3 130.3 56.3 224 42.4L224 32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6l0-20.6c0-17.7 14.3-32 32-32s32 14.3 32 32l0 20.6c58.3-12.5 104.1-58.4 116.6-116.6L384 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l20.6 0C392.1 165.7 346.3 119.9 288 107.4l0 20.6c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-20.6C165.7 119.9 119.9 165.7 107.4 224l20.6 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-20.6 0zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>`;
    
    var blob = new Blob(
      Array.prototype.map.call(
        document.querySelectorAll('script[type="text/js-worker"]'),
        function (oScript) {
          return oScript.textContent;
        }
      ),
      { type: "text/javascript" }
    );
    
    const worker = new Worker(window.URL.createObjectURL(blob), {
      type: "module"
    });
    
    const img = document.querySelector('img')
    
    worker.postMessage({
      width: 20,
      height: 20,
      svg: svg
    })
    
    worker.onmessage = (event) => {
      img.src = event.data.pngUrl
    }
    <script type="text/js-worker">
      import { DOMParser } from 'https://cdn.skypack.dev/@xmldom/xmldom@^0.7.5'
    import {
      Canvg,
      presets
    } from 'https://cdn.skypack.dev/canvg@^4.0.0'
    
    const preset = presets.offscreen({
      DOMParser
    })
    
    self.onmessage = async (event) => {
      const {
        width,
        height,
        svg
      } = event.data
      const canvas = new OffscreenCanvas(width, height)
      const ctx = canvas.getContext('2d')
      const v = await Canvg.from(ctx, svg, preset)
    
      // Render only first frame, ignoring animations and mouse.
      await v.render()
      // recolor
      ctx.fillStyle = "red";
      // next paint operation will only appear where it overlaps with already painted area
      ctx.globalCompositeOperation = "source-atop";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      
      const blob = await canvas.convertToBlob()
      const pngUrl = URL.createObjectURL(blob)
    
      self.postMessage({
        pngUrl
      })
    }
    </script>
    
    <img />

    Also, note that I didn't modify your code above, but you should really not create a PNG image from that canvas. Instead use an actual <canvas> element placeholder and call its transferControlToOffcsreen() method to get your OffscreenCanvas.