A full book in one poster

Not an original idea, I have been seing beautiful versions of this for ages, normally with movie scripts squeezed into a painting frame size. But I wanted to try my hand anyway. And with no less than the whole effing text of Umberto Eco's The Name of the Rose.

How it looks from afar.
How it looks from afar.

My first impulse was, of course, see if Scribus could handle it. I managed to put the full text in it. The file weights around 1 MB, and last time I tried to open it I had to kill Scribus after an 8+ minute wait. Scribus is a great publishing application, but unsuitable for what I needed.

It had to be done programmatically. And my go to language for that is Python. Basically, because I'm not proficient in anything else. Not that I'm proficient in Python either. Which speaks volumes about how newbie friendly Python is.

My first attempt took the book text line by line, and, using pango converted each line into a png slice which would be joined to the others with ImageMagick

I had not much control there, and wanted something a little bit more general, so after much trial and error I ended up doing the (abridged) following steps:

Obtain and prepare text

I had the ePub version, and, if I recall correctly, obtaining the plain text version was as easy as using Calibre's conversion tool.

There was some manual preparation, namely stripping out the prologue and deleting all line end so all was one single line. Any advanced text editor can handle that.

Add colour

I wanted every instance of the protagonist's name, Guglielmo in the Italian original, to be printed in a different colour. I came up with a makeshift solution, which was adding a "escape character" before any printable character to change colour to. I just had to be careful to not use a meaningful character, or I could get undesired colouring in other parts of the text. I chose ε for red and µ for black but could have been anything else.

The font

It was not just a matter of using some font and reducing it. I wanted to be in control of the size of the final text, so I improvised something that python could use to render each character.

Each letter would be a 2-dimensional array of pixels, each line would be an array of letters, and the final work would be an array of lines. NumPy can handle that.

So the font was to be something easy for python to convert to such an array. I just needed some JSON to easily create a font that would work good enough for my purposes.

# EXAMPLE GLYPH: A
#
# 0110 results in  ·XX·
# 1001 results in  X··X
# 1111 results in  XXXX
# 1001 results in  X··X
# 1001 results in  X··X

FONT = {
    ...
    "A": "01101001111110011001",
    ...
}
# Add a JSON pair for every character.

That makes the tiniest characters possible for what I wanted to do. The shape of the glyphs was inspired by CG pixel 4x5 mono by Ilmari Karonen, which I found, of course, at the blessing that StackExchange is.

Calculating the line length

Knowing the final print format, minus the margins; and the resolution, we can just convert that to pixels to see how many characters can we fit in one line.

In order to make the printed area more harmonic, we calculate the line length needed to get similar margins, or a printed area proportional to the printing format. This was my brute force solution for that:

def linelength(charnum,maxx,maxy,columnwidth,lineheight,strategy):
    """
    Parameters:
        charnum (int):       effective length of the text
        maxx (int):          max line length in characters
        maxy (int):          max line number in characters
        columnwidth (int):   column width in pixels
        lineheight (int):    line height in pixels
        strategy (str):      what means optimal for us?
    Returns
        bestcandidate (int): desirable line length
    """
    bestdifference = math.inf # Infinity to start, anything better than that is good
    bestcandidatex = maxx # We start with a text filling the whole width
    proportions = maxy / maxx # Used by the proportional strategy
    # We keep making the line 1 character shorter
    for x in range(maxx,0,-1):
        # How many lines with this length?
        y = math.ceil(charnum / x) # Any excess (even 1 char) means one line more
        if y > maxy:
            break
        else:
            textproportions = ((y * lineheight) / (x * columnwidth))
            textmargins = ((maxx*columnwidth) - (x*columnwidth), (maxy*lineheight) - (y*lineheight))
            if strategy == "proportional":
                # The closest the proportions of paper and text, the better
                difference = abs(proportions - textproportions)
            else:
                # Or the equalest the side and topbottom margins, the better
                difference = abs(textmargins[0] - textmargins[1])
            if difference < bestdifference:
                bestdifference = difference
                bestcandidatex = x

    return bestcandidatex

Cutting the text into lines

With the optimal line length, we iterate through the whole text and start building lines of said length, character by character. We need to take some things into account, e.g. if we find a "control character" we include it but doesn't count, if we find a white space at the start of a line, we discard it...

And if we find ourselves in the last line and it's too short, we need to complete it with whitespaces.

Now that our text forms a perfect rectangle (so NumPy can work with it as a multidimensional array), we save it in an intermediate file. Probably an unnecesary step.

Converting the text into an image

We feed each line of the intermediate file to a function that reads it character by character. If the character is a "control" one, the colour changes for the next character.

If the character exists in the keys of the font JSON, we convert each one and zero corresponding to that character into an array of values representing CMYK.

So, of the 01101001111110011001 corresponding to the A above each 'zero' would be translated to a [0,0,0,0] and each 'one' to [0,0,0,255].

Actually, it's a bit more complex than that: because I defined the font in rows (because it seemed the logical thing) and turns out NumPy needs those pixels ordered up-to-down and left-to-right, I needed to take the digits in a different order.x

Also, to that array of arrays we add a column right, equal to the "leading" space we define between characters. That is, if it's not the last character in the line.

The resulting array (line) of arrays (characters) of arrays (pixels) is appended to a final grid (an array of arrays of arrays of arrays), plus a number of rows of white pixels between each one.

Converting that NumPy grid into an image file is as easy as telling PIL to just do it for us.

Ugly finger for scale.
Ugly finger for scale.

To do

I'm working on a version of the Python script that does it all, but that can be used with any text file and desired final result just by changing a few constants.

In the meanwhile, if you want me to print something in this fashion, do not hesitate to contact me!