1

I have the below drawing, made of random shapes with various number of points, to which I can add, through the following XSLT, textboxes. The solution proposed in this thread (i.e. x="50%" y ="50%" and dominant-baseline="middle" text-anchor="middle") does not work, as all such textboxes end up in the same position of the drawing, overlapping. I would actually like them to be in the center of each path they are named after. Here is the fiddle that shows the behaviour. I have already asked if this could be achieved through Javascript but, since the transformation would be made through a VBA macro, I have been advised that would not be the correct solution. Basically, I would need to populate the fields x and y with the average height and width of the paths each textbox should fit into, those text holders are created in this part of the code:

      <text x="" y="" id="{$id}-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000 " dominant-baseline="middle" text-anchor="middle">
        <tspan id="{$id}-tspan" x="" y="">
          <xsl:value-of select="$id"/>
        </tspan>
      </text>

SVG

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="exportSvg" width="400" height="400">
    <defs/>
    <rect width="400" height="400" transform="translate(0, 0)" fill="rgb(255, 255, 255)" style="fill:rgb(255, 255, 255);"/>
    <g>
        <g id="Drawing-svg" clip-path="url(#rect-mask-Drawing)">
            <clipPath id="rect-mask-Drawing">
                <rect x="0" y="0" width="400" height="400"/>
            </clipPath>
            <g id="chart-svg">
                <g id="svg-main" clip-path="url(#rect-mask-Main)">
                    <clipPath id="rect-mask-Main">
                        <rect x="0" y="0" width="400" height="400"/>
                    </clipPath>
                    <g id="Drawing-svg">
                        <g id="Parts-svg">
                            <g id="Section-svg">
                                <g id="Item1-svg">
                                    <path d="M 155.09357,45.542471 104.77897,86.931934 75,200 152.79121,141.87343 200,84.246354 Z" stroke="#000000" style="fill:#e6e6e6;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round" id="Item1"/>
                                </g>
                                <g id="Item2-svg">
                                    <path d="M 198.06872,89.614437 -9.21291,31.643703 -23.42303,34.67823 51.52002,20.68699 47.20879,-57.62707 z" stroke="#000000" style="fill:#e6e6e6;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round" id="Item2"/>
                                </g>
                                <g id="Item3-svg">
                                    <path d="M 161.0455,182.56778 -41.68122,-5.64443 15.98375,27.05111 67.62172,3.73783 32.80201,-13.55927 z" stroke="#000000" style="fill:#e6e6e6;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round" id="Item3"/>
                                </g>
                            </g>
                        </g>
                    </g>
                </g>
            </g>
        </g>
    </g>
</svg>

XSLT

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    exclude-result-prefixes="svg"
    version="1.0">
<xsl:output method="xml" encoding="utf-8" omit-xml-declaration="yes"/>
  
  <xsl:strip-space elements="*"/>
  
  <xsl:template match="svg:g[@id[starts-with(., 'Item')]]">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
      <xsl:variable name="id" select="substring-before(@id, '-')"/>
      <text x="" y="" id="{$id}-text" style="-inkscape-font-specification:'Calibri, Normal';font-family:Calibri;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000 " dominant-baseline="middle" text-anchor="middle">
        <tspan id="{$id}-tspan" x="" y="">
          <xsl:value-of select="$id"/>
        </tspan>
      </text>
    </xsl:copy>
  </xsl:template>
  

  
  
 <xsl:template match="processing-instruction('xml-stylesheet')"/>
 
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
Oran G. Utan
  • 455
  • 1
  • 2
  • 10

2 Answers2

1

I made an attempt, below.

This is actually quite fiddly to do in XSLT 1, because parsing the svg:path/@d value requires a lot of string processing and recursion.

I wrote a named template get-bounding-box-edge-value which you call with 2 parameters; a string containing a list of x,y coordinates, and a string specifying which edge you want to find (either 'TOP', 'BOTTOM', 'LEFT', or 'RIGHT'). The template calls itself recursively to process the list, and returns the minimum y coordinate for 'TOP', the maximum y for 'BOTTOM', the minimum x for 'LEFT', and the maximum x for 'RIGHT'.

