svgo icon indicating copy to clipboard operation
svgo copied to clipboard

Please add the option to flatten transforms

Open Emasoft opened this issue 7 years ago • 39 comments

"Flatten Transforms" option is available in SVG editors like Affinity Designer (~$40 / Mac) and it is very useful. It also improve performances of SVG rendering because coordinate transforms are precalculated.

ppg1q

Please add this option to SVGO.

Emasoft avatar Nov 20 '16 18:11 Emasoft

What does it do? If you mean applying transforms to paths then SVGO already does that when it possible.

GreLI avatar Nov 20 '16 18:11 GreLI

It flattens the transforms of all elements (including elliptical arcs, gradients, text and tspan) concatenating the cumulative transforms of all parents since the root node and applying the resulting matrix to the leaf elements (rects are converted to polygons to allow that). When not possible to completely eliminate the transform (for edge cases like some filters on groups), the children element is left with a transform matrix that includes all transformations of the parents, so that the matrix multiplications required to get the the element local coordinates are minimized.

Emasoft avatar Nov 20 '16 18:11 Emasoft

Well, SVGO does something of that.

GreLI avatar Nov 20 '16 18:11 GreLI

It does a very little of this, actually. Transforms in the output file are still too many compared to Affinity Designer exported svg file.

Emasoft avatar Nov 20 '16 18:11 Emasoft

It'd be more helpful if you provide such examples. Also, there was a bunch of bugs with incorrectly moved transforms, so one need to be careful with them.

GreLI avatar Nov 20 '16 19:11 GreLI

Here's some examples of what I think @Emasoft is talking about:

Input:

<svg>
    <g transform="translate(2.240000, 10.453333)">
        <rect fill="#000" x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733"/>
        <rect fill="#FFF" x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733"/>
    </g>
</svg>

Output:

<svg>
    <g transform="translate(2.24 10.453)">
        <rect x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733"/>
        <rect fill="#FFF" x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733"/>
    </g>
</svg>

Should be:

<svg>
    <rect x="212.80" y="104.812" width="22.811" height="33.2" rx="3.733"/>
    <rect fill="#FFF" x="212.80" y="104.812" width="22.811" height="33.2" rx="3.733"/>
</svg>

Input:

<svg>
    <g transform="translate(2.240000, 10.453333)">
        <rect fill="#000" x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733"/>
    </g>
</svg>

Output:

<svg>
    <rect x="210.56" y="94.359" width="22.811" height="33.2" rx="3.733" transform="translate(2.24 10.453)"/>
</svg>

Should be:

<svg>
    <rect x="212.80" y="104.812" width="22.811" height="33.2" rx="3.733"/>
</svg>

steadicat avatar Dec 06 '16 19:12 steadicat

Any update on this? This is a much needed feature because it allows to copy paste any xml node from an svg file to inline html without having to copy all parents nodes.

Emasoft avatar Mar 10 '17 22:03 Emasoft

I'm planning such an operation, but no updates yet.

GreLI avatar Mar 11 '17 16:03 GreLI

+1

mattvenn avatar Jun 27 '17 16:06 mattvenn

How long before this is implemented? This is a much needed feature. Too much time wasted in doing this manually every day.

Emasoft avatar Jun 27 '17 17:06 Emasoft

I'm afraid not so soon. It's trivial enough, but I have no time for this.

GreLI avatar Jun 29 '17 07:06 GreLI

@GreLI I hope you get time to get this important feature implemented very sooooooooooooon. 😊

nashwaan avatar Jul 16 '17 04:07 nashwaan

would also be useful to convert svg graphics to simple paths, which can easily be used in react-native-svg

macrozone avatar Sep 20 '17 17:09 macrozone

seems theres an existing package https://github.com/stadline/svg-flatten to do it, maybe someone could transform this into an svgo plugin? btw the package does not seem to handle filters, maybe we should add a clause in the recursion to flatten the transform onto the filter as well. besides filters, are there any other cases where we might need to recurse transform flattening beyond a nodes children? masks maybe?

