svg-sprite icon indicating copy to clipboard operation
svg-sprite copied to clipboard

Defs block for extracting gradients.

Open jaywolters opened this issue 9 years ago • 11 comments

With Symbols is it possible to configure svg-sprite to extract all the gradients <linearGradient> (etc) into a <defs> block -- or will I need to use Cheerio for that?

jaywolters avatar Apr 20 '15 20:04 jaywolters

Hey @jaywolters,

it's not really within svg-sprite's scope to perform SVG manipulations like that. You could, however, use a custom transform to operate on each single SVG shape before it's compiled into the sprite. See here for a recent example of how to re-color a shape (used with grunt-svg-sprite, but it'll work with svg-sprite just the same way).

svg-sprite doesn't use Cheerio but operates on a raw DOM implementation instead (xmldom). Please see here for some info about custom callback transformations.

Finally, I am considering adding a global post-transformation on the final sprite, but that has to be done yet.

Hope this helps!?

Cheers, Joschi

jkphl avatar Apr 20 '15 20:04 jkphl

Using the Symbol method and when working with pre-colored Icons that use gradients-- moving the gradients to a <Defs> block is essential for FF and IE to render the SVG correctly. This problem has been brought up in a few SO posts and grunt/gulp-svgstore made the change just recently. Thanks for your response.

jaywolters avatar Apr 21 '15 16:04 jaywolters

Good to know, thanks for this info. You don't happen to have some resources handy elaborating on the topic? I might consider hardwiring it then ...

jkphl avatar Apr 21 '15 17:04 jkphl

Refer to David Bushell's blog post on SVG and MDN using gradients in SVG you will see where it shows the SVG markup and how all the Gradients are placed inside of the <defs> block.
I experimented with this problem before figuring it out myself as I was transitioning from using <defs> to <symbols>. I noticed that gradients worked fine when I was using <defs> alone but once I started using <symbols> I lost the gradient fills in FireFox. After reading the SVG 1.1 spec and reading SO posts like this one it turns out that gradient fills are resources that should be defined in the <defs> block. Firefox is trying to address this issue at bugzilla.
Gradients are a PITA when it comes to SVG sprite sheets - Even after you move all the gradients to a <defs> block it is problematic using an external svg file with a fragment identifier. Firefox will render the gradients and icons just fine while Chrome and Safari will do different things either filling with currentColor or simply not filling the space at all. David Bushell's article covers all this and says everything I learned from trial and error. In-order for Symbols with Gradients to display correctly and predictably across all browsers the svg-sprite must be inline (in the page/Dom) and the gradients have to be in a <defs> block. The svgstore people talk about it here. I hope this helps.

jaywolters avatar Apr 21 '15 19:04 jaywolters

Thanks a lot for these useful resources! You convinced me that this should be a core feature of svg-sprite, as it will greatly improve the overall usability of the sprites. I'll try to implement the gradient extraction, but please give me some time for that. Unfortunately, I'm extremely overloaded these days an am having a hard time getting all my things done. Again, thanks a lot!

jkphl avatar Apr 21 '15 19:04 jkphl

Necromancy!

Since nobody is working on this issue I decided to quickly create a solution using transforms in config. Here is my solution, in case somebody else needs to support gradients on Firefox.

@jkphl in the next few weeks I could create PR with this solution (cleaned up, of course). Though I am not sure where to place this kind of functionality. Any ideas?

const defs = new DOMParser().parseFromString('<defs></defs>');
let count = 0;

const config = {
  dest: 'public/svg',
  mode: {
    symbol: {
      dest: '.',
      sprite: 'sprite.svg',
      inline: true,
    },
  },
  shape: {
    transform: [
      gradientsExtraction,
      'svgo',
    ],
  },
  svg: {
    transform: [
      /**
        * Adds defs tag at the top of svg with all extracted gradients.
        * @param {string} svg
        * @return {string} svg
        */
      function(svg) {
        return svg.replace(
          '<symbol ',
          new XMLSerializer().serializeToString(defs) + '<symbol '
        );
      },
    ],
  },
};

/**
 * Extracts gradient from the sprite and replaces their ids to prevent duplicates.
 * @param {SVGShape} shape
 * @param {SVGSpriter} spriter
 * @param {Function} callback
 */
