Idiomdrottning’s homepage

Writing Inkscape word balloons in Emacs

I usually use Inkscape to letter my comics. Not sure why, the hinting isn’t the greatest if you convert to png (unless you go the Cairo route, but then you have other issues with embedded images etc.). And working with texts in Inkscape is a bit of a hassle.

But now I have a solution for at least the hassle part. Writing the word balloons in a textfile and then converting that text file into Inkscape texts.

I don’t do this for shorter strips, but for longer works it’s nice. You can use a copy of your script (I sometimes write comic scripts in fountain), just grepping out the lines you want to use for word balloons, and then edit them in Emacs or any text editor, and then convert them into Inkscape-style svg.

I’ve added the programs to a git repo that you can clone:

git clone https://idiomdrottning.org/writing_inkscape_word_balloons_in_emacs

The .mst source file

Write your texts in a file like this.

Oh, what a nice
winter day to write
a word balloon!

This is a second
balloon and it has
some *italic* text.

There’s no info about who’s speaking, what order the balloons should come in etc. So save a copy of your full script file separately. This is just an intermediary file that you make in order to get your text into Inkscape.

The Emacs mode

In order to help you break your lines nicely, set your frame font to the same font you’re going to be using in Inkscape.

(set-frame-font "Nimbus Sans L-11")

This is especially important if you’re going to use proportional fonts in Inkscape.

And you can use this emacs mode:

(fset 'widen-row
   (lambda (&optional arg) "Keyboard macro." (interactive "p") (kmacro-exec-ring-item (quote ([5 4 32 19 32 return 8 13 2] 0 "%d")) arg)))

(fset 'narrow-row
   (lambda (&optional arg) "Keyboard macro." (interactive "p") (kmacro-exec-ring-item (quote ([5 4 32 18 32 return 18 32 return 4 13 2] 0 "%d")) arg)))

(defun rebreak-line ()
  (interactive)
  (electric-newline-and-maybe-indent)
  (move-end-of-line nil)
  (insert " ")
  (delete-char 1)
  (move-end-of-line 0))