irisjae avatar Sep 23 '17 07:09 irisjae

I wrote code to flatten transforms for my own project, in case anyone wants to port it over. It doesn't quite support all possible transforms on all shapes yet, but it's a start.

steadicat avatar Sep 24 '17 18:09 steadicat

Another day, another starting point, my fork https://github.com/lemnis/svgo has support of removing translate of a select group of properties. It also has the tests of above mentioned code written by @steadicat, currently my code fails most of the tests and I am not planning to improve my code in the near future.

lemnis avatar Nov 08 '17 03:11 lemnis

Almost a year passed and we are still waiting. 😞

nashwaan avatar Nov 08 '17 03:11 nashwaan

@nashwaan Be grateful that other people are willing to use their precious time to create and extend code that everybody can use. Otherwise, you have 2 options, pay lots of money more for all software you use or create the code yourself.

Or maybe the last solution is the best for you, keep waiting patiently. 😉

lemnis avatar Nov 08 '17 04:11 lemnis

@lemnis Thank you for educating me and telling me I should be grateful to the open source community.

nashwaan avatar Nov 08 '17 06:11 nashwaan

I've had the same issue, it would be really nice to be able to flatten everything.

saivan avatar May 13 '18 10:05 saivan

+1 this would be awesome.

cheshrkat avatar Jul 04 '18 06:07 cheshrkat

I have this issue too, and I'd swear it used to work better. But currently SVGO doesn't "flatten" or precalculate even very simple transforms. I have examples where there's a transform translate of 1,1 on a group and even that isn't recalculated.

One very common source of unnecessary transformations stems from the behavior of design software for working with SVGs. They tend to be very literal. If you click the "path" tool, you get a path. If you click the "circle" tool, you get a circle. And if you draw an icon with some shapes, group them, then move it down with the arrow key 4 times and to the right 8 times, you'll end up with a group that has a transform translate of 8, 4. (This is true of every single editor I've used: Illustrator, Sketch, Inkscape, etc--though some of them offer ways to flatten the transforms. Sketch, however, [which we use] doesn't.)

As the OP mentions, this is just noise--there's no reason whatsoever to preserve a transformation of that kind and so files that preserve this are missing an obvious if relatively minor optimization.

But in my situation this leads to real issues. I have a use case where we need to convert our icons to Android's Vector Drawable format. We work with SVG as the "origin" format and convert to Vector Drawable after optimizing with SVGO. Vector Drawables are a little touchy, it seems (we had issues if we optimized away leading 0s), and there aren't many tools available to automate this conversion.

And, it turns out, these preserved translations also cause issues. They're unsupported or just ignored in the conversion process and result in the icon being mis-aligned and cut off relative to the viewbox.

Example SVGs, before and after

Before

<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
    <title>icon/UI 24px/Vision Small (eye)</title>
    <desc>Created with Sketch.</desc>
    <defs></defs>
    <g id="icon/UI-24px/Vision-Small-(eye)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
        <g id="Export" transform="translate(0.750000, 6.000000)" stroke="#000000" stroke-width="1.5">
            <circle id="Oval" cx="11.1290323" cy="5.93548387" r="5.86870968"></circle>
            <path d="M11.25,2.69999981 C13.0693329,2.69999981 14.5441935,4.1748605 14.5441935,5.99419336" id="Shape"></path>
            <path d="M22.25,6 C19.7671057,2.32106779 15.5674223,0.0515191927 11.1290323,0.0515191927 C6.69064219,0.0515191927 2.54224915,2.25655166 0.0593548387,5.93548387 C2.34284932,9.31404472 6.0448217,11.4641714 10.110942,11.7735029 C14.1770623,12.0828345 18.0141602,10.4943237 20.93,7.52322581 C21.3881491,7.05639433 21.8281491,6.5486524 22.25,6 Z" id="Shape"></path>
        </g>
    </g>
</svg>

