How to integrate PlantUML into other software
Draw illustrations painlessly with code
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.