How to integrate PlantUML into other software

Draw illustrations painlessly with code

Author's image
Tamás Sallai
6 mins

Why PlantUML?

I like text-based image generation for illustrating a point in an article. And I've found that putting images, any type of image, to break the text flow helps a lot with understanding. Even when I see some cat pictures strewn over it helps to avoid the wall-of-text effect. But why stop at unrelated cat pictures?

I experimented with draw.io and I found it to be an excellent free software. But the problem with a separate tool is that it segments the workflow of writing.

I draw the image, I export it, insert into the article. But what if I see a typo?

I need to go back to the drawing board, fix the mistake, export it again, then overwrite the original image. It is just a pain to do it, and this is exacerbated by time as I hardly feel like doing this for old articles.

Compare that with a typo in the text. I just edit the file and I'm done. No wonder why I see typos persisting inside images eternally but not in the text.

The other issue with draw.io is that it is graphics-based, which means I need to use the mouse which I don't like.

The solution is, of course, a text-based and integrated solution where illustrations are no different than the text content of the article. PlantUML checks the first box, and with a little bit of coding, I could check the other one.

The result?

I write this:

@startuml
hide footbox
actor me
-> me: Start writing
activate me
me -> me: Write text
me -> PlantUML: Write a diagram
return
@enduml

And this appears:

If there is a typo, I can just fix it just like any other part of the article. It will be changed during the next render, automatically, without the need to leave the editor at any point.

Let's see how to write a piece of code that gets the textual representation and outputs an SVG with the illustration!

Integrating PlantUML

The main problem with PlantUML is that it is a Java application. This is not an issue if you want to integrate it into a Java codebase, but I left that ecosystem a long time ago. Using it from NodeJS or any other non-JVM-based language requires calling the executable file and handle the input/output streams.

Installing PlantUML is trivial as it's in the Ubuntu repository: apt install plantuml.

Problems arise when you want to run it as a daemon that keeps listening for diagrams on its standard input and outputs the images on its standard output. The primary mode of operation is to process a directory of inputs and generate PNGs to an output directory then exit. But, well, we are talking about a Java application which is notoriously slow to start. The start -> generate images -> stop mode of operation quickly becomes too slow for practical purposes.

A better way is to keep it running and feed it the diagram descriptions when they come.

CLI parameters

PlantUML comes with a bunch of command-line arguments and a daemon-like operation requires several to be used.

-tsvg

Since I prefer vector images to rasterized ones, this is one of the arguments I use every time. This instructs PlantUML to generate SVG files instead of the default PNG.

-pipe

This starts PlantUML in pipe mode instead of the directory-processing sort. It gets the diagram description on the standard input and outputs the results on the standard output.

-pipeNoStderr

With just the -pipe option PlantUML generates an image on stdout if everything goes well, but if not then generates an error message on stderr along with the image on stdout.

This is a problem as the invoking code can not be sure when an image is coming on stdout that there will be no error message on stderr following it later. This creates a race condition.

With the -pipeNoStderr, the error message will be sent on stdout and no image is generated in an erroneous case.

-pipedelimitor

Finally, this argument lets you define a string that will be printed after every image. Without that, there is no way to know for sure when one diagram is processed as the result comes in chunks (remember, we are reading the stdout of an external program).

With the -pipedelimitor in place, we just need to generate a sufficiently random value and check for it.

-XX:+SuppressFatalErrorMessage

This is not for PlantUML but for the JVM. Killing the app will also kill the PlantUML application and Java generates a hs_err file with some details in this case. As a result, it will litter the working directory with these files.

Setting the JAVA_TOOL_OPTIONS: -XX:+SuppressFatalErrorMessage parameter instructs the JVM to not write the hs_err files.

Nodejs example

Using the tools above, a NodeJs implementation for a function that gets the definition and return an SVG (or an error) looks like this:

const {spawn} = require("child_process");

// ...

const generatePlantuml = (() => {
	const plantumlCache = {};

	const delimitor = `END${Math.random()}`;
	const child = spawn("plantuml", [
		"-tsvg",
		"-pipe",
		`-pipedelimitor <!--${delimitor}-->`,
		"-pipeNoStderr"
	], {
		stdio: ["pipe", "pipe", "pipe"],
		env: {"JAVA_TOOL_OPTIONS": "-XX:+SuppressFatalErrorMessage"}
	});

	let queue = Promise.resolve();

	return (code) => {
		if (plantumlCache[code] !== undefined) {
			return plantumlCache[code];
		}
		const result = queue.then(() => new Promise((res, rej) => {
			let result = "";

			child.stdin.write(`@startuml\n${code}\n@enduml\n`);
			const stdoutListener = (data) => {
				result += data.toString("utf8");
				if (data.toString("utf8").indexOf(delimitor) !== -1) {
					child.stdout.removeListener("data", stdoutListener);
					if (result.startsWith("ERROR")) {
						rej(result);
					} else{
						plantumlCache[code] = result;
						res(result);
					}
				}
			};
			child.stdout.on("data", stdoutListener);
		}));

		queue=result.catch(() => {});
		return result;
	};
})();

// ...

const svg = await generatePlantuml(code);

It starts PlantUML only once, and it also utilizes a memory cache to skip regeneration of an already processed diagram.

It also serializes the calls so that multiple concurrent invocations won't be a problem even if PlantUML does not handle this case.

It returns a Promise which makes it easy to insert it into an asynchronous workflow. And by rejecting the promise in case of errors it will work just as expected when calling it with await.

The downside is that it won't shut down PlantUML ever, which is fine for a static website generator but might not be ideal for other use cases.

Ruby

The blog you are reading right now is powered by Jekyll, which uses Ruby. The Ruby implementation of the above code:

require 'open3'
require 'securerandom'

# ...

delimitor = "<!--#{SecureRandom.hex}-->"
plantumlin, plantumlout = Open3.popen2("JAVA_TOOL_OPTIONS='-XX:+SuppressFatalErrorMessage' plantuml -tsvg -pipe -pipedelimitor '#{delimitor}' -pipeNoStderr")
@@plantuml = Hash.new do |h, key|
	plantumlin.puts "@startuml\n#{key}\n@enduml"
	res = ""
	while !res.include? delimitor
		res += plantumlout.gets
	end
	if res.strip.index("ERROR") == 0
		raise "PlantUML Error: #{res} in code #{key}"
	end

	svg = res.sub(delimitor, "")

	h[key] = svg
end

# ...

svg = @@plantuml[code]

Conclusion

Programmatic image generation is awesome and I hope more people will use them to illustrate the points in blog posts. PlantUML is just one tool for this, and if you know others that work similarly, please leave a comment. I'm looking for more variety when it comes to illustrations.

December 31, 2019

Free PDF guide

Sign up to our newsletter and download the "How Cognito User Pools work" guide.


In this article