0

How do I remove all transform from text elements in an SVG while keeping the text elements and the font size in place?

This question is similar to Removing transforms in SVG files, but this question her specifically addresses text elements.

I already tried the usual trick to ungroup everything, cut everything, delete the current layer and past everything again in place. But the transforms are kept. I also tried and installed the Inkscape extension https://github.com/Klowner/inkscape-applytransforms. However it only works for paths not for text elements.

Note, I must not convert the text elements into paths, because I need the SVG as a template for FreeCAD and FreeCAD requires the <svg:text> element in order to replace the text content.

I can also provide a sample file.

1 Answer 1

0

From the perspective of a graphic application it makes sense to prefer/keep transformations as long as possible when manipulating elements.

Otherwise the application would constantly need to recalculate multiple attributes like

  • x,y, dx, dy
  • width, height
  • font-size, line-height
  • stroke-width

etc

Keep in mind these recalculations would quickly lead to rounding errors – so editing your elements will soon lead to a loss of information.

Besides, <text> elements don't have a seperate attribute for rotation – yes, there is actually rotate but it's actually just a shorthand for a transformation.

JavaScript transformation converter

Provided your text is not rotated, you could recalculate transformations like translate, scale to harcoded presentation attribute values.

svg{
  border: 1px solid #ccc;
  width:10em;
}

textarea{
  width:100%;
  min-height:20em;
}
<p><button onclick="removeTextTransforms();">Remove text transforms</button></p>
<svg id="svg" viewBox="0 0 100 100">
  <text x="10" y="50" font-size="10" paint-order="stroke" font-family="sans-serif" stroke="red" stroke-width="2" transform="scale(1.2) translate(10, 20)">Text</text>
</svg>

<textarea id="output"></textarea>

<script>
  function removeTextTransforms() {
    let texts = document.querySelectorAll('text');
    texts.forEach(el => {
      let parent = el.farthestViewportElement;
      // check elements transformations
      let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
      let {
        a,
        b,
        c,
        d,
        e,
        f
      } = matrix;
      // round matrix
      [a, b, c, d, e, f] = [a, b, c, d, e, f].map(val => {
        return +val.toFixed(3)
      });
      let matrixStr = [a, b, c, d, e, f].join('');
      let isTransformed = matrixStr !== "100100" ? true : false;
      if (isTransformed) {
        // matrix to readable transform functions
        let transObj = qrDecomposeMatrix(matrix);
        let scale = (transObj.scaleX + transObj.scaleY) / 2;
        // convert transforms to attributes
        let {
          translateX,
          translateY,
          rotate
        } = transObj;
        let x = +el.getAttribute('x')
        let y = +el.getAttribute('y')
        let fontSize = +el.getAttribute('font-size')
        if (rotate) {
          return false
        } else {
          // scale stroke-width
          scaleStrokeWidthText(el, scale);
          el.setAttribute('x', (x * scale + translateX));
          el.setAttribute('y', (y * scale + translateY));
          el.setAttribute('font-size', (fontSize * scale));
          el.removeAttribute('transform');
        }
      }
    });
    
    output.value = new XMLSerializer().serializeToString(svg)
  }

  function scaleStrokeWidthText(el, scale) {
    let styles = window.getComputedStyle(el);
    let strokeWidth = styles.getPropertyValue('stroke-width');
    let stroke = styles.getPropertyValue('stroke');
    strokeWidth = stroke != 'none' ? parseFloat(strokeWidth) * scale : 0;
    el.setAttribute('stroke-width', strokeWidth);
    el.style.removeProperty('stroke-width');
  }
  /**
   *  Decompose matrix to readable transform properties 
   *  translate() rotate() scale() etc.
   *  based on @AndreaBogazzi's answer
   *  https://stackoverflow.com/questions/5107134/find-the-rotation-and-skew-of-a-matrix-transformation#32125700
   *  return object with seperate transform properties 
   *  and ready to use css or svg attribute strings
   */
  function qrDecomposeMatrix(matrix, precision = 3) {
    let {
      a,
      b,
      c,
      d,
      e,
      f
    } = matrix;
    // matrix is array
    if (Array.isArray(matrix)) {
      [a, b, c, d, e, f] = matrix;
    }
    let angle = Math.atan2(b, a),
      denom = Math.pow(a, 2) + Math.pow(b, 2),
      scaleX = Math.sqrt(denom),
      scaleY = (a * d - c * b) / scaleX,
      skewX = Math.atan2(a * c + b * d, denom) / (Math.PI / 180),
      translateX = e ? e : 0,
      translateY = f ? f : 0,
      rotate = angle ? angle / (Math.PI / 180) : 0;
    let transObj = {
      translateX: translateX,
      translateY: translateY,
      rotate: rotate,
      scaleX: scaleX,
      scaleY: scaleY,
      skewX: skewX,
      skewY: 0
    };
    let cssTransforms = [];
    let svgTransforms = [];
    for (let prop in transObj) {
      transObj[prop] = +parseFloat(transObj[prop]).toFixed(precision);
      let val = transObj[prop];
      let unit = "";
      if (prop == "rotate" || prop == "skewX") {
        unit = "deg";
      }
      if (prop.indexOf("translate") != -1) {
        unit = "px";
      }
      // combine these properties
      let convert = ["scaleX", "scaleY", "translateX", "translateY"];
      if (val !== 0) {
        cssTransforms.push(`${prop}(${val}${unit})`);
      }
      if (convert.indexOf(prop) == -1 && val !== 0) {
        svgTransforms.push(`${prop}(${val})`);
      } else if (prop == "scaleX") {
        svgTransforms.push(
          `scale(${+scaleX.toFixed(precision)} ${+scaleY.toFixed(precision)})`
        );
      } else if (prop == "translateX") {
        svgTransforms.push(
          `translate(${transObj.translateX} ${transObj.translateY})`
        );
      }
    }
    // append css style string to object
    transObj.cssTransform = cssTransforms.join(" ");
    transObj.svgTransform = svgTransforms.join(" ");
    return transObj;
  }
</script>

You might also try this experimental codepen helper I recently created to strip as many (nested) transform as possible.

Not the answer you're looking for? Browse other questions tagged or ask your own question.