fogbound.net




Tue, 7 Dec 2021

Manipulating SVG files in PHP, Part 1.

— SjG @ 9:10 am

Now, before I even start, I have to admit this is a weird idea. I’m sure there are better languages and libraries for this stuff. But I’ll explain the rationale and then go into the details.

I’ve been creating a lot of designs for cards and other things which I cut out of paper with a Silhouette Cameo or out of other materials using the laser cutter at CrashSpace. I sell some of these designs at Etsy.com. To streamline the process of making files available in a variety of formats and posting them for sale at Etsy, I’ve written myself some PHP scripts that manipulate files, build thumbnails, and bundle stuff together and submit them for sale via Etsy’s API. Why PHP? The short answer is that it’s the language I use most on a day-to-day basis for work, and I have a lot of experience using it to manipulate images and interact with APIs. Are there languages that would be better suited? Probably, but the point of these scripts is not to be elegant. They just have to be something I can maintain, extend, and use to get the job done.

Anyway, I don’t know exactly how people will use the design files or what software they’ll use, so to make things flexible for them, I export the different layers of a file separately and in combination, e.g., a file with the outline, a file with the scoring lines, a file with the design, and a file combining all of the elements. To do this, I have a naming convention for the layers that I create in Affinity Designer, and use those names to manipulate the layers.

So, for example, start with a card with three layers: Outline, Score, and Design. I export it as an SVG file from Designer. There are a few settings that make the easier to manipulate later: especially “flatten transforms” and “add line breaks”.

SVG export settings

SVG files are just fancy text files. Consider the following sample image:

Sample card with outline, score, and design layers

If you open up this SVG file in a text editor, you’ll see the source. It’s got a header with some file details like viewport size and defaults for rendering lines and shapes. Each layer from Designer is represented in the file as a group (the <g> tag), and each group contains paths with the actual geometry:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="3300px" height="2550px" version="1.1" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
     xmlns:serif="http://www.serif.com/"
     style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
    <g id="Score">
        <path d="M1650.09,152.011L1650.09,2246.78" style="fill:none;stroke:black;stroke-
width:0.42px;stroke-dasharray:1.25,4.17,0,0;"/>
    </g>
    <g id="Outline">
        <path d="M150,150L3150,150" style="fill:none;stroke:black;stroke-width:4.63px;"/>
        <path d="M3150,150L3150,2250" style="fill:none;stroke:black;stroke-width:4.63px;"/>
        <path d="M150,150L150,2250" style="fill:none;stroke:black;stroke-width:4.63px;"/>
        <path d="M150.124,2250.07L3150.12,2250.07" style="fill:none;stroke:black;stroke-width:4.63px;"/>
    </g>
    <g id="Design">
        <path d="M2594.13,912.644L2594.13,524.494L2087.86,524.494L2087.86,1030.77L2387.83,1030.77L2387.83,
1362.85L2838.03,1362.85L2838.03,912.644L2594.13,912.644Z" style="fill:rgb(128,128,128);stroke:black;
stroke-width:1.46px;"/>
    </g>
</svg>

One of the things my scripts do is create “hairlined” versions of the file. This makes sure all shapes are unfilled and have very thin border strokes. Since the SVG file is just text, I can manipulate it with same kinds of tools I’d use to manipulate any text file. If you’re as demented as I am, this includes regular expressions, which is what I use for the hairlining:

$svg = file('original.svg');
foreach ($svg as $tlidx => $tl)
{
   if (preg_match_all('/fill:((?!none)[^";]+)/', $svg[$tlidx], $matches))
   {
      foreach ($matches[1] as $tm)
         $svg[$tlidx] = str_replace($tm, 'none', $svg[$tlidx]);
   }
   if (preg_match_all('/stroke-width:([^";]+)/', $svg[$tlidx], $matches))
   {
      foreach ($matches[1] as $tm)
         $svg[$tlidx] = str_replace('stroke-width:' . $tm, 'stroke-width:1px', $svg[$tlidx]);
   }
}
file_put_contents('converted.svg',implode("\n",$svg));

This is crude but effective. It replaces every fill style with “none,” and converts every stroke-width to a single pixel width. Using regular expressions is generally a fool’s errand, but this particular set seems to work pretty reliably.

One of the other things that my scripts do is take the source file with all three layers, and save versions with fewer layers. For example, one file contains only layers that will be cut rather than scored. This is where the specifics of the SVG format gets interesting. It turns out that the SVG file format is based on XML, thus we have a wealth of tools at our disposal to process them. In this case, we will use the power of PHP’s dreaded DOMDocument to manipulate the SVG file.

Here’s how to go through and remove the “Score” layer from the file.

$remove = [];
$dom = new DOMDocument();
$dom->load('original.svg');
$groups = $dom->getElementsByTagName('g');
foreach ($groups as $this_group)
{
    $layer_name = $this_group->getAttribute('id');
    if (!strcasecmp($layer_name, 'Score'))
    {
        array_push($remove, $this_group);
    }
}
foreach ($remove as $this_removal)
{
    $this_removal->parentNode->removeChild($this_removal);
}
$res = $dom->saveXML();
$out = fopen("scoreless.svg", 'w');
fwrite($out, $res);
fclose($out);

In the SVG file, the layer name is stored in the group’s id attribute, so the code iterates through the groups in the and uses that attribute to identify the layer it wishes to remove.

In the next post on this subject, I’ll discuss combining DOMDocument manipulations and string manipulation to do other fancy stuff like subtracting shapes from one another.


Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.