Introduction
This tool was created to enable the porting of a pair of large LaTeX
documents to a more modern and maintainable
Asciidoc-based Antora
site. The only thing I couldn’t do without was the wonderful
bytefield
LaTeX package for drawing byte field diagrams, and
bytefield-svg
was my way to bring it forward to this new world.
It
worked wonderfully.
Along the way, I switched from LaTeX-style commands to using Clojure to build my own domain-specific language for expressing byte field diagrams. Having the full power of a modern Lisp made the diagram source a lot more compact and faster to write, and it is easy to define your own helper functions to eliminate repetitive writing.
Clojure’s rich set of native data types (including keywords, vectors, maps, and sets), and compact way of representing them in source, are a big part of why this worked so well.
But this means that to get really productive with
Here is an introduction to the syntax, a cheat sheet and some help deciphering what will initially look like strange characters that are hard to search for on the web. The sci interpreter that runs your
|
The Drawing Model
The basic purpose of bytefield-svg
is to support the drawing of
byte-field diagrams to aid in the understanding of network protocols,
memory layouts, and similar binary structures. It sets up a drawing
environment to facilitate that, as well as the automatic generation of
column and row headers to help readers keep track of byte addresses.
Reasonable defaults are provided, but everything is very
configurable. Before diving into those details, it will help to look
at some concrete examples. The main function you’ll use for drawing
byte boxes is draw-box
. By default it creates a
box that represents one byte. So if we create a diagram with only
(draw-box 0)
as its content, here is what we get:
Notice that the value is represented as two hexadecimal digits, to reinforce that it is a single byte. But why is it positioned where it is? We can get a hint of that if we draw the column headers before calling it. So, a slightly more full-featured diagram specification would be:
(draw-column-headers)
(draw-box 0)
That’s more clear: The diagram is configured to show sixteen byte boxes per row, which is why our box was drawn where it is. The diagram is centered, as well (although there is room on the left for row address headers, which will appear when the diagram grows beyond one row).
But why were the column headers not drawn until we explicitly asked
for them? That is to give us a chance to change things like the number
of boxes per row and the left margin before we start drawing. Those
values are set up as defaults, but can be changed by calling def
(a
standard Clojure function[1]) with the symbol of the
setting we want to define or redefine, and the value we want to give
it, like so:
(def left-margin 1)
(def boxes-per-row 4)
(draw-column-headers)
(draw-box 0)
That gives us a nicely centered diagram with four byte boxes per row.
If you have to draw a lot of values in a sequence, you can use normal
Clojure sequence handling forms like
doseq
:
(draw-column-headers)
(doseq [val (range 30)]
(draw-box val))
And we get thirty boxes containing the numbers from 0 to 29 expressed in hexadecimal.
For situations where you have a series of bytes that you want to
convey are part of a related structure, you can use the
draw-related-boxes
function. It takes a
sequence as its first argument, and does the looping for you, in
addition to using dashed borders inside the related group:
(draw-column-headers)
(draw-related-boxes (range 30))
Of course, sometimes we have values that take more than one byte, and
sometimes we want to draw labels describing their contents, rather
than just showing values. If you pass a string rather than a number,
it is drawn as a label. And draw-box
takes a second argument after
the box content, which is a map of attributes, identified by keyword,
that modify the box. The :span
attribute controls how many bytes
wide the box should be, with a default value of 1. Putting that
together, and using a new draw-gap
function which
is designed to communicate variable-length structures, we can draw
something like this:
(draw-column-headers)
(draw-box "Address" {:span 4})
(draw-box "Size" {:span 2})
(draw-box 0 {:span 2})
(draw-gap "Payload")
(draw-bottom)
We had to call
draw-bottom
at the end, because sometimes we want control over the border at the bottom of a gap, which will become more clear in the discussion of box borders.
Note that the third box we drew, which we labeled with a number, was drawn using four hex digits, because we told it to span two byte boxes, and that’s how many hexadecimal digits fit in two bytes.
With that introduction, we’re ready to start exploring all the values that can be configured, and the full details of the functions that you can use for drawing.
def
is actually a special form rather than a function, but that is further afield than this introduction needs to go.