[More quick guides]

A quick guide to
baseline alignment
in Prince

All your baselines are belong to us

Prince is a HTML-to-PDF-via-CSS converter. Prince is often used for typesetting books, and baseline alignment — where lines of body text align across columns and pages — is a commonly requested feature. CSS does not (yet) provide for this, but Prince is able to align baselines with the help of a small script. This guide will show you how.

We invite you to download Prince and try formatting the examples below yourself.

When lines just fall into place

In a document with only plain text, baseline alignment will happen automatically. In this example, all lines align perfectly with no need for adjustments:

htmlarticle {
  columns: 2;
  font: 12pt/1.5 Georgia, serif;
}
p {
  margin: 0; padding: 0;
  text-indent: 1.4em;
}

Headlines and figures

The harmony of plain body text is quickly broken when headlines and figures are introduced. As can be seen in this example, lines of body text are no longer aligned:

htmlarticle { 
  columns: 2;
}
img {  
  float: top;
}

Click on the thumbnail above to see the non-aligned lines. For typographers, this is a no-no.

Adding a script

The trick to restore harmony in this document is to add space above and below elements that disturb the peaceful rhythm. This is done through a little script. After typesetting the document, the script calculates how much space must be added to align lines. Thereafter, a second round of formatting is started.

In the next example, the script carefully adds padding so that baselines align. To better see the added padding, a background color is added to the adjusted elements:

htmlarticle { 
  columns: 2;
  font: 12pt/1.5 Georgia, serif; /* line-height is 18 point */
}
h1, img { 
  background: khaki;
}
img {  
  float: top;
}
...
<body onload="snap('18pt', 'h1, img', 'padding')">

Voila!

In the previous example, the onload attribute of the body element contains the call to the snap() function. There are three arguments. The first is a length which represents the line-height (in points, pixels or mm) of the body text. The line-height of this document is 18 points.

Thereafter comes a selector, and the name of the method to be used when adjusting the element. In the above example, padding method is used, which tells the script to adjust padding for h1 and img elements so that their height is a multiple of 18 points.

More elements

To more easily see the effect, we add more headlines and figures:

htmlh1, img { 
  background: khaki;
}
...
<body onload="snap('18pt', 'h1, img', 'padding')">

As can be seen in the rendering above, the extra padding is not evenly distributed: elements that are on the top of the page have most of the padding set on padding-bottom, and vice versa for elements at the bottom. The idea is to fill the whole of the page and not leave white space along the edges.

Even padding

If you prefer an even distribution of space above and below the content, you can use the 'padding-even' method. This is a not CSS property, it's just the name of a method which our script recognizes.

htmlh1, img { 
  background: khaki;
}
...
<body onload="snap('18pt', 'h1, img', 'padding-even')">

Margin, not padding

By setting margin instead of padding, the khaki-colored areas diminish, but baselines are still aligned:

html<body onload="snap('18pt', 'h1, img', 'margin')">

In general, it's better to use the 'padding' method rather than 'margin'; margin-collapsing may get in the way.

Adjusting height

The script can also increase the height of elements rather than adding padding or margins. For textual elements, this looks the same as if the padding is added at the bottom of the element. But for images, the added height will result in a slightly changed aspect ratio:

html<body onload="snap('18pt', 'h1, img', 'height')">

To retain the aspect ratio for images, the object-fit property can be used:

htmlimg { 
  float: top; 
  object-fit: cover;
}
...

<body onload="snap('18pt', 'h1, img', 'height')">

Pushing elements

Side boxes with padding, images, and body text of their own pose a challenge. Consider this example:

htmlfigure {
  background: #ddd;
  padding: 0.6em;
  float: top;
}
...
<h1>Birds</h1>
...
<figure>
  <img src=gjess1.jpg>
  <figcaption>...
</figure>
<body onload="snap('18pt', 'h1', 'height'); ">

The size of the heading element has been adjusted, but two challenges remain. First, the body text inside the figure element must be baseline-aligned with the body text in the left column. Second, the size of the figure element must be adjusted so that the text below the caption also aligns with the left column.

We will solve the first challenge by using a method called push. This method pushes an element downwards until it snaps into place. To solve the second challenge, another margin-bottom method is used.

html<body onload="
   snap('18pt', 'h1', 'margin-bottom'); 
   snap('18pt', 'figcaption', 'push'); 
   snap('18pt', 'figure', 'margin-bottom'); 
">

In the above example, the snap function is called three times. Each call causes another rerun of Prince. For large documents, you want to minimize the number of reruns. In some cases it is possible to combine several function calls into one. In this example, the first two calls are independent of each other and can therefore be combined:

html<body onload="
   snap('18pt', 'h1', 'margin-bottom, 'figcaption', 'push'); 
   snap('18pt', 'figure', 'margin-bottom'); 
">

Superscripts

Superscripts sometimes upset the baseline rhythm by pushing lines further apart:

html... <sup>2</sup>

You will notice that the superscript at the end of the first paragraph makes the line slightly higher than the others. This can be fixed by setting the line-height of the sup element:

htmlsup { line-height: 0 }

Aligning with bottom-floating content

Bottom-floating content is extra challenging. One must control both the height of the float as well as the height of the page area into which content is poured. In this example, 20px is the line-height of the body text, and all other heights should be a multiple of 20px. The height of the page is set to 640px (a multiple of 20px), and the page margin is 20px. As a result, the height of the page area is 600px. The page area is light blue and has a 20px dotty grid in the background:

html@page {
  size: 300px 640px;
  margin: 20px;
  background: url(data:image/png;base64,iVBOR...
}

body { background: rgba(0,255,255,0.3); }
h1 { font: 30px/25px sans-serif; background: rgba(255,255,0,0.3); }
p { font: 15px/20px serif; }
img { float: bottom; padding: 0; margin: 0; opacity: 0.5; }

If you study the renderd PDF carefully (by clicking on the thumbnail), you will see that the height of the heading does not align with the grid, and neither does the height of the image. As a result, the space between the image and the text above it is somewhat unpredictable.

To fix this, we must adjust the heights of the h1 and img elements to a multiple of 20px. The snap() function comes to rescue:

html@page {
  size: 300px 640px;
  margin: 20px;
  background: url(data:image/png;base64,iVBOR...
}

body { background: rgba(0,255,255,0.3); }
h1 { font: 30px/25px sans-serif; background: rgba(255,255,0,0.3); }
p { font: 15px/20px serif; }
img { float: bottom; padding: 0; margin: 0; opacity: 0.5; }

<script type="text/javascript" src="../charms.js"></script>
<body onload="snap('20px', 'h1, img', 'height')">

As can be seen above, all height are now a multiple of 20px.

Caveat

The observant reader has probably realized that the script, while wonderful, does not really know anything about baselines. It merely tries to ensure that all boxes in the document have a height which is a multiple of the line-height. This strategy makes it possible for a simple script to fix an important problem, but it also has limitations. In some cases, the increase in size will result in elements being pushed to the next column, or the next page. The current script does not alert authors when this happens.

2024-02-21 Håkon Wium Lie