After

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" display="block" pointer-events="none" viewBox="0 0 24 24">
  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" transform="translate(.75 6)">
    <circle cx="11.129" cy="5.935" r="5.869"/>
    <path d="M11.25 2.7a3.294 3.294 0 0 1 3.294 3.294M22.25 6C19.767 2.321 15.567.052 11.129.052A13.355 13.355 0 0 0 .059 5.935a13.355 13.355 0 0 0 10.052 5.839c4.066.309 7.903-1.28 10.819-4.25.458-.468.898-.975 1.32-1.524z"/>
  </g>
</svg>

Before

<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
    <title>icon/Display 32px/Vision (eye)</title>
    <desc>Created with Sketch.</desc>
    <defs></defs>
    <g id="icon/Display-32px/Vision-(eye)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
        <g id="Export" transform="translate(1.000000, 8.000000)" stroke="#000000" stroke-width="1.5">
            <circle id="Oval" cx="15" cy="8" r="7.91"></circle>
            <path d="M15,3.56 C17.4521443,3.56 19.44,5.54785571 19.44,8" id="Shape"></path>
            <path d="M30.3862847,8.58371311 C30.3073441,8.50705295 30.2439761,8.42555519 30.1961806,8.33921984 C30.1483851,8.25288448 30.0563249,8.1398112 29.92,8 C26.5734903,3.04143919 20.9821779,0.0694389119 15,0.0694389119 C9.01782208,0.0694389119 3.42650973,3.04143919 0.08,8 C3.15775344,12.5537124 8.14736837,15.4517092 13.6277914,15.8686344 C19.1082144,16.2855596 24.4788024,14.175719 28.21,10.14 C28.2774066,10.0677786 28.3279616,10.0136126 28.3616649,9.97750186 C28.3901838,9.94694589 28.4422526,9.89716988 28.5178711,9.82817383" id="Shape"></path>
        </g>
    </g>
</svg>

After

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" display="block" pointer-events="none" viewBox="0 0 32 32">
  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" transform="translate(1 8)">
    <circle cx="15" cy="8" r="7.91"/>
    <path d="M15 3.56A4.44 4.44 0 0 1 19.44 8m10.946.584a1.052 1.052 0 0 1-.19-.245c-.048-.086-.14-.2-.276-.339A18 18 0 0 0 .08 8a18 18 0 0 0 28.13 2.14l.152-.162c.028-.031.08-.08.156-.15"/>
  </g>
</svg>

Before

<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
    <title>icon/UI 24px/Pin (map marker, location)</title>
    <desc>Created with Sketch.</desc>
    <defs></defs>
    <g id="icon/UI-24px/Pin-(map-marker,-location)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Export" transform="translate(4.125000, 1.750000)" stroke="#000000" stroke-width="1.5">
            <path d="M15.75,7.875 C15.75,14.625 7.875,20.25 7.875,20.25 C7.875,20.25 0,14.625 0,7.875 C0,3.4875 3.4875,0 7.875,0 C12.2625,0 15.75,3.4875 15.75,7.875 Z" id="Shape"></path>
            <circle id="Oval" cx="7.875" cy="7.75" r="3.25"></circle>
        </g>
    </g>
</svg>

After

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" display="block" pointer-events="none" viewBox="0 0 24 24">
  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-width="1.5" transform="translate(4.125 1.75)">
    <path d="M15.75 7.875c0 6.75-7.875 12.375-7.875 12.375S0 14.625 0 7.875A7.827 7.827 0 0 1 7.875 0a7.827 7.827 0 0 1 7.875 7.875z"/>
    <circle cx="7.875" cy="7.75" r="3.25"/>
  </g>
</svg>

Before