Then when processing an svg:path, I call this template 4 times to retrieve the 4 edges that define the bounding box of the path, calculate the centre point from those 4 values, and position the svg:text element at that point, setting the dominant-baseline and text-anchor attributes so that the textual content of the svg:text is centred around that point.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" exclude-result-prefixes="svg" version="1.0">
   <xsl:output method="xml" encoding="utf-8" omit-xml-declaration="yes" indent="yes"/>
    
   <xsl:template name="get-bounding-box-edge-value">
      <xsl:param name="which-edge"/>
      <xsl:param name="coordinate-path"/>
      <xsl:variable name="next-coordinate-pair" select="substring-before($coordinate-path, ' ')"/>
      <xsl:variable name="remaining-coordinates" select="substring-after($coordinate-path, ' ')"/>
      <!-- get the next value (either an x or y coordinate) from the first of the list of coordinate pairs -->
      <xsl:variable name="next-value">
         <xsl:choose>
            <xsl:when test="$which-edge='TOP' or $which-edge='BOTTOM'">
               <!-- we want the Y coordinate -->
               <xsl:value-of select="substring-after($next-coordinate-pair, ',')"/>
            </xsl:when>
            <xsl:otherwise>
               <!-- we want the X coordinate -->
               <xsl:value-of select="substring-before($next-coordinate-pair, ',')"/>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:variable>
      <xsl:choose>
         <xsl:when test="$remaining-coordinates">
            <xsl:variable name="remaining-edge-value">
               <xsl:call-template name="get-bounding-box-edge-value">
                  <xsl:with-param name="which-edge" select="$which-edge"/>
                  <xsl:with-param name="coordinate-path" select="$remaining-coordinates"/>
               </xsl:call-template>
            </xsl:variable>
            <xsl:choose>
               <xsl:when test="$which-edge='TOP' or $which-edge='LEFT'">
                  <!-- we're calculating the minimum value (NB 0,0 is the upper-left corner) -->
                  <xsl:choose>
                     <xsl:when test="number($next-value) &lt; number($remaining-edge-value)">
                        <xsl:value-of select="$next-value"/>
                     </xsl:when>
                     <xsl:otherwise>
                        <xsl:value-of select="$remaining-edge-value"/>
                     </xsl:otherwise>
                  </xsl:choose>
               </xsl:when>
               <xsl:otherwise>
                  <!-- we're calculating the maximum value -->
                  <xsl:choose>
                     <xsl:when test="number($next-value) &gt; number($remaining-edge-value)">
                        <xsl:value-of select="$next-value"/>
                     </xsl:when>
                     <xsl:otherwise>
                        <xsl:value-of select="$remaining-edge-value"/>
                     </xsl:otherwise>
                  </xsl:choose>
               </xsl:otherwise>
            </xsl:choose>
         </xsl:when>
         <xsl:otherwise>
            <!-- there are no more coordinates in the path - this is the last coordinate pair -->
            <!-- so we just return the value taken from this coordinate pair -->
            <xsl:value-of select="$next-value"/>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>
    
   <xsl:template match="svg:g[starts-with(@id, 'Item')]">
      <!-- calculate the bounding box of the path by extracting the TOP, LEFT, RIGHT, and BOTTOM coordinate value-->
      <xsl:variable name="coordinate-path" select="translate(substring-after(svg:path/@d, 'M '), 'zZ', '')"/>
      <xsl:variable name="top">
         <xsl:call-template name="get-bounding-box-edge-value">
            <xsl:with-param name="which-edge">TOP</xsl:with-param>
            <xsl:with-param name="coordinate-path" select="$coordinate-path"/>
         </xsl:call-template>
      </xsl:variable>
      <xsl:variable name="left">
         <xsl:call-template name="get-bounding-box-edge-value">
            <xsl:with-param name="which-edge">LEFT</xsl:with-param>
            <xsl:with-param name="coordinate-path" select="$coordinate-path"/>
         </xsl:call-template>
      </xsl:variable>
      <xsl:variable name="bottom">
         <xsl:call-template name="get-bounding-box-edge-value">
            <xsl:with-param name="which-edge">BOTTOM</xsl:with-param>
            <xsl:with-param name="coordinate-path" select="$coordinate-path"/>
         </xsl:call-template>
      </xsl:variable>
      <xsl:variable name="right">
         <xsl:call-template name="get-bounding-box-edge-value">
            <xsl:with-param name="which-edge">RIGHT</xsl:with-param>
            <xsl:with-param name="coordinate-path" select="$coordinate-path"/>
         </xsl:call-template>
      </xsl:variable>
      <!-- calculate the coordinates of the centroid -->
      <xsl:variable name="center-x" select="(number($left) + number($right)) div 2"/>
      <xsl:variable name="center-y" select="(number($top) + number($bottom)) div 2"/>
      <xsl:copy>
         <xsl:apply-templates select="@* | node()"/>
         <xsl:variable name="id" select="substring-before(@id, '-')"/>
         <text x="50%" y="50%" id="{$id}-text" style="
            -inkscape-font-specification:'Calibri, Normal';
            font-family:Calibri;font-weight:normal;font-style:normal;
            font-stretch:normal;font-variant:normal;font-size:20px;font-variant-ligatures:normal;
            font-variant-caps:normal;font-variant-numeric:normal;
            font-variant-east-asian:normal;
            fill:#000000;
            text-align:center
         ">
            <tspan id="{$id}-tspan" x="{$center-x}" y="{$center-y}" dominant-baseline="middle" text-anchor="middle">
               <xsl:value-of select="$id"/>
            </tspan>
         </text>
      </xsl:copy>
   </xsl:template>
    
   <xsl:template match="@* | node()">
      <xsl:copy>
         <xsl:apply-templates select="@* | node()"/>
      </xsl:copy>
   </xsl:template>