function gradientsExtraction(shape, spriter, callback) {
  const idsToReplace = [].concat(
    extractGradients(shape, 'linearGradient'),
    extractGradients(shape, 'radialGradient')
  );

  shape.setSVG(updateUrls(shape.getSVG(), idsToReplace));

  callback(null);
}

/**
 * Extracts specific gradient defined by tag from given shape.
 * @param {SVGShape} shape
 * @param {string} tag
 * @return {Array}
 */
function extractGradients(shape, tag) {
  const idsToReplace = [];

  const gradients = shape.dom.getElementsByTagName(tag);
  while (gradients.length > 0) {
    // Add gradient to defs block
    defs.documentElement.appendChild(gradients[0]);

    // Give gradient new ID
    const id = gradients[0].getAttribute('id');
    const newId = 'g' + (++count);
    gradients[0].setAttribute('id', newId);

    idsToReplace.push([id, newId]);
  }

  return idsToReplace;
}

/**
 * Updates urls in given SVG from array of [oldId, newId].
 * @param {string} svg
 * @param {Array} idsToReplace
 * @return {string}
 */
function updateUrls(svg, idsToReplace) {
  for (let i = 0; i < idsToReplace.length; i++) {
    const str = 'url(#' + idsToReplace[i][0] + ')';
    svg = svg.replace(
      new RegExp(regexEscape(str), 'g'),
      'url(#' + idsToReplace[i][1] + ')'
    );
  }

  return svg;
}

/**
 * Escape regex characters in given string
 * @param {string} str
 * @return {string}
 */
function regexEscape(str) {
  return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

Laruxo avatar Apr 01 '17 18:04 Laruxo

let DOMParser = require('xmldom').DOMParser;
let defs = new DOMParser().parseFromString('<defs></defs>');
let count = 0;

const config = {
  dest: 'public/svg',
  mode: {
    symbol: {
      dest: '.',
      sprite: 'sprite.svg',
      inline: true,
    },
  },
  shape: {
    transform: [
      gradientsExtraction,
      'svgo',
    ],
  },
  svg: {
    transform: [
      /**
        * Adds defs tag at the top of svg with all extracted gradients.
        * @param {string} svg
        * @return {string} svg
        */
      function(svg) {
        return svg.replace(
          '<symbol ',
          defs.firstChild.toString() + '<symbol '
        );
      },
    ],
  },
};

.... the rest of your code

Basically I have added the require and changed the transform function to not use the xmlserializer part, because it was to error prone (with the modules I have found).

func0der avatar Jul 21 '17 10:07 func0der

Any updates on this?

jkevingutierrez avatar May 03 '19 18:05 jkevingutierrez

Sharing this in case it helps anyone (most likely my future self). Snippet will add your defs trimming whitespace and new lines:

transform: [
  function (svg) {
    const defs = `
      <defs>
        <linearGradient id="gradient" x1="56.0849658%" y1="50%" x2="105.749837%" y2="128.905203%">
          <stop stop-color="#FFAE6C" offset="0%"></stop>
          <stop stop-color="#FF4E51" offset="100%"></stop>
        </linearGradient>
      </defs>
    `

    return svg
      .replace('<symbol', `${defs.split(/\n/).map((s) => s.trim()).join('')}<symbol`)
      .replace(/<symbol/gi, '<symbol fill="url(#gradient)"')
  },
],

hacknug avatar Mar 19 '20 18:03 hacknug

I ran into this issue as well recently (gradients not appearing in spritesheet SVGs), and also came up with a solution.

This just replaces all the individual <defs> with empty strings, and combines them into one <defs> block just before the first <symbol>. I haven't tested it with a particularly wide variety of SVGs, ~~it requires Node.js 15+ for the .replaceAll()~~, and the IDs end up not-particularly-minified, so others' mileage may vary.

svg: {
  transform: [
    function(svg) {
      let globalDefs = '';

      return svg
        .replace(/<defs>(.+?)<\/defs>/g, (_match, def) => { globalDefs += def })
        .replace('<symbol', `<defs>${ globalDefs }</defs><symbol`);
    },
  ],
},

Minor edit: Obviously, .replaceAll() wasn't needed over just .replace()!

burntcustard avatar Oct 14 '21 08:10 burntcustard

Any updates on this? Any alternative library?

max-arias avatar Dec 14 '23 17:12 max-arias