<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
    <title>icon/Display 32px/Prescription Generic (pharmacy, medicine, bottle, Rx)</title>
    <desc>Created with Sketch.</desc>
    <defs></defs>
    <g id="icon/Display-32px/Prescription-Generic-(pharmacy,-medicine,-bottle,-Rx)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Export" transform="translate(6.000000, 1.750000)" stroke="#000000" stroke-width="1.5">
            <path d="M2.06,0.08 L17.94,0.08 C18.4253462,0.08 18.8908138,0.272803018 19.2340054,0.61599459 C19.577197,0.959186163 19.77,1.42465382 19.77,1.91 L19.77,5 L0.229972639,5 L0.23,1.92 C0.22733834,1.43292351 0.4189657,0.964882708 0.762446184,0.619525281 C1.10592667,0.274167854 1.57291623,0.0799927277 2.06,0.08 Z" id="Shape" stroke-linejoin="round"></path>
            <path d="M0.23,13 L19.77,13" id="Shape" stroke-linejoin="round"></path>
            <path d="M19.77,24.5 L0.23,24.5" id="Shape" stroke-linejoin="round"></path>
            <path d="M2.75,7.24 L2.75,7.95 C2.75114043,8.94951168 2.22325652,9.89667602 1.45,10.53 C0.679451144,11.1611045 0.231855477,12.1039905 0.23,13.1 L0.23,28.5 L19.7700022,28.5 L19.77,13.1 C19.7711404,12.1004883 19.3232565,11.153324 18.55,10.52 C17.7767435,9.88667602 17.2488596,8.93951168 17.25,7.94 L17.25,7.24" id="Shape" stroke-linejoin="round"></path>
            <circle id="Oval-3" cx="10" cy="18.75" r="2.75"></circle>
        </g>
    </g>
</svg>

After

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" display="block" pointer-events="none" viewBox="0 0 32 32">
  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-width="1.5" transform="translate(6 1.75)">
    <path stroke-linejoin="round" d="M2.06.08h15.88a1.83 1.83 0 0 1 1.83 1.83V5H.23V1.92A1.83 1.83 0 0 1 2.06.08zM.23 13h19.54m0 11.5H.23M2.75 7.24v.71c.001 1-.527 1.947-1.3 2.58A3.33 3.33 0 0 0 .23 13.1v15.4h19.54V13.1a3.33 3.33 0 0 0-1.22-2.58c-.773-.633-1.301-1.58-1.3-2.58v-.7"/>
    <circle cx="10" cy="18.75" r="2.75"/>
  </g>
</svg>


Before

<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
    <title>icon/UI 24px/Search (magnifying glass, find, discover)</title>
    <desc>Created with Sketch.</desc>
    <defs></defs>
    <g id="icon/UI-24px/Search-(magnifying-glass,-find,-discover)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Export" transform="translate(1.000000, 1.000000)" stroke="#000000" stroke-width="1.5">
            <circle id="Oval" cx="8.5" cy="8.5" r="7.75"></circle>
            <path d="M21,21 L13.9821656,13.9821656" id="Shape"></path>
        </g>
    </g>
</svg>

After

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" display="block" pointer-events="none" viewBox="0 0 24 24">
  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-width="1.5" transform="translate(1 1)">
    <circle cx="8.5" cy="8.5" r="7.75"/>
    <path d="M21 21l-7.018-7.018"/>
  </g>
</svg>

SVGO config
const hash = require('string-hash');

module.exports = {
  full: true,
  multipass: true,
  precision: 3,
  // order of plugins is important to correct functionality
  plugins: [
    { removeDoctype: true },
    { removeXMLProcInst: true },
    { removeComments: true },
    { removeMetadata: true },
    { removeXMLNS: false },
    { removeEditorsNSData: true },
    { cleanupAttrs: true },
    { inlineStyles: true },
    { minifyStyles: true },
    { convertStyleToAttrs: true },
    { cleanupIDs: true },
    {
      prefixIds: {
        prefix: function(element, filePath) {
          const fileNameId = hash(
            filePath
              .split('/')
              .slice(-2)
              .join('/')
          );
          return `i${fileNameId}`;
        }
      }
    },
    { removeRasterImages: true },
    { removeUselessDefs: true },
    { cleanupNumericValues: true },
    { cleanupListOfValues: true },
    { convertColors: { currentColor: true } },
    { removeUnknownsAndDefaults: true },
    { removeNonInheritableGroupAttrs: true },
    { removeUselessStrokeAndFill: true },
    { removeViewBox: false },
    { cleanupEnableBackground: true },
    { removeHiddenElems: true },
    { removeEmptyText: true },
    { convertShapeToPath: true },
    { moveElemsAttrsToGroup: true },
    { moveGroupAttrsToElems: true },
    { collapseGroups: true },
    { convertPathData: true },
    { convertTransform: true },
    { removeEmptyAttrs: true },
    { removeEmptyContainers: true },
    { mergePaths: true },
    { removeUnusedNS: true },
    { sortAttrs: true },
    { removeTitle: true },
    { removeDesc: true },
    { removeDimensions: false },
    { removeAttrs: false },
    { removeElementsByAttr: false },
    { addClassesToSVGElement: false },
    { removeStyleElement: true },
    { removeScriptElement: true },
    {
      addAttributesToSVGElement: {
        attributes: [{ display: 'block' }, { 'pointer-events': 'none' }]
      }
    }
  ],
  js2svg: {
    pretty: false,
    indent: ''
  }
};

