fogbound.net




Fri, 17 Dec 2021

Manipulating SVG in PHP, part 2.

— SjG @ 1:06 pm

As mentioned in Part 1, the purpose of this code is to help automate file format conversions and presentation for posting vector designs to Etsy. One kind of design I make is “shadow theaters,” which are essentially little layered dioramas that look nice when backlit.

Sample 8-Layer Shadow Theater

The individual layers of these designs comprise an outline, scoring lines for folding the paper, and the design itself. However, creating a thumbnail to represent the design has a unique challenge: the layers are negatives. The design file fills the areas that will be cut out, but this is where the greatest illumination will come through. I want the thumbnail to look like the photograph above. Furthermore, I can’t simply composite all the layers, as they are opaque, and the result would only show the top layer.

So the task becomes to invert the layers. But even that isn’t so simple. In each of the layers, I want to take the filled area and make it transparent, and take the unfilled area and fill it. To really make it work, the filled areas should not be a uniform color, but should vary by how far “down” in the stack the layer is.

However, this isn’t as simple as removing layers like I did in Part 1. What I have to do is take the outermost layer, fill it, and subtract the geometry of the inner design from it. Fortunately, that’s not so difficult to do in SVG. If you take a path in SVG and close it, you can continue to add sub-paths. When those sub-paths are outside the original path, they are added to it. If the sub-paths are inside the original path, they are subtracted from it. If they overlap, they’re both added and subtracted. This is probably better explained with pictures.

Disclaimer: for the purpose of this discussion, I’m assuming two SVG settings: fill-rule and clip-rule are set to “evenodd”. There are other options, but they aren’t helpful for what I want to do.

What do these paths and sub-paths look like? If you look at the example from Part 1, you can see that a path has a d attribute, which is a string of commands and coordinates. Rather than go through those here, I’ll just link the relevant Mozilla SVG tutorial page which I found helpful. The key thing is that a complete, closed path ends with a Z command. Any path details after that are a subpath.

Looking at an SVG file of one of my shadow gallery layers, what I want to do is convert the “Design” layer to a fully-contained sub-path of the “PaperOutline” layer. Based on what we know of SVG paths, that means I can just append the design path to the outline path – the design path will become a sub-path which will be subtracted from the outline path! I then can set the fill color of this composite path, and I will effectively have filled the outline and subtracted my design.

I again take advantage of the fact that the layer names are following my standard. I search through the file for the “Design” layer, and copy its path into the $subtract_path variable. Then I loop through the groups again, find the “PaperOutline” layer, and append the path. While I’m at it, I set the fill color with a shade I calculate based on the layer number.

<?php
$total_layer_count = 1;
for ($this_layer_index = 1; $this_layer_index <= $total_layer_count; $this_layer_index++)
{
    $doc = new DOMDocument();
    $doc->load("layer-{$this_layer_index}.svg");
    $shade = 255 * ($total_layer_count - $this_layer_index / 1.5) / $total_layer_count;
    $subtract_path = '';
    $rm = [];
    $groups = $doc->getElementsByTagName('g');
    foreach ($groups as $this_group)
    {
        $layer_name = $this_group->getAttribute('id');
        if (preg_match('/design/i', $layer_name))
        {
            foreach ($this_group->childNodes as $node)
            {
                if (in_array(get_class($node), ['DOMNode', 'DOMElement']))
                    $subtract_path .= $node->getAttribute('d');
            }
            array_push($rm, $this_group);
        }
    }
    foreach ($groups as $this_group)
    {
        $layer_name = $this_group->getAttribute('id');
        if (preg_match('/outlinepaper/i', $layer_name))
        {
            foreach ($this_group->childNodes as $node)
            {
                if (in_array(get_class($node), ['DOMNode', 'DOMElement']))
                {
                    $opath = $node->getAttribute('d');
                    $node->setAttribute('d', $opath . $subtract_path);
                    $style = $node->getAttribute('style');
                    if (preg_match_all('/fill:([^;]+)/', $style, $matches))
                    {
                        foreach ($matches[1] as $tm)
                        {
                            $style = str_replace($tm, "rgb($shade,$shade,$shade)", $style);
                        }
                    }
                    $node->setAttribute('style', $style);
                }
            }
        }
    }
    foreach ($rm as $trm)
    {
        $trm->parentNode->removeChild($trm);
    }
    $res = $doc->saveXML();
    $out = fopen("layer-{$this_layer_index}-inverted.svg", 'w');
    fwrite($out, $res);
    fclose($out);
}