</xsl:stylesheet>
Conal Tuohy
  • 2,561
  • 1
  • 8
  • 15
  • NB this won't parse any `path/@d` value in arbitrary SVG, but it handles your input data which only uses `M` (moveto) and `Z` (close) commands (and `z` which means the same as `Z`). I'm assuming from your comment about how the SVG is generated that this is probably safe. But for arbitrary paths you'd need to also handle other commands like `H` (horizontal lines), `C` (curves) etc, and that named template would need to be more complex. – Conal Tuohy Oct 27 '22 at 14:17
  • Thank you, I actually meant all kinds of paths, totally arbitrary and with Bezier curves, but since this was not clear in the code above, I accepted the answer. It is useful anyway in case of polygons, although in my case I will have to find a way to adjust it. Would it be very much more complicated? – Oran G. Utan Oct 28 '22 at 05:38
  • Now I realize I should have spent more time thinking about how to ask the question, there are many variables I did not consider explaining, should I reformulate or ask yet another one if I cannot manage? – Oran G. Utan Oct 28 '22 at 06:22
  • It would be a bit more complicated, yes. The tricky thing is parsing that `@d` "command" string, because its syntax has various optional features; e.g. you can say "M0,0 M0,1 Z" or you can say "M 0,0 0,1 z" and they mean the same thing (the second "M" command is redundant because it follows the earlier one). So that named template would need to have another parameter to specify the "current command" so that it knew what command was being executed in the case that it wasn't specified explicitly, and another two parameters to hold the "current position" so it could calculate relative coordinates – Conal Tuohy Oct 28 '22 at 09:23
  • (commands that are specified with lower case letters use relative coordinates, whereas upper case letters use absolute coordinates). The other commands have a different number of parameters, too; the horizontal and vertical line commands specify only a single point, and the bezier commands specify control points... that named template would probably double in size I'm guessing. – Conal Tuohy Oct 28 '22 at 09:27
