This program is an implementation of the ideas in
<http://lists.canonical.org/pipermail/kragen-tol/2012-April/000955.html>.  It
generated <http://canonical.org/~kragen/bible-columns.png>, a hundred-megapixel
PNG containing the full text of the King James Bible in a format that should be
readably printable on a 600dpi laser printer.

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Render text to a given width using a proportional pixel font.

Without linebreaks except when it runs out of space on a line.

The font file is a black-and-white image whose height is one pixel
more than the height of the glyphs in the font.  The bottom row of
pixels indicates glyph boundaries: every black pixel in it is below
the rightmost column of a glyph.  (Therefore all glyphs are at least
one pixel wide.)  The other rows of pixels comprise the pixel of the
glyphs, using a black foreground and a white background.

The order of the glyphs is fairly bizarre.  There are 96 glyphs.  The
first 95 glyphs are for bytes 32-126; the 96th is the glyph for
newline, byte 10.

Here’s the font I’m using, as a base64-encoded PNG file.  The 96th
glyph looks like a sort of squished pilcrow.

iVBORw0KGgoAAAANSUhEUgAAAXYAAAAHAQMAAAAmmfT2AAAAAXNSR0IArs4c6QAAAAZQTFRF////
AAAAVcLTfgAAAT5JREFUGNNj0ApUFJrCwNJptXPnBBZlmRkz71w9GigyWVh61aJVv90cmRgamlQl
GBgYmEB4oQeD1u+uKR4NrFFdi7hWeU5YGhoqKNIauG3VqoVaq1YJOS2YLTlRmtFzmZqpeahr6JNJ
MxgUggVZYg9znpi8WVBCgFP3pujMmNaJoau1BbVWuSgpMWgtWr1qaWdoaFhSaMgUHwmgTT8WtXg0
CHpydC3q4JygCDQ/ZGpgyCqtTu1tS5yEgOo3Si31DA0LEQoN6WmZIAE0v9MlhCNg9oaNykYuirIR
My9cPRkrMkloo6XSkt9iDNKSM7mWxoaqmCQaabo+mcDBwMDIwNQgwAAGDQxwwAGh+IDYAMhmAamQ
YFLgYAhkcdFUUlmk4uLi4qrk4qQEBhxAnoujIEunEIgRpMTRAQRNSoIuKgCUXGFT9L+WxgAAAABJ
RU5ErkJggg==

This program will read the entire input into memory and construct an
output image for the whole thing at once, so it is limited in the size
of images that it can handle.  It seems to use about one byte of RAM
per output pixel.

"""

from __future__ import division

import math
import sys

import Image

codepoints = range(32, 127) + [10]
glyph_indices = {}
for _glyph_index, _codepoint in enumerate(codepoints):
    glyph_indices[_codepoint] = _glyph_index

def main(argv):
    _, font_filename, n_columns, total_width, page_height = argv
    font = Image.open(font_filename)
    total_width = int(total_width)
    n_columns = int(n_columns)
    page_height = int(page_height)
    iwidth, iheight = font.size
    height = iheight - 1
    page_lines = int(math.floor(page_height, height))

    text = sys.stdin.read()

    column_width = int(math.floor(total_width / n_columns))-1

    ends = glyph_ends(font)
    starts = [0] + [ii+1 for ii in ends[:-1]]
    widths = list(deltas(ends, -1))

    # How many lines would we need in all the columns together?
    n_lines = count_lines(widths, column_width, text)
    lines_per_column = int(math.ceil(n_lines / n_columns))
    pixel_height = lines_per_column * height

    glyphs = [get_glyph(font, starts, widths, glyph_index)
              for glyph_index in range(len(widths))]
    outbuf = Image.new(mode='1', size=(total_width, pixel_height))
    for line, x, glyph_index in glyph_positions(widths, column_width, text):
        column, within_column = divmod(line, lines_per_column)
        outbuf.paste(glyphs[glyph_index],
                     ((column_width+1) * column + x, within_column * height))

    outbuf.save(sys.stdout, format='png')

def glyph_ends(font):
    "Read the bottom line of the font to get the widths."
    iwidth, iheight = font.size
    return [ii for ii in range(iwidth) if font.getpixel((ii, iheight-1))]

def deltas(seq, last=0):
    for item in seq:
        yield item - last
        last = item

def count_lines(widths, pixel_width, text):
    for full_lines, _, _ in glyph_positions(widths, pixel_width, text):
        pass
    return full_lines + 1

def glyph_positions(widths, pixel_width, text):
    """Yield (lineno, pixelcol, glyph_index) tuples for all the glyphs.

    lineno and pixelcol count from 0.
    """
    full_lines, x = 0, 0

    for byte in text:
        glyph_index = glyph_indices[ord(byte)]
        width = widths[glyph_index]
        if width + x > pixel_width:
            full_lines += 1
            x = 0

        yield full_lines, x, glyph_index
        x += width
        assert x <= pixel_width
    
def get_glyph(font, starts, widths, glyph_index):
    iwidth, iheight = font.size
    height = iheight - 1
    x = starts[glyph_index]
    return font.crop((x, 0, x + widths[glyph_index], height))

if __name__ == '__main__':
    main(sys.argv)
-- 
To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-hacks

Reply via email to