Not the most elegant code in the world, and will certainly fail on SVG files that don’t match my naming and layout conventions, but illustrates the process.

Once I’ve created these inverted SVGs, I convert them into PNGs, composite the stack into a single image, and come up with what I consider a handsome representation for my design thumbnail.

Composited, complete

Once I have the ability to take layers and change the transparent areas and the fill color, I can do all sorts of other manipulations. For example, I can create striking negative images, or feed the the layers into other scripts to create “exploded” views of the shadow theaters.


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.


Wed, 22 Sep 2021

Legacy Code / Image Gallery

— SjG @ 10:36 am

One site I help manage uses WordPress and an image gallery plugin by Huge-It. Following a recent WordPress update, the gallery’s admin side has stopped allowing the admin to add images to a gallery. There’s a button, you click it, and nothing happens.

There is no obvious issue, until you look at the Javascript console and see the following error message: this._addClass is not a function. A quick web search confirms that this is likely either a conflict in Javascript libraries included by various plugins, or a jQuery version conflict.

The quick and dirty solution is to edit wp-content/plugins/gallery-and-caption/includes/admin/class-photo-gallery-wp-admin-functions.php and add the following lines:

function custom_admin_js() {
echo '"<script type="text/javascript" src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>"';
}
add_action('admin_footer', 'custom_admin_js');

This basically forces the plugin to use a compatible version of jQuery so it’ll work again.

Hooray! The forces of entropy are held in check for another brief moment.


Fri, 9 Jul 2021

Virtual Apache SSL Hosts in a Docker Container

— SjG @ 1:40 pm

You might want to “containerize” your development hosting environment so you can easily migrate it from machine to machine. As a Docker noob, I had a bunch of issues getting this set up the first time, and wanted to share a working configuration. This example assumes you have Docker installed and operating. You can also skip reading this, and just download the files at GitHub.

First, we’ll need to create some directories. I create one for the Apache configurations, and one for the code projects I’ll be working on.
mkdir apache-php
mkdir project

Within project, you can check out the code for your various projects into subdirectories. For simplicity, I’ve created project1 and project2 in the sample code. The Apache web server within the container will serve content from these directories.

We’re going to use a fictional top-level domain (TLD) for our development environment. This way, the URL you use to access your sites will be the same every time you spin up a new dev environment, without having to worry about name servers. You do, however, have to worry about your /etc/hosts file (or your platform’s equivalent). Choose a TLD that will be easy to remember. For my example, I’m using “mylocal”. One thing to avoid: start the TLD with a letter rather than a number. Don’t include characters like hyphens. Please learn from my mistakes.

Edit your /etc/hosts, and add the lines:
127.0.0.1 project1.mylocal
127.0.0.1 project2.mylocal

Next, we’ll want to create an SSL certificate for use in development. The easiest way to do this is with mkcert. Once you have mkcert installed and working, you’ll create a wildcard certificate for your TLD:
cd php-apache
mkcert -install mylocal "*.mylocal"

Next, we create a compose.yaml file:

version: "3.9"
services:
php:
container_name: ApachePHPVirtual
networks:
- apache
build:
context: .
dockerfile: PhpApacheDockerfile
volumes:
- "./project:/var/www"
ports:
- 80:80
- 443:443
extra_hosts:
- "project1.mylocal:127.0.0.1" # remember to add "127.0.0.1 project1.mylocal" to your /etc/hosts file or equivalent
- "project2.mylocal:127.0.0.1" # remember to add "127.0.0.1 project2.mylocal" to your /etc/hosts file or equivalent
hostname: project1.mylocal # default
domainname: mylocal
tty: true # if you want to debug
networks:
apache:

