svgo icon indicating copy to clipboard operation
svgo copied to clipboard

convertTransform` does not apply translate(x, y) to `<text>`, `<circle>`, `<rect>`, etc

Open icai opened this issue 5 months ago • 3 comments

Summary

The convertTransform plugin currently applies transform="translate(...)" only to certain elements like <path>, but does not apply it to other valid SVG elements such as:

  • <text>
  • <circle>
  • <rect>
  • <tspan>
  • <g> (nested)

This results in inconsistent transform flattening and prevents full removal of transform from <g> containers in many real-world designs, particularly when working with Sketch or Illustrator-generated SVGs.

Example (Before)

<g transform="translate(200 188)">
  <rect width="100" height="80"/>
  <text><tspan x="30" y="40">Hello</tspan></text>
</g>

Expected (After)

<rect x="200" y="188" width="100" height="80"/>
<text transform="translate(200 188)"><tspan x="30" y="40">Hello</tspan></text>

Problem

In current output, convertTransform partially transforms the <rect> into absolute x, y, but leaves the <text> as-is or wrapped in residual transform. This breaks downstream animation or layout systems that expect flat coordinate-based SVGs without transform wrappers.

Suggestion

  • Extend convertTransform to support applying translations to:

    • x / y attributes (e.g. rect, circle, text)
    • cx / cy (e.g. circle)
    • Wrap text with its own transform if inline transform is not possible
    • Possibly recursively flatten nested <g> elements

This would significantly improve usability of SVGO for SVG animation and simplify DOM manipulation for frontend developers.


Version

SVGO v4.0.0

My Config

export default {
  multipass: true,
  js2svg: { indent: 2, pretty: true },
  plugins: [
    {
      name: 'removeViewBox',
      active: false
    },
    {
      name: 'removeXMLNS',
      active: false
    },
    {
      name: 'preset-default',
    },
    'collapseGroups',
    'convertShapeToPath',
    'convertPathData',
    'mergePaths',
    'convertTransform',
  ]
}

relative:

https://github.com/stadline/svg-flatten https://kurachiweb.github.io/svg-rect-to-path/ https://lean-svg.netlify.app/

icai avatar Jul 16 '25 13:07 icai

It's never meant to. convertTransform optimizes transform's representation. E.g. long sequence to a short matrix, or a matrix to even shorter variant (e.g. scale or skewX). Unfortunately, there is no plugin to apply transforms to basic shapes (only convertPathData does for paths). It's a longstanding issue, and not that hard to implement. So, PRs are welcome.

GreLI avatar Jul 16 '25 19:07 GreLI

Boy do I have a PR for you!

KTibow avatar Jul 16 '25 20:07 KTibow

@KTibow I make a simple version, 中国有句古话,“邪修总让人讨厌” 😂

export const applyTranslateTransform = {
  name: 'wrapTransformGroup',
  description: 'Wrap <g transform="translate(...)"> into outer <g> only if it has <rect> children',
  type: 'visitor',

  fn: () => {
    return {
      element: {
        enter: (node, parentNode) => {
          // Must be a <g> and contain transform="translate(...)"
          if (
            node.name !== 'g' ||
            !node.attributes?.transform ||
            !node.attributes.transform.startsWith('translate(')
          ) return;

          // Check if there is at least one <rect> child element
          const hasRect = (node.children || []).some(
            child => child.type === 'element' && ['rect', 'circle', 'ellipse'].includes(child.name)
          );

          if (!hasRect) return; // If no <rect>, do nothing

          // Create a new outer <g>
          const outerG = {
            type: 'element',
            name: 'g',
            attributes: {},
            children: [structuredClone(node)],
          };

          // Move transform to the inner <g> and remove from the original node
          outerG.children[0].attributes.transform = node.attributes.transform;
          delete node.attributes.transform;

          // Move the rest of node's attributes to outerG
          for (const [key, value] of Object.entries(node.attributes)) {
            outerG.attributes[key] = value;
          }

          // Replace node with outerG in the parent's children
          const index = parentNode.children.indexOf(node);
          if (index !== -1) {
            parentNode.children.splice(index, 1, outerG);
          }
        },
      },
    };
  },
};

icai avatar Jul 17 '25 10:07 icai