Could be this works better with some configurations than others, but I'm sure we're all aware that SVGO can be a bit challenging to configure, so if anyone has some tips on that, please share, it could help in some cases.

(One configuration possibility that I tried was not doubling up on moveElemsAttrsToGroup and moveGroupAttrsToElems, but enabling either option alone didn't help flatten any of these. I don't 100% guarantee I didn't make a mistake on that experiment, because all 3 ways ended up with the transform on the group [though other attributes did move], which strikes me as a little odd.)

morewry avatar Aug 17 '18 23:08 morewry

@Emasoft, recently I made this repo, just like SVGOMG but with Vue. I added flatten svg option, whose code was provided by Timo in his gist (with some modifications). It is still in experimental stage. It worked on the transforms of most of the paths (except some text and clipaths). So can anyone check that option and give your opinions.

Thank you all.

upendra-web avatar Sep 29 '18 10:09 upendra-web

@upendra-web Superb work with lean-SVG !

andrewrcollins avatar Jan 10 '19 04:01 andrewrcollins

@upendra-web Superb work with lean-SVG !

Thank you very much @andrewrcollins

upendra-web avatar Jan 12 '19 09:01 upendra-web

Lean-SVG still doesn't work with rect and circle. It's been 3 years, and I can see you put an enormous amount of effort in SVGO, so why not finish it with transform applying?

MarkJeronimus avatar Oct 22 '19 10:10 MarkJeronimus

If you are using Inkscape:

  1. Select everything and ungroup
  2. Save as "Optimised Svg (*.svg)"

In all cases I have tried, this has removed any transform attributes, then I can run through SVGO without having to worry about transforms. Not sure if it works for all SVG, but certainly the ones I have tried so far.

maxwell8888 avatar Feb 05 '20 20:02 maxwell8888

@maxwell8888 this works well for some elements, but not all unfortunately.

Some very tricky ones are:

  • linear gradients
  • masks
  • clones (sometimes it is preferable to maintain as clones)

I realise that the status of this is not likely to change, but I would love to know if it does (or if anyone finds a suitable workaround).

user98765446 avatar Nov 23 '20 04:11 user98765446

I've been able to apply transforms to path elements manually using stadline/svg-flatten provided that the transform attribute was associated with the path element itself (and not a group) by running in the nodejs interpreter (inside the project directory):

var flatten = require('./')
var svg = fs.readFileSync('./logo.svg')

flatten(svg).transform().value().toString()

However, this doesn't work as far as preserving the gradients if they have been defined with gradientUnits="userSpaceOnUse". If such a gradient is specified as a fill then the gradient also needs to have the transform applied to it. I imagine this applies to patterns as well.

specious avatar Jul 12 '21 19:07 specious

I really hope this eventually comes in SVGO, I'm relying on Lean-SVG for my day-to-day work right now, but I'd love SVGO to handle everything. I think most of the debate earlier was about the reliability of the transforms, but as long as it's clear that it's an opt-in one, I can't see why not.

davidwebca avatar Jul 16 '21 11:07 davidwebca