0

The following templates transform a path like

<path d="M 155.09357,45.542471 104.77897,86.931934 75,200 152.79121,141.87343 200,84.246354 Z"/>

into a specification of its center like

<center x="137.5" y="122.7712355"/>

They make certain assumptions about the commas and spaces in the path, but these can easily be adapted. They also use the non-standard function node-set, which is supported by almost all XSLT 1.0 processors, just under different names, for example exslt:node-set.

<xsl:template match="path">
  <xsl:copy-of select="."/>  <!-- copy the <path> element -->
  <xsl:call-template name="bbox">
    <xsl:with-param name="path" select="substring-before(substring-after(@d,'M '),' Z')"/>
    <xsl:with-param name="bbox">
      <bbox x="10000" X="-10000" y="10000" Y="-10000"/>
    </xsl:with-param>
  </xsl:call-template>
</xsl:template>
<xsl:template name="bbox">
  <xsl:param name="path"/>
  <xsl:param name="bbox"/>
  <xsl:variable name="b" select="node-set($bbox)/*"/>
  <xsl:choose>
    <xsl:when test="$path">
      <xsl:variable name="x" select="number(substring-before($path,','))"/>
      <xsl:variable name="y" select="number(substring-after(substring-before($path,' '),','))"/>
      <xsl:variable name="next" select="substring-after($path,' ')"/>
      <xsl:call-template name="bbox">
        <xsl:with-param name="path" select="$next"/>
        <xsl:with-param name="bbox">
          <bbox>
            <xsl:attribute name="x">
              <xsl:choose>
                <xsl:when test="$x &lt; $b/@x">
                  <xsl:value-of select="$x"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:value-of select="$b/@x"/>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:attribute>
            <xsl:attribute name="X">
              <xsl:choose>
                <xsl:when test="$x &gt; $b/@X">
                  <xsl:value-of select="$x"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:value-of select="$b/@X"/>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:attribute>
            <xsl:attribute name="y">
              <xsl:choose>
                <xsl:when test="$y &lt; $b/@y">
                  <xsl:value-of select="$y"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:value-of select="$b/@y"/>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:attribute>
            <xsl:attribute name="Y">
              <xsl:choose>
                <xsl:when test="$y &gt; $b/@Y">
                  <xsl:value-of select="$y"/>
                </xsl:when>
                <xsl:otherwise>
                  <xsl:value-of select="$b/@Y"/>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:attribute>
          </bbox>
        </xsl:with-param>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <center x="{($b/@x + $b/@X) div 2}" y="{($b/@y + $b/@Y) div 2}"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

For your particular case, replace the <center> element with the text you want at these coordinates:

<xsl:variable name="id" select="substring-before(parent::g/@id,'-')"/>
<text x="{($b/@x + $b/@X) div 2}" y="{($b/@y + $b/@Y) div 2}" id="{$id}-text">
  <tspan id="{$id}-tspan" x="{($b/@x + $b/@X) div 2}" y="{($b/@y + $b/@Y) div 2}">
    <xsl:value-of select="$id"/>
  </tspan>
</text>
Heiko Theißen
  • 12,807
  • 2
  • 7
  • 31
  • thank you, forgive my ignorance, but how do I integrate this in the above? I tried putting before or after the block creating the textboxes but nothing happened, I also tried making another stylesheet that transforms the file resulting from the previous transormation. – Oran G. Utan Oct 26 '22 at 18:15
  • Replace the `
    ` element with whatever you want output for a given `` element: probably the path and the associated text element.
    – Heiko Theißen Oct 26 '22 at 18:25
  • I humbly admit I don't know how... I tried with `` but it's not the correct way it seems. – Oran G. Utan Oct 26 '22 at 19:20
  • See my augmented answer. – Heiko Theißen Oct 27 '22 at 05:29