# vim: set fileencoding=utf8 : # # Using cairo for drawing on a multi-page surface, this code manipulates the # cairo context to allow multiple-up drawing. # This code assumes 'normal' papersizes with an aspect ratio of √2. # I know many of you in the Americas (and Philippines) are not so lucky, # please try to understand, and feel free to change this code. # The function signature of these functions is # do_Nup(y_trans, context, pages) # or # do_nup(N, y_trans, context, pages) # Where N is the amount of 'up', y_trans is the height of the page, context # is the initialized cairo context and pages is a sequence with arbitrary # page objects. The result of all functions is a sequence of 2-tuples: # (context, page) # At the moment of yielding the tuple, the context is initialized to be drawn # upon for one page. # # In this code I try to show a way to analize the function and rebuild it # in a functional programming style using high-order functions. # This highlights the differences with the original function that uses an # imperative programming style with control structures. from math import sqrt, log from cairo import Matrix from itertools import cycle, chain, izip, starmap from functools import partial def do_1up(y_trans, context, pages): """This is the base example of the interface exposed by all other Nup functions. If your code produces correct results using this generator, it will also work with the others. """ for page in pages: yield context, page context.show_page() def do_2up_imperative(y_trans, context, pages): """Using the yielded values of this function allows drawing a 2up layout. This is what you will supposedly get when implementing this from scratch (perhaps with drawing code directly plugged in at the yield). """ # Scale to get from 'normal' to 'small' scale = sqrt(2) # If y_trans is the height, x_trans is the width x_trans = y_trans / scale # Pre-calculate the negative value x_trans_neg = 0 - x_trans pagenr = 0 context.scale(scale, scale) # Scale to the small format (do_scale) for page in pages: pagenr += 1 yield context, page # The page should now get painted if pagenr % 2 != 0: # Go to the right (move_item) context.translate(x_trans, 0) else: # Go to the next page (move_page) context.translate(x_trans_neg, 0) context.show_page() # do_2up_imperative pre-calculates all variables not used in the control # structure, and uses 1 intermediate variable. # The algorithm is really simple, but we will complicate things in the # next steps. # Note: we do a useless translate after the last page is drawn, # this is mainly to keep the code a bit cleaner to read. def do_nup_imperative(nup, y_trans, context, pages): """A generalized version of do_2up_imperative, the first parameter specifies the amount of 'up'. We have a bit more pre-calculated variables, and a new dimension to take into account while translating the context. """ # Scale to get from 'normal' to 'small' resize = sqrt(2) # The height of a single page is: height / ( sqrt(2) ** log(nup, 2)) resize_count = log(nup, 2) # Scale to get from 'normal' to 'small' scale = 1 / (resize ** resize_count) # If y_trans is the height, x_trans is the width x_trans = y_trans / resize # The number of pages on each 'line' return_count = int(sqrt(nup * (1 + resize_count % 2))) # The translation to move back the line x_trans_back = x_trans * (1 - return_count) # The translation to move back the page y_trans_back = y_trans * (1 - nup / return_count) pagenr = 0 context.scale(scale, scale) # Scale to the small format (do_scale) for page in pages: pagenr += 1 yield context, page # The page should now be painted if pagenr % return_count != 0: # Go to the right (move_item) context.translate(x_trans, 0) elif pagenr % nup != 0: # Go to the next line (move_line) context.translate(x_trans_back, y_trans) else: # Go to the next page (move_page) context.translate(x_trans_back, y_trans_back) context.show_page() # By now, the conditions in the control structure start to scare me, we have 2 # branches (if clauses) dependant on the iteration counter (pagenr), and # external state-manipulation (translate call) in each one. # # While still easy to understand I hope this illustrates why changing or at # least reasoning about higer-order functions makes sense. The following # functions use python's itertools functionality, these implement the # high-order functions usually also found in functional programming languages. def nup_page(context, matrix, show_page, page): """We pull the common parts from all 3 possible paths into this function, I replaced the translate calls with a transform call, this is a bit more flexible and makes it possible to pre-calculate the matrix. """ # Activate the transform context.transform(matrix) # Show the page if we are ready if show_page: context.show_page() # we can now return the next value return context, page # The nup_page function returns the tuple we yielded in our imperative # versions, but it needs the parameters matrix and show_page to know how to # change the context's state. def do_nup(nup, y_trans, context, pages): """With the nup_page function above, we do not need the branches, and without the branches, we can use the provided iteration tools to specify the loop. """ # Scale to get from 'normal' to 'small' resize = sqrt(2) # The height of a single page is: height / ( sqrt(2) ** log(nup, 2)) resize_count = log(nup, 2) # Scale to get from 'normal' to 'small' scale = 1 / (resize ** resize_count) # If y_trans is the height, x_trans is the width x_trans = y_trans / resize # The number of pages on each 'line' return_count = int(sqrt(nup * (1 + resize_count % 2))) # The translation to move back the line x_trans_back = x_trans * (1 - return_count) # The translation to move back the page y_trans_back = y_trans * (1 - nup / return_count) # The transformations: # Calculating these transformations is really quite easy if you read up a # bit on the theory, I find the SVG specification to be a good source. # http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined # Please note that the ordering of the values is different, but the theory # is the same. # However, there is also the non-theory way to get these matrices: # >>> m = Matrix(); m.scale(123, 456); m # cairo.Matrix(123, 0, 0, 456, 0, 0) # >>> m = Matrix(); m.translate(123, 456); m # cairo.Matrix(1, 0, 0, 1, 123, 456) # Here is the result: do_scale = Matrix(scale, 0, 0, scale, 0, 0) move_item = Matrix(1, 0, 0, 1, x_trans, 0) move_line = Matrix(1, 0, 0, 1, x_trans_back, y_trans) move_page = Matrix(1, 0, 0, 1, x_trans_back, y_trans_back) # Chain the transformations to an infinite iterator items_in_line = [move_item] * (return_count - 1) line = items_in_line + [move_line] lines_in_page = line * (nup / return_count - 1) page = lines_in_page + items_in_line + [move_page] inf = cycle(page) # Handle any amount of pages transformations = chain([do_scale], inf) # Chain the special first item # Show the paper when returning to the first page, except the first time show_page_when = chain([False], cycle([False] * (nup - 1) + [True])) # The nup_page function needs the context as first argument cx_nup_page = partial(nup_page, context) # make an iterator with the parameters to cx_nup_page params = izip(transformations, show_page_when, pages) # Map the parameters on cx_nup_page, returning the iterator return starmap(cx_nup_page, params) # do_nup builds 2 intermediate (and infinite) sequences (transformations and # show_page_when), that corresponds with the parameters to nup_page. The # sequence of pages is not infinite, so zipping them together makes a finite # sequence of parameter groups to nup_page, the only thing left is actually # call nup_page for each parameter group. This is handled by starmap. def do_2up_intermediate(y_trans, context, pages): """Having built the rather huge functional-style do_nup, we can now specialize again to the 2up variant. This means dropping a lot of the calculations. """ # Scale to get from 'normal' to 'small' scale = 1 / sqrt(2) # If y_trans is the height, x_trans is the width x_trans = y_trans * scale # The transformations do_scale = Matrix(scale, 0, 0, scale, 0, 0) move_item = Matrix(1, 0, 0, 1, x_trans, 0) move_page = Matrix(1, 0, 0, 1, 0 - x_trans, 0) transformations = chain([do_scale], cycle([move_item, move_page])) # When to show the page show_page_when = chain([False], cycle([False, True])) # The parameters params = izip(transformations, show_page_when, pages) # The nup_page function needs the context as first argument cx_nup_page = partial(nup_page, context) # Map the parameters on cx_nup_page, returning the iterator return starmap(cx_nup_page, params) # The final result: def do_2up(y_trans, context, pages): """As a last step, we can completely remove the intermediate variables, This results in only 1 (somewhat ugly) statement that completely passes the control flow of the 2up implementation to a few builtin high-order functions. """ return starmap( partial(nup_page, context), izip( chain( [Matrix(1 / sqrt(2), 0, 0, 1 / sqrt(2), 0, 0)], cycle([ Matrix(1, 0, 0, 1, y_trans / sqrt(2), 0), Matrix(1, 0, 0, 1, 0 - y_trans / sqrt(2), 0) ]) ), chain([False], cycle([False, True])), pages ) )