(defvar mst-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-4") 'set-fill-column)
    (define-key map (kbd "C-7") 'rebreak-line)
    (define-key map (kbd "C-9") 'narrow-row)
    (define-key map (kbd "C-0") 'widen-row)
    map)
  "Keymap for `mst-mode'.")

(add-to-list 'auto-mode-alist '("\\.mst\'" . mst-mode))

(define-derived-mode mst-mode fundamental-mode "multispute"
   "A major mode for writing text for word balloons.")

(I want to rewrite the stored macros to be normal procedures when I have time, since stored macros don’t “undo” as well as normal procedures do.)

This mode has four functions.

Set fill column

First of all, being able to change the fill column all the time is useful. You can set the fill column to a low number (tip: the default is the column your cursor is on) and then hit M-q to see Emacs’ best attempt at filling the bubble. Sometimes it’s good. You can also set the fill column to a huge number, like 1000 or so, to use M-q to put all the text in a word balloon on a long line so you can start over.

A typical workflow for me is to do just that – put the balloon text on a long line, and then go over it backwards and hitting C-j to break the line, ensuring that I don’t get orphan words and then eyeballing the lines to be good lengths.

I might turn

Oh, what a nice winter day to write a word balloon!

into

Oh, what a nice winter day to write
a word balloon!

since that looks at first glance to be a good width and doesn’t leave orphan words. The last line in a balloon is usually the most important to me.

and then into

Oh, what a nice
winter day to write
a word balloon!

So most of the time I’m just C-j on long lines, and using C-4 and M-q to start over with long lines.

But, the mode does provide a couple of other convenience functions.

Rebreak line

If I have my cursor after “day” as in the following example,

Oh, what a nice
winter day| to write
a word balloon!

and then hit C-j, I get

Oh, what a nice
winter day
 to write
a word balloon!

Sometimes that’s what I want.

But sometimes I instead want it to turn into this:

Oh, what a nice
winter day
to write a word balloon!

if I’m tweaking a balloon that doesn’t look great. That’s what rebreak-line is for. I set it to C-7.

Narrowing and widening rows

Makes rows one word shorter or longer at the benefit/expense of the row below it. With these functions, you can have the cursor at any position on the line. It’s moved to the end of the line.

For example,

Oh, what a nice
winter day| to write
a word balloon!

turns into

Oh, what a nice
winter day to write a|
word balloon!

if you hit widen-row (set to C-0), and

Oh, what a nice
winter day to|
write a word balloon!

if you hit narrow-row (set to C-9). These are designed to be used several times in a row, combined with C-n and C-p to fine-tune the text. You might go “widen, widen, widen … ok that’s too far, narrow… that’s nice”.

The awk program

I’ve got this saved in multispute.awk and set to executable.

#!/usr/bin/gawk -f
BEGIN {RS="";
FS="\n";
printf "<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n\
<svg\n\
   xmlns:dc="http://purl.org/dc/elements/1.1/"\n\
   xmlns:cc="http://creativecommons.org/ns#"\n\
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n\
   xmlns:svg="http://www.w3.org/2000/svg"\n\
   xmlns="http://www.w3.org/2000/svg"\n\
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\n\
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\n\
   width="210mm"\n\
   height="297mm"\n\
   viewBox="0 0 210 297"\n\
   version="1.1"\n\
   id="svg4510"\n\
   inkscape:version="0.92.1 r15371"\n\
   sodipodi:docname="several_lines.svgz">\n\
  <defs\n\
     id="defs4504" />\n\
  <sodipodi:namedview\n\
     id="base"\n\
     pagecolor="#ffffff"\n\
     bordercolor="#666666"\n\
     borderopacity="1.0"\n\
     inkscape:pageopacity="0.0"\n\
     inkscape:pageshadow="2"\n\
     inkscape:zoom="1"\n\
     inkscape:cx="101.57143"\n\
     inkscape:cy="973"\n\
     inkscape:document-units="mm"\n\
     inkscape:current-layer="layer1"\n\
     showgrid="false"\n\
     inkscape:window-width="1600"\n\
     inkscape:window-height="1177"\n\
     inkscape:window-x="-1"\n\
     inkscape:window-y="22"\n\
     inkscape:window-maximized="0" />\n\
  <metadata\n\
     id="metadata4507">\n\
    <rdf:RDF>\n\
      <cc:Work\n\
         rdf:about="">\n\
        <dc:format>image/svg+xml</dc:format>\n\
        <dc:type\n\
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\n\
        <dc:title></dc:title>\n\
      </cc:Work>\n\
    </rdf:RDF>\n\
  </metadata>\n\
  <g\n\
     inkscape:label="Layer 1"\n\
     inkscape:groupmode="layer"\n\
     id="layer1">\n"}
{printf "    <text\n\
       xml:space="preserve"\n\
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88055563px;line-height:100%%;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';font-variant-ligatures:no-contextual;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:center;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:middle;white-space:normal;shape-padding:0;display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"\n\
       x="13.947322"\n\
       y="8.6419668">"
    for (i = 1; i<=NF; i++) printf "<tspan\n\
         sodipodi:role="line"\n\
         x="13.947322"\n\
         y="%f"\n\
         style="stroke-width:0.26458332px">%s</tspan>",8.6419668+4.850694*i ,gensub(/\*([^*]*)\*/, "<tspan\n\
   style="font-style:italic">\\1</tspan>" ,"g", gensub(/\*\*([^*]*)\*\*/, "<tspan\n\
   style="font-weight:bold">\\1</tspan>" ,"g", $i))
print "</text>"}
END {printf "  </g>\n\
</svg>"}

To call it, I write

multispute.awk < my_input_file.mst > my_output_file.svg

or

multispute.awk < my_input_file.mst |gzip -c > my_output_file.svgz

You can probably figure out how it works. I started with an svg-file I had already made in Inkscape. You can make your own the same way, if you have your own favorite fonts etc. You can also use other templating languages to modify your inkscape file if you know them better than awk.

(I went with gawk over mawk in order to get gensub, to support italics and bold, with asterisks.)

The result

All the texts will end up in the same place, with the last balloons on top in the z-order. Looking a bit jumbled.

Jumbled text

It’s up to you to move them to where you want and to put balloon shapes under anyway you want.

You can make further tweaks to them here in Inkscape. Changing fonts, edit the text etc.

For the ballon shape I usually make a very wide ellipse, convert it into a path with S-C-c, and in the node editor select the two middle nodes, and then hit the comma until those two nodes are almost-but-not-quite where the handles of the top and bottom node is.

This gives us that TV shape we like. This can be done in seconds but I still usually only do one that I then duplicate, resize (make sure to have stroke scaling off) and reuse for all balloons.

The tails I draw with spiro splines and then union with the ballons.

This is what I ended up with:

All sorted out

(These were made with 125% lineheight. I’ve since changed to 100% lineheight. You can play around with different values, see what you like.)