Commits

Victor Gavro committed 56fb9b3

initial

  • Participants

Comments (0)

Files changed (1)

+#!/usr/bin/python
+'''
+This small module generates PDF from specified text using fixed-width font,
+determining font-size to stretch text to width without wrapping.
+See docstring of main function for details.
+Author: Victor Gavro
+'''
+
+from reportlab.lib import pagesizes
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.pdfbase import pdfmetrics, ttfonts
+from os.path import basename
+
+def _get_page_size(page_size):
+    if hasattr(pagesizes, page_size):
+        return getattr(pagesizes, page_size)
+    elif len(page_size) !=2 or not hasattr(page_size, '__iter__'):
+        raise ValueError('%s is not valid page size' % pagesize)
+    return page_size
+
+def _get_page_padding(padding):
+    if not hasattr(padding, '__iter__'):
+        padding = (padding, )*4
+    elif len(padding) == 2:
+        # top, right, bottom, left
+        padding = (padding[0], padding[1], padding[0], padding[1])
+    elif len(padding) != 4:
+        raise ValueError('%s is not valid page padding' % padding)
+    return padding
+
+def _get_text_with_params(text):
+    '''
+    Strips text, returns (text, max_length)
+    '''
+    max_length = 0
+    lines = []
+    for line in text.split(u"\n"):
+        line = line.rstrip()
+        if max_length < len(line):
+            max_length = len(line)
+        lines.append(line)
+
+    # deleting empty lines from end
+    for x in reversed(range(len(lines))):
+        if lines[x]:
+            break
+        del lines[x]
+
+    return lines, max_length
+
+def _get_page_params(page_size, page_padding, width_ratio, max_length,
+                     auto_rotate_font_size):
+    '''
+    Function determines best font_size and optional page rotation for text with
+    max_length length, for pdf filled with fixed-with font with width_ratio
+    regarding to font-size.
+
+    page_size is tuple of (page_width, page_height),
+    page_padding is tuple of (top, right, bottom, left) padding,
+    width_ratio (regarding to font-size) (font_size * width_ratio = char_width),
+    max_length is text line maximum length,
+    auto_rotate_font_size is minimum font-size before auto_rotate
+    (if auto_rotate_font_size == None - page never rotates)
+
+    Returns page_size, font_size
+    '''
+
+    page_width, page_height = page_size
+    if auto_rotate_font_size and page_width > page_height:
+        # Always start with portrait orientation if auto_rotate_font_size is on
+        page_width, page_height = page_height, page_width
+
+    side_padding = page_padding[1] + page_padding[3]
+
+    char_width = (page_width - side_padding) / max_length
+    if auto_rotate_font_size \
+       and (char_width < (auto_rotate_font_size * width_ratio)):
+        page_width, page_height = page_height, page_width
+        char_width = (page_width - side_padding) / max_length
+
+    return (page_width, page_height), char_width / width_ratio
+
+def main(file, text, title='Untitled', page_size='A4', page_padding=10,
+         font='/usr/share/fonts/truetype/freefont/FreeMono.ttf',
+         text_leading=1, auto_rotate_font_size=8, max_font_size=18):
+    '''
+    This function generates PDF from specified text using fixed-width font,
+    determining font-size to stretch text to width without wrapping.
+
+    file is file-like object or path for PDF saving,
+    text must be in unicode,
+    title - is title of PDF document,
+    page_size may be tuple or string to import from reportlab.lib.pagesizes,
+    page_padding is 1, 2 or 4 values tuple (according to css),
+    (note that for size and padding you may use reportlab.lib.units module),
+    font is reportlab.pdfbase.ttfbase.TTFont object or path to font file,
+    text_leading (relative to font-size) moves cursor down on line's end,
+    specify auto_rotate_font_size to None to switch off page rotation
+    '''
+
+    if not isinstance(font, ttfonts.TTFont):
+        font = ttfonts.TTFont(basename(font), font)
+    if font.fontName not in pdfmetrics.getRegisteredFontNames():
+        pdfmetrics.registerFont(font)
+
+    # this is width ratio regarding to font-size
+    width_ratio = font.stringWidth('C', 1)
+    if width_ratio != font.stringWidth('1', 1):
+        raise ValueError('Font %s is not fixed-width' % font.fontName)
+
+    page_size = _get_page_size(page_size)
+    page_padding = _get_page_padding(page_padding)
+
+    lines, max_length = _get_text_with_params(text)
+    page_size, font_size = _get_page_params(page_size, page_padding, width_ratio,
+                                            max_length, auto_rotate_font_size)
+    if max_font_size and font_size > max_font_size:
+        font_size = max_font_size
+
+    # count how many lines can be at one page
+    # note that we don't count leading for first line
+    page_lines = int((page_size[1] - page_padding[0] - page_padding[2] -
+                     font_size) / font_size * text_leading) + 1
+
+    c = Canvas(file, pagesize=page_size)
+    c.setTitle(title)
+    c.setAuthor(basename(__file__))
+    for x, line in enumerate(lines):
+        if x % page_lines == 0:
+            if x:
+                c.drawText(t)
+                c.showPage()
+            t = c.beginText()
+            t.setFont(font.fontName, font_size)
+            t.setLeading(font_size * text_leading)
+            t.setTextOrigin(page_padding[3], page_size[1] - page_padding[0]
+                            - font_size)
+        t.textLine(line)
+    if lines:
+        c.drawText(t)
+    c.showPage()
+    c.save()
+
+if __name__ == '__main__':
+    from optparse import OptionParser
+    parser = OptionParser(description='Generates PDF from specified text file '
+                          'using fixed-width font, determines font-size to '
+                          'stretch text to width without wrapping',
+                          usage='usage: %prog [options] INPUT.TXT OUTPUT.PDF')
+    parser.add_option('-c', '--codepage', default='UTF8',
+                      help='input file encoding [default: %default]')
+    parser.add_option('-p', '--page', default='A4',
+                      help='page size [default: %default]')
+    parser.add_option('-b', '--padding', type="float", default=10,
+                      help='page padding [default: %default]')
+    parser.add_option('-f', '--font',
+                      default='/usr/share/fonts/truetype/freefont/FreeMono.ttf',
+                      help='Fixed-with TTFont path [default: %default]')
+    parser.add_option('-l', '--leading', default=1, type="float",
+                      help='lines leading [default: %default]')
+    parser.add_option('-r', '--rotate', default=8, type="float",
+                      help='min font-size for page rotation [default: %default]')
+    parser.add_option('-m', '--max_size', default=18, type="float",
+                      help='max font-size [default: %default]')
+    opts, args = parser.parse_args()
+    if len(args) != 2:
+        parser.print_help()
+        from sys import exit
+        exit('ERROR: INVALID ARGUMENTS COUNT')
+
+    main(file=args[1], text=open(args[0], 'r').read().decode(opts.codepage),
+         title=basename(args[0]), page_size=opts.page,
+         page_padding=opts.padding, font=opts.font,
+         text_leading=opts.leading, auto_rotate_font_size=opts.rotate,
+         max_font_size=opts.max_size)