Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defs block for extracting gradients. #74

Open
jaywolters opened this issue Apr 20, 2015 · 11 comments
Open

Defs block for extracting gradients. #74

jaywolters opened this issue Apr 20, 2015 · 11 comments
Assignees
Milestone

Comments

@jaywolters
Copy link

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?

@jkphl jkphl added the question label Apr 20, 2015
@jkphl jkphl self-assigned this Apr 20, 2015
@jkphl
Copy link
Collaborator

jkphl commented Apr 20, 2015

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 jkphl closed this as completed Apr 20, 2015
@jaywolters
Copy link
Author

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.

@jkphl
Copy link
Collaborator

jkphl commented Apr 21, 2015

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 ...

@jaywolters
Copy link
Author

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.

@jkphl
Copy link
Collaborator

jkphl commented Apr 21, 2015

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!

@Laruxo
Copy link

Laruxo commented Apr 1, 2017

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, '\\$&');
}

@jkphl jkphl added this to the 2.x milestone May 30, 2017
@func0der
Copy link

func0der commented Jul 21, 2017

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).

@jkevingutierrez
Copy link

Any updates on this?

@hacknug
Copy link

hacknug commented Mar 19, 2020

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)"')
  },
],

@burntcustard
Copy link

burntcustard commented Oct 14, 2021

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()!

@max-arias
Copy link

Any updates on this? Any alternative library?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants