21 releases

0.8.0 Oct 20, 2024
0.7.0 Jun 17, 2023
0.6.0 Jun 1, 2023
0.5.2 May 31, 2023
0.1.7 Nov 27, 2021

#265 in Encoding

Download history 2/week @ 2024-09-23 137/week @ 2024-10-14 43/week @ 2024-10-21 15/week @ 2024-11-04 396/week @ 2024-12-02

397 downloads per month

Custom license

4.5MB
4.5K SLoC

Rust 4K SLoC // 0.1% comments AsciiDoc 591 SLoC Shell 1 SLoC

Contains (WOFF font, 59KB) impact.woff2, (WOFF font, 59KB) assets/fonts/impact.woff2

Collagen — The Collage Generator

collagen

Collagen — from “collage” and “generate” (and because collagen is the protein that holds your body together; s/protein/tool/;s/body/images/) — is a program that takes as input a folder containing a JSON manifest file that describes the layout of any number of SVG elements (<rect>, <line>, <g>, etc.), image files (JPEG, PNG, etc.), other SVGs, and other Collagen folders, and produces as output a single SVG file with any external assets embedded. That is, it converts a textual description of an SVG and any provided assets into a single portable SVG file.

In addition to mapping the manifest directly into SVG, Collagen supports a number of features that make creating graphics more convenient for the author, such as variable assignment and interpolation into SVG attributes, a LISP-like language to evaluate mathematical expressions inside strings, “if” tags to conditionally generate elements, and “for-each” tags to generate sequences of elements. For instance, the following manifest will create a rainbow pinwheel.

collagen.json

{
    "$schema": "https://rben01.github.io/collagen/schemas/schema.json",
    "vars": {
        "width": 100,
        "height": "{width}",
        "n-spokes": 16,
        "cx": "{(/ width 2)}",
        "cy": "{(/ height 2)}",
        "spoke-length": "{(* width 0.75)}"
    },
    "attrs": {
        "viewBox": "0 0 {width} {height}"
    },
    "children": [
        {
            "for_each": {
                "variable": "i",
                "in": { "start": 0, "end": "{n-spokes}" }
            },
            "do": {
                "tag": "line",
                "vars": {
                    "theta": "{(* (/ i n-spokes) (pi))}",
                    "dx": "{(* (/ spoke-length 2) (cos theta))}",
                    "dy": "{(* (/ spoke-length 2) (sin theta))}"
                },
                "attrs": {
                    "x1": "{(+ cx dx)}",
                    "x2": "{(- cx dx)}",
                    "y1": "{(+ cy dy)}",
                    "y2": "{(- cy dy)}",
                    "stroke": "hsl({(* (/ i n-spokes) 360)}, 100%, 50%)"
                }
            }
        }
    ]
}

A rainbow pinwheel

Rationale

Creating and editing images is hard. Most of the difficulty is due to lack of programmatic facilities offered by image editing programs. (I must caveat this by admitting I have not used every graphics editing program on the market. But the ones I have tried fall short.) Collagen aims to fill this gap in the following ways:

  1. Plain text is portable and trivially editable. When editing an image is done by editing a text file, the only barrier to entry is knowing JSON, knowing the schema used by Collagen, and knowing the catalog of SVG elements. In addition, a folder containing a text file and some images is simple to share: just zip it and send it along. Finally, Collagen is nondestructive; all assets used in the final image are available in their original form right in that folder.

  2. While most image editing programs support “guides” and “snapping” that allow graphical elements to be precisely positioned relative to each other, there is no “glue” to hold the elements together indefinitely; moving one tends not to move the other. For instance, an arrow pointing from a given corner of a rectangle to a fixed point cannot be made to have its tail always lie on that corner while leaving its head stationary. Variables solve this problem by letting the author use the same variable(s) to specify the position of both the rectangle’s corner and the arrowhead. More generally, variables support the problem of keeping several values in sync without having to edit multiple hard-coded values.

  3. Image editing programs output a single image file which is of one of the well-known image types (JPEG, PNG, etc). Different image formats are optimized for different types of image data, and break down when made to store image data they weren’t optimized for.

    1. For instance, JPEG is optimized for images with smoothly varying gradients (which tends to mean photographs taken by a physical camera). Therefore it produces images with ugly compression artifacts when made to store geometric shapes or text. Below are a PNG image of some text, and that same PNG after conversion to JPEG. The JPEG has been enlarged to make the compression artifacts more easily visible.

      A screenshot of the word “Collagen” in PNG format

      A screenshot of the word “Collagen” in PNG format

      A screenshot of the word “Collagen” in JPG format, zoomed in

      A screenshot of the word “Collagen” in JPG format zoomed

    2. On the other hand, PNG is optimized for images with long runs of few distinct colors, and requires a massive file size to store the kind of data that JPEG is optimized for. Despite displaying exactly the same image (source), the PNG file below is 6.6 times bigger than the JPEG.

      A JPEG, weighing in at 407KB

      A bunch of cherries

      A PNG, weighing in at 2.7MB

      A bunch of cherries

    3. JPEGs and PNGs are both raster formats, which means they correspond to a rectangular grid of pixels. A given raster image has a fixed resolution (given in, say, pixels per inch), which is, roughly speaking, the amount of detail present in the image. When you zoom in far enough on a raster image, you’ll be able see the individual pixels that comprise the image. Meanwhile, vector graphics store geometric objects such as lines, rectangles, ellipses, and even text, which have no resolution to speak of — you can zoom infinitely far on them and they’ll always maintain that smooth, pixel-perfect appearance. Without Collagen, if you want to, say, add some text on top of a JPEG, you have no choice to but to rasterize the text, converting the infinitely smooth shapes to a grid of pixels and losing the precision inherent in vector graphics.

    Collagen fixes this by allowing JPEGs, PNGs, and any other images supported by browsers to coexist with each other and with vector graphic elements in an SVG file, leading to neither the loss in quality nor the increase in file size that arise when using the wrong image format. (Collagen achieves this by simply base64-encoding the source images and embedding them directly into the SVG.)

  4. Creating several similar elements by hand is annoying, and keeping them in sync is even worse. Collagen provides a for_each tag to programmatically create arbitrary numbers of elements, and the children elements can make use of the loop variable to control their behavior. We saw this above in the pinwheel, which used the loop variable i to set the angle and color of each spoke. The for loop itself had access to the n-spokes variable set at the beginning of the file, which goes back to point 2: variables make things easy.

  5. Why SVG at all? Why not some other output image format?

    • SVGs can indeed store vector graphics and the different kinds of raster images alongside each other.

    • SVGs are widely compatible, as they’re supported by nearly every browser.

    • SVGs are "just" a tree of nodes with some attributes, so they’re simple to implement.

    • SVGs are written in XML, which is plain text and simple(-ish) to edit.

The above features make Collagen suitable as an “image editor for programmers”. Everybody loves memes, programmers included, so let’s use Collagen to make one.

collagen.json

{
    "$schema": "https://rben01.github.io/collagen/schemas/schema.json",
    "vars": { "width": 800 },
    "attrs": { "viewBox": "0 0 {width} 650" },
    "children": [
        {
            "tag": "defs",
            "children": [
                {
                    "tag": "style",
                    "text": "@import url(\"https://my-fonts.pages.dev/Impact/impact.css\");",
                    "should_escape_text": false
                }
            ]
        },
        {
            "image_path": "./drake-small.jpg",
            "attrs": {
                "width": "{width}"
            }
        },
        {
            "vars": {
                "x": 550,
                "dy": 50
            },
            "tag": "text",
            "attrs": {
                "font-family": "Impact",
                "font-size": 50,
                "color": "black",
                "text-anchor": "middle",
                "vertical-align": "top",
                "x": "{x}",
                "y": 420
            },
            "children": [
                {
                    "for_each": [
                        { "variable": "i", "in": { "start": 0, "end": 4 } },
                        {
                            "variable": "line",
                            "in": [
                                "Using SVG-based text,",
                                "which is infinitely",
                                "zoomable and has",
                                "no artifacts"
                            ]
                        }
                    ],
                    "do": {
                        "tag": "tspan",
                        "text": "{line}",
                        "attrs": { "x": "{x}", "dy": "{(if (= i 0) 0 dy)}" }
                    }
                }
            ]
        }
    ]
}

A Drake meme. Top panel

Using Collagen

Quick Start

Install Collagen with cargo install collagen. This will install the executable clgn.

Once installed, if you have a manifest file at path path/to/collagen/manifest.json, you can run the following command:

clgn -i path/to/collagen -o output-file.svg

To continuously monitor the input folder and re-run on any changes, you can run Collagen in watch mode:

clgn -i path/to/collagen -o output-file.svg --watch

In watch mode, every time a file in path/to/collagen is modified, Collagen will attempt to regenerate the output file and will either print a generic success message or log the specific error encountered, as the case may be. Watch mode will never terminate on its own. As with most terminal commands, you can terminate it with Ctrl-C.

This doc has several examples that can serve as a good starting point for creating a manifest. More examples are available as test cases in tests/examples.

Definitions

Collagen The name of this project.
clgn The executable that does the conversion to SVG.
Skeleton A folder that is the input to clgn. It must contain a collagen.json file and any assets specified by collagen.json. For instance, if skeleton my_skeleton’s `collagen.json contains { "image_path": "path/to/image" }, then my_skeleton/path/to/image must exist.
Manifest The collagen.json file residing at the top level inside a skeleton.

In-Depth Description

The input to Collagen is a folder containing, at the bare minimum, a manifest file named collagen.json. Such a folder will be referred to as a skeleton. A manifest file is more or less a JSON-ified version of an SVG (which is itself XML), with some facilities to make common operations, such as for loops and including an image by path, more ergonomic. For instance, without Collagen, in order to embed an image of yours in an SVG, you would have to base64-encode it and construct that image tag manually, which would look something like this:

<image href="data:image/png;base64,iVBORw0KGgoAAAA...(many, many bytes omitted)..."></image>

In contrast, including an image in a Collagen manifest is as simple as including the following JSON object as a descendent of the root tag:

{ "image_path": "path/to/image" }

Collagen handles base64-encoding the image and constructing the <image> tag with the correct attributes for you.

Basic Schema

In order to produce an SVG from JSON, Collagen must know how to convert an object representing a tag into an actual SVG tag, including performing any additional work (such as base64-encoding an image). Collagen identifies the type of an object it deserializes simply by the keys it contains. For instance, the presence of the "for_each" property tells Collagen that this tag is a for loop tag, while the "image_path" property tells Collagen that this tag is an <image> tag with an associated image file to embed. To avoid ambiguities, it is an error for an object to contain unexpected keys.

For users: add "$schema": "https://rben01.github.io/collagen/schemas/schema.json" to the top-level object of your manifest to get validation and suggestions. For developers: the tags are listed at docs.rs/collagen.

FAQ

  1. How is this different from a templating language like Liquid?

    Templating languages generally consist of two components: the templating library, which does the rendering of the template, and the template language, which usually resembles HTML with the addition of things like control flow and interpolation. The library is responsible for combining a template file and some external data and turning them into an output file. But you can’t (generally) write your data literally in the template file, which is inconvenient, and the overhead of needing to write down your data separately can be quite large compared to the complexity of the image you would use Collagen to create. In addition, to actually drive the templating library probably requires writing some code in the library’s language and running it in the language’s runtime.

    In contrast, Collagen lets you include data (such as lists to loop over) directly in the manifest and runs via single executable with no runtime to speak of. It also lets you write your image in a single file (collagen.json) instead of two (the template file and the “real code” that creates the output.) In addition, with Collagen, there is no syntax to learn, per se; you simply write JSON. If you reference Collagen’s JSON schema, writing a Collagen manifest becomes pretty simple and convenient.

  2. How does Collagen handle paths across multiple platforms?

    In general, filesystem paths are not necessarily valid UTF-8 strings. Furthermore, Windows and *nix systems use different path separators. How, then, does Collagen handle paths to files on disk in a platform-agnostic way? All paths consumed by Collagen must be valid UTF-8 strings using forward slashes (/) as the path separator. Forward slashes are replaced with the system path separator before resolving the path. So path/to/image remains unchanged on \nix systems, but becomes path\to\image on Windows. This means that in order to be portable, path components should not contain the path separator of any system, even if it is legal on the system on which the skeleton is authored. For instance, filenames with backslashes \ are legal on Linux, but would pose a problem when decoding on Windows. Generally speaking, if you restrict your file and folder names to use word characters, hyphens, whitespace, and a limited set of punctuation, you should be fine.

    Naturally you are also limited by the inherent system limitations on path names. For instance, while CON is a valid filename on Linux, it is forbidden by Windows. Collagen makes no effort to do filename validation on behalf of systems on which it may be used; it is up to the author of a skeleton to ensure that it can be decoded on a target device.

Dependencies

~14–25MB
~387K SLoC