This is pretty straightforward. We’re creating a container which we’ll call “ApachePHPVirtual,” it will have a network we call “apache” if we want to connect using other containers, and it links our top level project directory to /var/www in the container. We map ports 80 and 443 on our host machine to those same ports in the container. The extra_hosts directive adds our project names to the container’s /etc/hosts. We set up the container’s hostname to match our first project, and set the default domain to our “mylocal” TLD.

We then want to create configurations for each of the Apache virtual hosts. In the php-apache directory, we create config files for each project. These are just standard virtual host declarations, e.g.:

<VirtualHost *:80>
    ServerName project1.mylocal
    Redirect permanent / https://project1.mylocal/
</VirtualHost>
<VirtualHost *:443>
    ServerName project1.mylocal
    DocumentRoot /var/www/project1
    ErrorLog ${APACHE_LOG_DIR}/project1-error.log
    CustomLog ${APACHE_LOG_DIR}/project1-access.log combined
    DirectoryIndex index.php

    <Directory "/var/www/project1">
        Options -Indexes +FollowSymLinks
        AllowOverride all
        Order allow,deny
        Allow from all
    </Directory>

    SSLEngine On
    SSLCertificateFile    /etc/apache2/ssl/cert.pem
    SSLCertificateKeyFile /etc/apache2/ssl/cert-key.pem
</VirtualHost>

You’ll need to create a similar configuration for each project. Note that the Document Root points at the mapped host directory. That means you won’t need to rebuild the container to see project changes.

The actual image for the Apache/PHP container is created and configured in our next file, “PhpApacheDockerfile”. So we create that:

FROM php:8.0.8-apache-buster

# add some packages
RUN docker-php-ext-install curl gd iconv pdo pdo_mysql soap zip

# Apache Config
COPY php-apache/project1.conf /etc/apache2/sites-available/project1.conf
COPY php-apache/project2.conf /etc/apache2/sites-available/project2.conf
COPY php-apache/mylocal+1-key.pem /etc/apache2/ssl/cert-key.pem
COPY php-apache/mylocal+1.pem /etc/apache2/ssl/cert.pem

# mod rewrite! SSL!
RUN a2enmod rewrite
RUN a2enmod ssl

# enable sites
RUN a2ensite project1.conf
RUN a2ensite project2.conf
RUN service apache2 restart

This pulls the php-8.0.8 image from DockerHub, adds in some PHP extensions, copies over our SSL certificate and key, copies our virtual host configuration files over, enables the projects, and restarts the Apache server.

Now, all that remains to do is build it and power it up:

docker-compose build && docker-compose up -d

You can now visit the project URls in your browser, e.g., https://project1.mylocal/


Sat, 12 Sep 2020

Those pesky /usr/local/include headers under MacOS Catalina

— SjG @ 11:04 am

Here’s another of those “system upgrade moves stuff around” problems. My work iMac seems to be suffering a slow disk failure. It gets slower and slower as it tries to run.

Queue restoring TimeMachine backups onto a new iMac. A lot of stuff just works. But things like building xsendfile for the Apache development server under Mac ports threw lots of errors. The compiler couldn’t find the headers:

mod_xsendfile.c fatal error: stdio.h: no such file or directory

There are lots of suggestions out there to run xcode-select --install, but I’d already done that.

Turns out that XCode has stopped storing the SDKs in /Library/Developer/CommandLineTools/SDKs and moved them to /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs. I spent a bit of time trying to figure out how to pass that path to the Mac Ports version of apxs2 to use the new include path, but eventualy gave up and just did the hacky thing:
ln -s /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs /Library/Developer/CommandLineTools/SDKs

Another issue, just so I rememebr next time, was that the web server and the command line were mysteriously running different versions of PHP. The key, of course, was that I had forgotten to run port select, e.g., for this project, I needed port select php php70.