From e4b83cba87fdd077323831d71402a1775b7435e4 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Mon, 3 Mar 2014 16:10:30 +0100 Subject: Use raw strings in docstrings with backslashes in them --- screenplain/richstring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/screenplain/richstring.py b/screenplain/richstring.py index 43f5949..a72f948 100644 --- a/screenplain/richstring.py +++ b/screenplain/richstring.py @@ -215,7 +215,7 @@ all_styles = (Bold, Italic, Underline) def _unescape(source): - """Converts backslash-escaped stars in a string to the magic + r"""Converts backslash-escaped stars in a string to the magic "literal star" character. >>> _unescape(r'\*hello\*') @@ -226,7 +226,7 @@ def _unescape(source): def _demagic_literals(text): - """Converts "literal star" characters to actual stars: "*" + r"""Converts "literal star" characters to actual stars: "*" >>> _demagic_literals(u'\ue706hello\ue706') u'*hello*' -- cgit v1.2.3 From 0ee0f40642f4e53058027947c07154056a26ad6d Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 3 Apr 2014 00:50:23 +0200 Subject: Add setup script --- .gitignore | 4 ++++ bin/screenplain | 3 --- setup.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100755 setup.py diff --git a/.gitignore b/.gitignore index 52e4e61..2ee284d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.pyc *.pyo + +# Generated by setuptools +MANIFEST +dist/ diff --git a/bin/screenplain b/bin/screenplain index ad6a673..e28cb73 100755 --- a/bin/screenplain +++ b/bin/screenplain @@ -1,9 +1,6 @@ #!/usr/bin/env python import sys -from os.path import dirname, join, abspath, pardir if __name__ == '__main__': - p = abspath(join(dirname(__file__), pardir)) - sys.path.append(p) from screenplain.main import main main(sys.argv[1:]) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8261a7f --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup( + name='screenplain', + version='0.2', + description='Convert text file to viewable screenplay.', + author='Martin Vilcans', + author_email='screenplain@librador.com', + url='http://www.screenplain.com/', + packages=[ + 'screenplain', + 'screenplain.export', + 'screenplain.parsers', + ], + package_data={ + 'screenplain.export': ['default.css'] + }, + scripts=[ + 'bin/screenplain' + ] +) -- cgit v1.2.3 From 0202bad44c99a5ccba793d97fc6debc130a32576 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 3 Apr 2014 23:52:26 +0200 Subject: Travis no longer supports Python 2.5; remove it --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0253859..b4da1b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,5 @@ language: python python: - "2.7" - "2.6" - - "2.5" install: pip install -r requirements.txt script: bin/test -- cgit v1.2.3 From 8ee8e6158284e2c3f776c814c62fe17b4eebc969 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 3 Sep 2014 15:47:16 +0200 Subject: Code consistency using _CreateStyledString --- screenplain/richstring.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/screenplain/richstring.py b/screenplain/richstring.py index a72f948..06adf50 100644 --- a/screenplain/richstring.py +++ b/screenplain/richstring.py @@ -192,7 +192,7 @@ class _CreateStyledString(object): with a single segment with a specified style. """ def __init__(self, styles): - self.styles = styles + self.styles = set(styles) def __call__(self, text): return RichString(Segment(text, self.styles)) @@ -200,9 +200,9 @@ class _CreateStyledString(object): def __add__(self, other): return _CreateStyledString(self.styles.union(other.styles)) -plain = _CreateStyledString(set()) -bold = _CreateStyledString(set((Bold,))) -italic = _CreateStyledString(set((Italic,))) +plain = _CreateStyledString(()) +bold = _CreateStyledString((Bold,)) +italic = _CreateStyledString((Italic,)) underline = _CreateStyledString((Underline,)) empty_string = RichString() -- cgit v1.2.3 From 8f9e7ded799c173e3bddd971f337e2895304c944 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 7 Oct 2014 22:50:50 +0200 Subject: First stab at PDF output --- requirements.txt | 2 +- screenplain/export/pdf.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++ screenplain/main.py | 5 +- setup.py | 3 + 4 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 screenplain/export/pdf.py diff --git a/requirements.txt b/requirements.txt index 3d09f64..321e747 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -#reportlab +reportlab unittest2 nose pep8 diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py new file mode 100644 index 0000000..b43d801 --- /dev/null +++ b/screenplain/export/pdf.py @@ -0,0 +1,138 @@ +# Copyright (c) 2014 Martin Vilcans +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +from reportlab.lib import pagesizes +from reportlab.platypus import BaseDocTemplate, Paragraph, Frame, PageTemplate +from reportlab.lib.units import inch +from reportlab.lib.styles import ParagraphStyle + +from screenplain.types import ( + Action, Dialog, DualDialog, Transition, Slug +) + +lines_per_page = 55 +characters_per_line = 61 +frame_height = 12 * lines_per_page +frame_width = characters_per_line * 72 / 10.0 # Courier pitch is 10 chars/inch +page_width, page_height = pagesizes.letter +left_margin = 1.5 * inch +right_margin = page_width - left_margin - frame_width +top_margin = 1 * inch +bottom_margin = page_height - top_margin - frame_height + +character_width = 1.0 / 10 * inch + +default_style = ParagraphStyle( + 'default', + fontName='Courier', + fontSize=12, + leading=12, + spaceBefore=0, + spaceAfter=0, + leftIndent=0, + rightIndent=0, +) +character_style = ParagraphStyle( + 'character', default_style, + spaceBefore=12, + leftIndent=19 * character_width, + keepWithNext=1, +) +dialog_style = ParagraphStyle( + 'dialog', default_style, + leftIndent=9 * character_width, + rightIndent=frame_width - (45 * character_width), +) +parenthentical_style = ParagraphStyle( + 'parenthentical', default_style, + leftIndent=13 * character_width +) +action_style = ParagraphStyle( + 'action', default_style, + spaceBefore=12, +) +slug_style = ParagraphStyle( + 'slug', default_style, + spaceBefore=12, + spaceAfter=12, + keepWithNext=1, +) +transition_style = ParagraphStyle( + 'transition', default_style, + spaceBefore=12, + spaceAfter=12, +) + + +class DocTemplate(BaseDocTemplate): + def __init__(self, *args, **kwargs): + frame = Frame( + left_margin, bottom_margin, frame_width, frame_height, + id='normal', + leftPadding=0, topPadding=0, rightPadding=0, bottomPadding=0 + ) + pageTemplates = [ + PageTemplate(id='standard', frames=[frame]) + ] + BaseDocTemplate.__init__( + self, pageTemplates=pageTemplates, *args, **kwargs + ) + + def handle_pageBegin(self): + self.canv.setFont('Courier', 12, leading=12) + page = self.page + 1 + if page >= 2: + self.canv.drawRightString( + left_margin + frame_width, + page_height - 42, + '%s.' % page + ) + self._handle_pageBegin() + + +def add_paragraph(story, para, style): + for line in para.lines: + story.append(Paragraph(line.to_html(), style)) + + +def add_dialog(story, dialog): + story.append(Paragraph(dialog.character.to_html(), character_style)) + for parenthetical, line in dialog.blocks: + if parenthetical: + story.append(Paragraph(line.to_html(), parenthentical_style)) + else: + story.append(Paragraph(line.to_html(), dialog_style)) + + +def add_dual_dialog(story, dual): + # TODO: format dual dialog + add_dialog(story, dual.left) + add_dialog(story, dual.right) + + +def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): + doc = template_constructor( + output_filename, + pagesize=(page_width, page_height), + #showBoundary=True, + ) + story = [] + for para in screenplay: + if isinstance(para, Dialog): + add_dialog(story, para) + elif isinstance(para, DualDialog): + add_dual_dialog(story, para) + elif isinstance(para, Action): + # TODO: heed para.centered + add_paragraph(story, para, action_style) + elif isinstance(para, Slug): + add_paragraph(story, para, slug_style) + elif isinstance(para, Transition): + add_paragraph(story, para, transition_style) + #elif isinstance(para, PageBreak): + # TODO: page break + else: + # Ignore unknown types + pass + doc.build(story) diff --git a/screenplain/main.py b/screenplain/main.py index 6def5af..ac308b5 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -4,7 +4,6 @@ # Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php -import fileinput import sys import codecs from optparse import OptionParser @@ -12,7 +11,7 @@ from optparse import OptionParser from screenplain.parsers import fountain output_formats = ( - 'fdx', 'html' + 'fdx', 'html', 'pdf' ) usage = """Usage: %prog [options] [input-file [output-file]] @@ -62,6 +61,8 @@ def main(args): format = 'fdx' elif output_file.endswith('.html'): format = 'html' + elif output_file.endswith('.pdf'): + format = 'pdf' else: invalid_format( parser, diff --git a/setup.py b/setup.py index 8261a7f..6165ef5 100755 --- a/setup.py +++ b/setup.py @@ -9,6 +9,9 @@ setup( author='Martin Vilcans', author_email='screenplain@librador.com', url='http://www.screenplain.com/', + requires=[ + 'reportlab', + ], packages=[ 'screenplain', 'screenplain.export', -- cgit v1.2.3 From 601e5119e79fd4f00fe0f2bc5ae4438e7574a9f6 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 09:42:03 +0200 Subject: Add page break support in PDF output --- screenplain/export/pdf.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index b43d801..62653d5 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -3,13 +3,20 @@ # http://www.opensource.org/licenses/mit-license.php from reportlab.lib import pagesizes -from reportlab.platypus import BaseDocTemplate, Paragraph, Frame, PageTemplate +from reportlab.platypus import ( + BaseDocTemplate, + Paragraph, + Frame, + PageTemplate, +) +from reportlab import platypus from reportlab.lib.units import inch from reportlab.lib.styles import ParagraphStyle from screenplain.types import ( Action, Dialog, DualDialog, Transition, Slug ) +from screenplain import types lines_per_page = 55 characters_per_line = 61 @@ -130,8 +137,8 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): add_paragraph(story, para, slug_style) elif isinstance(para, Transition): add_paragraph(story, para, transition_style) - #elif isinstance(para, PageBreak): - # TODO: page break + elif isinstance(para, types.PageBreak): + story.append(platypus.PageBreak()) else: # Ignore unknown types pass -- cgit v1.2.3 From 42122263807657061682e82c6699e089042f6a96 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 09:42:19 +0200 Subject: Keep parentheticals with next in PDF output --- screenplain/export/pdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 62653d5..dcad855 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -53,7 +53,8 @@ dialog_style = ParagraphStyle( ) parenthentical_style = ParagraphStyle( 'parenthentical', default_style, - leftIndent=13 * character_width + leftIndent=13 * character_width, + keepWithNext=1, ) action_style = ParagraphStyle( 'action', default_style, -- cgit v1.2.3 From 3656b855dadbdbd1a5b30f41b39646bdb07659e9 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 09:53:45 +0200 Subject: Support centered action paragraphs in pdf --- screenplain/export/pdf.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index dcad855..957dd82 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -12,6 +12,7 @@ from reportlab.platypus import ( from reportlab import platypus from reportlab.lib.units import inch from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.enums import TA_CENTER from screenplain.types import ( Action, Dialog, DualDialog, Transition, Slug @@ -60,6 +61,10 @@ action_style = ParagraphStyle( 'action', default_style, spaceBefore=12, ) +centered_action_style = ParagraphStyle( + 'centered-action', action_style, + alignment=TA_CENTER, +) slug_style = ParagraphStyle( 'slug', default_style, spaceBefore=12, @@ -132,8 +137,10 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): elif isinstance(para, DualDialog): add_dual_dialog(story, para) elif isinstance(para, Action): - # TODO: heed para.centered - add_paragraph(story, para, action_style) + add_paragraph( + story, para, + centered_action_style if para.centered else action_style + ) elif isinstance(para, Slug): add_paragraph(story, para, slug_style) elif isinstance(para, Transition): -- cgit v1.2.3 From 2fe3eb633815aa214498884f7959baff4e2d2047 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 09:56:55 +0200 Subject: Make pep8 happy --- screenplain/export/pdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 957dd82..b5f0fb0 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -128,7 +128,6 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): doc = template_constructor( output_filename, pagesize=(page_width, page_height), - #showBoundary=True, ) story = [] for para in screenplay: -- cgit v1.2.3 From cc53331eeed6aa5d68587e7dca430b65a7a9bf5c Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 11:48:57 +0200 Subject: Travis: remove Python 2.6, Reportlab doesn't support it --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b4da1b0..b90b600 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - "2.7" - - "2.6" install: pip install -r requirements.txt script: bin/test -- cgit v1.2.3 From 63843e9b485b13274a6024d34a5bd6a9c07f63e3 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 14:29:57 +0200 Subject: Bump version to 0.3.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6165ef5..f97413c 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.2', + version='0.3.0', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 527ef777921efebf1bda6ef0ccfd724165de8477 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 9 Oct 2014 18:52:57 +0200 Subject: Function to add one line at a time to a Dialog This is useful for parsers that receive character, dialog and parentheticals as separate elements. --- README.markdown | 5 +++++ screenplain/types.py | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index 1979d48..90bd645 100644 --- a/README.markdown +++ b/README.markdown @@ -33,6 +33,11 @@ the master branch may not always work. I'm currently working on supporting the whole [Fountain](http://fountain.io) specification. (Fountain was previously known as "Screenplay Markdown" or "SPMD.") +Installing +========== + + pip install screenplain + Credits ======= diff --git a/screenplain/types.py b/screenplain/types.py index f8454ef..8464ea5 100644 --- a/screenplain/types.py +++ b/screenplain/types.py @@ -46,10 +46,11 @@ class Section(object): class Dialog(object): - def __init__(self, character, lines): + def __init__(self, character, lines=None): self.character = character self.blocks = [] # list of tuples of (is_parenthetical, text) - self._parse(lines) + if lines: + self._parse(lines) def _parse(self, lines): inside_parenthesis = False @@ -60,6 +61,10 @@ class Dialog(object): if line.endswith(')'): inside_parenthesis = False + def add_line(self, line): + parenthetical = line.startswith('(') + self.blocks.append((parenthetical, line)) + class DualDialog(object): def __init__(self, left_dialog, right_dialog): -- cgit v1.2.3 From 1e565b5761129f75258e01666d67f99e91adff47 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 14 Oct 2014 22:29:58 +0200 Subject: Bump to version 0.3.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f97413c..86c53f6 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.3.0', + version='0.3.1', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 58711bbda0c2354b7a173ddebf4809cfbdf2cfad Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 14 Oct 2014 23:09:31 +0200 Subject: Check for Reportlab. Required for PDF output. --- screenplain/export/pdf.py | 9 +++++++++ setup.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index b5f0fb0..76b4d4a 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -2,6 +2,15 @@ # Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php +import sys + +try: + import reportlab +except ImportError: + sys.stderr.write('ERROR: ReportLab is required for PDF output\n') + raise +del reportlab + from reportlab.lib import pagesizes from reportlab.platypus import ( BaseDocTemplate, diff --git a/setup.py b/setup.py index 86c53f6..b158c28 100755 --- a/setup.py +++ b/setup.py @@ -9,9 +9,9 @@ setup( author='Martin Vilcans', author_email='screenplain@librador.com', url='http://www.screenplain.com/', - requires=[ - 'reportlab', - ], + extras_require={ + 'PDF': 'reportlab' + }, packages=[ 'screenplain', 'screenplain.export', -- cgit v1.2.3 From d665b51438372b109bfd6bebe1d06fd110fca85d Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 14 Oct 2014 23:10:42 +0200 Subject: Bump to version 0.3.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b158c28..4d17941 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.3.1', + version='0.3.2', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 5bc3859d931ea9d15824ebc9038bca5530c2f48f Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 15 Oct 2014 00:40:44 +0200 Subject: How to install with PDF support --- README.markdown | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.markdown b/README.markdown index 90bd645..3ebb453 100644 --- a/README.markdown +++ b/README.markdown @@ -38,6 +38,10 @@ Installing pip install screenplain +To enable PDF output, install with the PDF extra (installs ReportLab): + + pip install 'screenplain[PDF]' + Credits ======= -- cgit v1.2.3 From bb97fb5f30c41e82b86eb88eecf44a49b2bd9780 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 15 Oct 2014 17:05:02 +0200 Subject: Add Screenplay class This is to be able to access title page data. --- screenplain/parsers/fountain.py | 26 ++++++++-- screenplain/types.py | 30 ++++++++++++ tests/fountain_test.py | 104 ++++++++++++++++++++-------------------- 3 files changed, 103 insertions(+), 57 deletions(-) diff --git a/screenplain/parsers/fountain.py b/screenplain/parsers/fountain.py index df3d147..9b0c6c7 100644 --- a/screenplain/parsers/fountain.py +++ b/screenplain/parsers/fountain.py @@ -7,7 +7,8 @@ from itertools import takewhile import re from screenplain.types import ( - Slug, Action, Dialog, DualDialog, Transition, Section, PageBreak + Slug, Action, Dialog, DualDialog, Transition, Section, PageBreak, + Screenplay ) from screenplain.richstring import parse_emphasis, plain @@ -205,6 +206,11 @@ def _is_blank(line): def parse(stream): + """Parses Fountain source. + + Returns a Screenplay object. + + """ content = stream.read() content = boneyard_re.sub('', content) lines = linebreak_re.split(content) @@ -213,8 +219,11 @@ def parse(stream): def parse_lines(source): - """Reads raw text input and generates paragraph objects.""" + """Reads raw text input and generates paragraph objects. + + Returns a Screenplay object. + """ source = (_preprocess_line(line) for line in source) title_page_lines = list(takewhile(lambda line: line != '', source)) @@ -223,12 +232,14 @@ def parse_lines(source): if title_page: # The first lines were a title page. # Parse the rest of the source as screenplay body. - # TODO: Create a title page from the data in title_page - return parse_body(source) + return Screenplay(title_page, parse_body(source)) else: # The first lines were not a title page. # Parse them as part of the screenplay body. - return parse_body(itertools.chain(title_page_lines, [''], source)) + return Screenplay( + {}, + parse_body(itertools.chain(title_page_lines, [''], source)) + ) def parse_body(source): @@ -244,7 +255,12 @@ def parse_body(source): def parse_title_page(lines): + """Parse the title page. + Spec: http://fountain.io/syntax#section-titlepage + Returns None if the document does not have a title page section, + otherwise a dictionary with the data. + """ result = {} it = iter(lines) diff --git a/screenplain/types.py b/screenplain/types.py index 8464ea5..6acdce6 100644 --- a/screenplain/types.py +++ b/screenplain/types.py @@ -3,6 +3,36 @@ # http://www.opensource.org/licenses/mit-license.php +class Screenplay(object): + def __init__(self, title_page=None, paragraphs=None): + """ + Create a Screenplay object. + + `title_page` is a dictionary mapping string keys to strings. + `paragraphs` is a sequence of paragraph objects. + """ + + # Key/value pairs for title page + if title_page is None: + self.title_page = {} + else: + self.title_page = title_page + + # The paragraphs of the actual script + if paragraphs is None: + self.paragraphs = [] + else: + self.paragraphs = paragraphs + + def append(self, paragraph): + """Append a paragraph to this screenplay.""" + self.paragraphs.append(paragraph) + + def __iter__(self): + """Get an iterator over the paragraphs of this screenplay.""" + return iter(self.paragraphs) + + class Slug(object): def __init__(self, line, scene_number=None): diff --git a/tests/fountain_test.py b/tests/fountain_test.py index e265da9..9a5d308 100644 --- a/tests/fountain_test.py +++ b/tests/fountain_test.py @@ -14,25 +14,25 @@ from StringIO import StringIO def parse(lines): content = '\n'.join(lines) - return fountain.parse(StringIO(content)) + return list(fountain.parse(StringIO(content))) class SlugTests(TestCase): def test_slug_with_prefix(self): - paras = list(parse([ + paras = parse([ 'INT. SOMEWHERE - DAY', '', 'THIS IS JUST ACTION', - ])) + ]) self.assertEquals([Slug, Action], [type(p) for p in paras]) def test_slug_must_be_single_line(self): - paras = list(parse([ + paras = parse([ 'INT. SOMEWHERE - DAY', 'ANOTHER LINE', '', 'Some action', - ])) + ]) self.assertEquals([Dialog, Action], [type(p) for p in paras]) # What looks like a scene headingis parsed as a character name. # Unexpected perhaps, but that's how I interpreted the spec. @@ -40,10 +40,10 @@ class SlugTests(TestCase): self.assertEquals([plain('Some action')], paras[1].lines) def test_action_is_not_a_slug(self): - paras = list(parse([ + paras = parse([ '', 'THIS IS JUST ACTION', - ])) + ]) self.assertEquals([Action], [type(p) for p in paras]) def test_two_lines_creates_no_slug(self): @@ -151,25 +151,25 @@ class DialogTests(TestCase): # Fountain would not be able to support a character named "23". We # might need a syntax to force a character element. def test_nonalpha_character(self): - paras = list(parse([ + paras = parse([ '23', 'Hello', - ])) + ]) self.assertEquals([Action], [type(p) for p in paras]) def test_twospaced_line_is_not_character(self): - paras = list(parse([ + paras = parse([ 'SCANNING THE AISLES... ', 'Where is that pit boss?', - ])) + ]) self.assertEquals([Action], [type(p) for p in paras]) def test_simple_parenthetical(self): - paras = list(parse([ + paras = parse([ 'STEEL', '(starting the engine)', 'So much for retirement!', - ])) + ]) self.assertEquals(1, len(paras)) dialog = paras[0] self.assertEqual(2, len(dialog.blocks)) @@ -183,12 +183,12 @@ class DialogTests(TestCase): ) def test_twospace_keeps_dialog_together(self): - paras = list(parse([ + paras = parse([ 'SOMEONE', 'One', ' ', 'Two', - ])) + ]) self.assertEquals([Dialog], [type(p) for p in paras]) self.assertEquals([ (False, plain('One')), @@ -197,13 +197,13 @@ class DialogTests(TestCase): ], paras[0].blocks) def test_dual_dialog(self): - paras = list(parse([ + paras = parse([ 'BRICK', 'Fuck retirement.', '', 'STEEL ^', 'Fuck retirement!', - ])) + ]) self.assertEquals([DualDialog], [type(p) for p in paras]) dual = paras[0] self.assertEquals(plain('BRICK'), dual.left.character) @@ -218,12 +218,12 @@ class DialogTests(TestCase): ) def test_dual_dialog_without_previous_dialog_is_ignored(self): - paras = list(parse([ + paras = parse([ 'Brick strolls down the street.', '', 'BRICK ^', 'Nice retirement.', - ])) + ]) self.assertEquals([Action, Dialog], [type(p) for p in paras]) dialog = paras[1] self.assertEqual(plain('BRICK ^'), dialog.character) @@ -232,13 +232,13 @@ class DialogTests(TestCase): ], dialog.blocks) def test_leading_and_trailing_spaces_in_dialog(self): - paras = list(parse([ + paras = parse([ 'JULIET', 'O Romeo, Romeo! wherefore art thou Romeo?', ' Deny thy father and refuse thy name; ', 'Or, if thou wilt not, be but sworn my love,', " And I'll no longer be a Capulet.", - ])) + ]) self.assertEquals([Dialog], [type(p) for p in paras]) self.assertEquals([ (False, plain(u'O Romeo, Romeo! wherefore art thou Romeo?')), @@ -251,84 +251,84 @@ class DialogTests(TestCase): class TransitionTests(TestCase): def test_standard_transition(self): - paras = list(parse([ + paras = parse([ 'Jack begins to argue vociferously in Vietnamese (?)', '', 'CUT TO:', '', "EXT. BRICK'S POOL - DAY", - ])) + ]) self.assertEquals([Action, Transition, Slug], [type(p) for p in paras]) def test_transition_must_end_with_to(self): - paras = list(parse([ + paras = parse([ 'CUT TOO:', '', "EXT. BRICK'S POOL - DAY", - ])) + ]) self.assertEquals([Action, Slug], [type(p) for p in paras]) def test_transition_needs_to_be_upper_case(self): - paras = list(parse([ + paras = parse([ 'Jack begins to argue vociferously in Vietnamese (?)', '', 'cut to:', '', "EXT. BRICK'S POOL - DAY", - ])) + ]) self.assertEquals([Action, Action, Slug], [type(p) for p in paras]) def test_not_a_transition_on_trailing_whitespace(self): - paras = list(parse([ + paras = parse([ 'Jack begins to argue vociferously in Vietnamese (?)', '', 'CUT TO: ', '', "EXT. BRICK'S POOL - DAY", - ])) + ]) self.assertEquals([Action, Action, Slug], [type(p) for p in paras]) def test_transition_does_not_have_to_be_followed_by_slug(self): # The "followed by slug" requirement is gone from the Jan 2012 spec - paras = list(parse([ + paras = parse([ 'Bill lights a cigarette.', '', 'CUT TO:', '', 'SOME GUY mowing the lawn.', - ])) + ]) self.assertEquals( [Action, Transition, Action], [type(p) for p in paras] ) def test_greater_than_sign_means_transition(self): - paras = list(parse([ + paras = parse([ 'Bill blows out the match.', '', '> FADE OUT.', '', '.DARKNESS', - ])) + ]) self.assertEquals([Action, Transition, Slug], [type(p) for p in paras]) self.assertEquals(plain('FADE OUT.'), paras[1].line) def test_centered_text_is_not_parsed_as_transition(self): - paras = list(parse([ + paras = parse([ 'Bill blows out the match.', '', '> THE END. <', '', 'bye!' - ])) + ]) self.assertEquals([Action, Action, Action], [type(p) for p in paras]) def test_transition_at_end(self): - paras = list(parse([ + paras = parse([ 'They stroll hand in hand down the street.', '', '> FADE OUT.', - ])) + ]) self.assertEquals([Action, Transition], [type(p) for p in paras]) self.assertEquals(plain('FADE OUT.'), paras[1].line) @@ -336,12 +336,12 @@ class TransitionTests(TestCase): class ActionTests(TestCase): def test_action_preserves_leading_whitespace(self): - paras = list(parse([ + paras = parse([ 'hello', '', ' two spaces', ' three spaces ', - ])) + ]) self.assertEquals([Action, Action], [type(p) for p in paras]) self.assertEquals( [ @@ -351,7 +351,7 @@ class ActionTests(TestCase): ) def test_single_centered_line(self): - paras = list(parse(['> center me! <'])) + paras = parse(['> center me! <']) self.assertEquals([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) @@ -361,7 +361,7 @@ class ActionTests(TestCase): ' > second! <', '> third!< ', ] - paras = list(parse(lines)) + paras = parse(lines) self.assertEquals([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) self.assertEquals([ @@ -371,11 +371,11 @@ class ActionTests(TestCase): ], paras[0].lines) def test_upper_case_centered_not_parsed_as_dialog(self): - paras = list(parse([ + paras = parse([ '> FIRST! <', ' > SECOND! <', '> THIRD! <', - ])) + ]) self.assertEquals([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) @@ -385,7 +385,7 @@ class ActionTests(TestCase): '> second! <', 'third!', ] - paras = list(parse(lines)) + paras = parse(lines) self.assertEquals([Action], [type(p) for p in paras]) self.assertFalse(paras[0].centered) self.assertEquals([plain(line) for line in lines], paras[0].lines) @@ -393,12 +393,12 @@ class ActionTests(TestCase): class SynopsisTests(TestCase): def test_synopsis_after_slug_adds_synopsis_to_scene(self): - paras = list(parse([ + paras = parse([ "EXT. BRICK'S PATIO - DAY", '', "= Set up Brick & Steel's new life." '', - ])) + ]) self.assertEquals([Slug], [type(p) for p in paras]) self.assertEquals( "Set up Brick & Steel's new life.", @@ -406,11 +406,11 @@ class SynopsisTests(TestCase): ) def test_synopsis_in_section(self): - paras = list(parse([ + paras = parse([ '# section one', '', '= In which we get to know our characters' - ])) + ]) self.assertEquals([Section], [type(p) for p in paras]) self.assertEquals( 'In which we get to know our characters', @@ -418,11 +418,11 @@ class SynopsisTests(TestCase): ) def test_synopsis_syntax_parsed_as_literal(self): - paras = list(parse([ + paras = parse([ 'Some action', '', '= A line that just happens to look like a synopsis' - ])) + ]) self.assertEquals([Action, Action], [type(p) for p in paras]) self.assertEquals( [plain('= A line that just happens to look like a synopsis')], @@ -485,9 +485,9 @@ class TitlePageTests(TestCase): class PageBreakTests(TestCase): def test_page_break_is_parsed(self): - paras = list(parse([ + paras = parse([ '====', '', 'So here we go' - ])) + ]) self.assertEquals([PageBreak, Action], [type(p) for p in paras]) -- cgit v1.2.3 From 2d423932c448546692a8e277f0b50eb087886a2d Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 5 Nov 2014 17:53:11 +0100 Subject: Render title page in PDF output --- screenplain/export/pdf.py | 94 ++++++++++++++++++++++++++++++++++++++++++++--- screenplain/types.py | 14 +++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 76b4d4a..81fe81b 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -17,6 +17,7 @@ from reportlab.platypus import ( Paragraph, Frame, PageTemplate, + Spacer, ) from reportlab import platypus from reportlab.lib.units import inch @@ -50,6 +51,12 @@ default_style = ParagraphStyle( leftIndent=0, rightIndent=0, ) +default_centered_style = ParagraphStyle( + 'default-centered', default_style, + alignment=TA_CENTER, +) + +# Screenplay styles character_style = ParagraphStyle( 'character', default_style, spaceBefore=12, @@ -86,6 +93,18 @@ transition_style = ParagraphStyle( spaceAfter=12, ) +# Title page styles +title_style = ParagraphStyle( + 'title', default_style, + fontSize=24, leading=36, + alignment=TA_CENTER, +) +contact_style = ParagraphStyle( + 'contact', default_style, + leftIndent=2.7 * inch, + rightIndent=0, +) + class DocTemplate(BaseDocTemplate): def __init__(self, *args, **kwargs): @@ -133,12 +152,72 @@ def add_dual_dialog(story, dual): add_dialog(story, dual.right) -def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): - doc = template_constructor( - output_filename, - pagesize=(page_width, page_height), - ) +def get_title_page_story(screenplay): + """Get Platypus flowables for the title page + + """ + # From Fountain spec: + # The recommendation is that Title, Credit, Author (or Authors, either + # is a valid key syntax), and Source will be centered on the page in + # formatted output. Contact and Draft date would be placed at the lower + # left. + + def add_lines(story, attribute, style, space_before=0): + lines = screenplay.get_rich_attribute(attribute) + if not lines: + return 0 + + if space_before: + story.append(Spacer(frame_width, space_before)) + + total_height = 0 + for line in lines: + html = line.to_html() + para = Paragraph(html, style) + width, height = para.wrap(frame_width, frame_height) + story.append(para) + total_height += height + return space_before + total_height + + title_story = [] + title_height = sum(( + add_lines(title_story, 'Title', title_style), + add_lines(title_story, 'Credit', default_centered_style, space_before=12), + add_lines(title_story, 'Author', default_centered_style), + add_lines(title_story, 'Authors', default_centered_style), + add_lines(title_story, 'Source', default_centered_style), + )) + + lower_story = [] + lower_height = sum(( + add_lines(lower_story, 'Draft date', default_style), + add_lines(lower_story, 'Contact', contact_style, space_before=12), + add_lines(lower_story, 'Copyright', default_style, space_before=12), + )) + + if not title_story and not lower_story: + return [] + story = [] + top_space = min( + frame_height / 3.0, + frame_height - lower_height - title_height + ) + if top_space > 0: + story.append(Spacer(frame_width, top_space)) + story += title_story + middle_space = frame_height - top_space - title_height - lower_height + if middle_space > 0: + story.append(Spacer(frame_width, middle_space)) + story += lower_story + + story.append(platypus.PageBreak()) + return story + + +def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): + story = get_title_page_story(screenplay) + for para in screenplay: if isinstance(para, Dialog): add_dialog(story, para) @@ -158,4 +237,9 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): else: # Ignore unknown types pass + + doc = template_constructor( + output_filename, + pagesize=(page_width, page_height), + ) doc.build(story) diff --git a/screenplain/types.py b/screenplain/types.py index 6acdce6..0325148 100644 --- a/screenplain/types.py +++ b/screenplain/types.py @@ -2,6 +2,8 @@ # Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php +from screenplain.richstring import parse_emphasis + class Screenplay(object): def __init__(self, title_page=None, paragraphs=None): @@ -24,6 +26,18 @@ class Screenplay(object): else: self.paragraphs = paragraphs + def get_rich_attribute(self, name, default=[]): + """Get an attribute from the title page parsed into a RichString. + Returns a list of RichString objects. + + E.g. `screenplay.get_rich_attribute('Title')` + + """ + if name in self.title_page: + return [parse_emphasis(line) for line in self.title_page[name]] + else: + return default + def append(self, paragraph): """Append a paragraph to this screenplay.""" self.paragraphs.append(paragraph) -- cgit v1.2.3 From 617c1ab0ae79984c9e3064c7263422ef2ea808c2 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 5 Nov 2014 22:32:43 +0100 Subject: 82 characters in a line: a flagrant PEP8 violation! --- screenplain/export/pdf.py | 10 +++++----- setup.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 81fe81b..a22b933 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -51,7 +51,7 @@ default_style = ParagraphStyle( leftIndent=0, rightIndent=0, ) -default_centered_style = ParagraphStyle( +centered_style = ParagraphStyle( 'default-centered', default_style, alignment=TA_CENTER, ) @@ -182,10 +182,10 @@ def get_title_page_story(screenplay): title_story = [] title_height = sum(( add_lines(title_story, 'Title', title_style), - add_lines(title_story, 'Credit', default_centered_style, space_before=12), - add_lines(title_story, 'Author', default_centered_style), - add_lines(title_story, 'Authors', default_centered_style), - add_lines(title_story, 'Source', default_centered_style), + add_lines(title_story, 'Credit', centered_style, space_before=12), + add_lines(title_story, 'Author', centered_style), + add_lines(title_story, 'Authors', centered_style), + add_lines(title_story, 'Source', centered_style), )) lower_story = [] diff --git a/setup.py b/setup.py index 4d17941..e4ad1ab 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.3.2', + version='0.4.0', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 304d397c163338e3b6caaca9f729a6d5e72f0b3b Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 12 Nov 2014 15:57:12 +0100 Subject: Title page: 6 points of buffer to avoid blank page Otherwise, for some input I got a blank second page. --- screenplain/export/pdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index a22b933..6c2f1a6 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -206,7 +206,8 @@ def get_title_page_story(screenplay): if top_space > 0: story.append(Spacer(frame_width, top_space)) story += title_story - middle_space = frame_height - top_space - title_height - lower_height + # The minus 6 adds some room for rounding errors and whatnot + middle_space = frame_height - top_space - title_height - lower_height - 6 if middle_space > 0: story.append(Spacer(frame_width, middle_space)) story += lower_story -- cgit v1.2.3 From 2bbeaeb2cab56631a635379a0fbbf9c25da7920d Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 12 Nov 2014 16:09:39 +0100 Subject: Don't count title page when numbering pages --- screenplain/export/pdf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 6c2f1a6..13218f5 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -108,6 +108,7 @@ contact_style = ParagraphStyle( class DocTemplate(BaseDocTemplate): def __init__(self, *args, **kwargs): + self.has_title_page = kwargs.pop('has_title_page', False) frame = Frame( left_margin, bottom_margin, frame_width, frame_height, id='normal', @@ -122,7 +123,10 @@ class DocTemplate(BaseDocTemplate): def handle_pageBegin(self): self.canv.setFont('Courier', 12, leading=12) - page = self.page + 1 + if self.has_title_page: + page = self.page # self.page is 0 on first page + else: + page = self.page + 1 if page >= 2: self.canv.drawRightString( left_margin + frame_width, @@ -218,6 +222,7 @@ def get_title_page_story(screenplay): def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): story = get_title_page_story(screenplay) + has_title_page = bool(story) for para in screenplay: if isinstance(para, Dialog): @@ -242,5 +247,6 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): doc = template_constructor( output_filename, pagesize=(page_width, page_height), + has_title_page=has_title_page ) doc.build(story) -- cgit v1.2.3 From 1913d5f2ec048899ab47c62948b79ddf62b8d5db Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 13 Nov 2014 12:48:46 +0100 Subject: Bump version to 0.5.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e4ad1ab..5f624d1 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.4.0', + version='0.5.0', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From d56a7ab86934e1f77c261cbcff40a07776295c60 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 26 Nov 2014 12:01:06 +0100 Subject: Slight layout changes to cover page --- screenplain/export/pdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 13218f5..ee038b1 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -101,7 +101,7 @@ title_style = ParagraphStyle( ) contact_style = ParagraphStyle( 'contact', default_style, - leftIndent=2.7 * inch, + leftIndent=3.9 * inch, rightIndent=0, ) @@ -196,7 +196,7 @@ def get_title_page_story(screenplay): lower_height = sum(( add_lines(lower_story, 'Draft date', default_style), add_lines(lower_story, 'Contact', contact_style, space_before=12), - add_lines(lower_story, 'Copyright', default_style, space_before=12), + add_lines(lower_story, 'Copyright', centered_style, space_before=12), )) if not title_story and not lower_story: -- cgit v1.2.3 From 1eadd7485573843b2ac5e5f5dbd5005d827cded1 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 26 Nov 2014 12:01:37 +0100 Subject: Bump to version 0.5.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f624d1..3ef80f8 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.5.0', + version='0.5.1', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 0c67ec55781b12c0f6b194073f62833e460dd615 Mon Sep 17 00:00:00 2001 From: michaelx386 Date: Fri, 3 Apr 2015 10:38:12 +0100 Subject: Right align transitions in PDF output --- screenplain/export/pdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index ee038b1..5f4380f 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -22,7 +22,7 @@ from reportlab.platypus import ( from reportlab import platypus from reportlab.lib.units import inch from reportlab.lib.styles import ParagraphStyle -from reportlab.lib.enums import TA_CENTER +from reportlab.lib.enums import TA_CENTER, TA_RIGHT from screenplain.types import ( Action, Dialog, DualDialog, Transition, Slug @@ -91,6 +91,7 @@ transition_style = ParagraphStyle( 'transition', default_style, spaceBefore=12, spaceAfter=12, + alignment=TA_RIGHT, ) # Title page styles -- cgit v1.2.3 From f2f57ec054094bdbf7499b3595b22b86311df726 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Fri, 3 Apr 2015 14:55:15 +0200 Subject: Ignore PEP8 error about imports not at top of file This appeared after pep8 version 1.6.0. See jcrocholl/pep8#264 --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index dcefe4f..2d35baf 100755 --- a/bin/test +++ b/bin/test @@ -1,3 +1,3 @@ #!/bin/bash nosetests --nocapture --with-doctest --doctest-tests $* && \ - pep8 screenplain tests + pep8 --ignore=E402 screenplain tests -- cgit v1.2.3 From ea7685a9b0b949a58e216c0b1488e3c240508681 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Fri, 3 Apr 2015 15:00:11 +0200 Subject: Use `pip install -e .` in development instructions --- .gitignore | 3 +++ README.markdown | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 2ee284d..3b4120c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.pyc *.pyo +# Generated by `pip install -e .` +*.egg-info + # Generated by setuptools MANIFEST dist/ diff --git a/README.markdown b/README.markdown index 3ebb453..ce453af 100644 --- a/README.markdown +++ b/README.markdown @@ -69,3 +69,10 @@ Set up environment using virtualenvwrapper: mkvirtualenv --no-site-packages screenplain pip install -r requirements.txt + pip install -e . + +After this, the `screenplain` command will use the working copy of your code. + +To run unit tests and style checks, run: + + bin/test -- cgit v1.2.3 From d7a223968547cbe713845249c90f607574ed73df Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Fri, 3 Apr 2015 18:17:21 +0200 Subject: Bumped version to 0.6.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ef80f8..81eda93 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.5.1', + version='0.6.0', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 8b154c0d5b9268d5e41a7774d6e87bbbac1a33e4 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 23 Jul 2015 09:33:21 +0200 Subject: Add test for dual dialogue in FDX output --- tests/files/dual-dialogue.fountain.fdx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/files/dual-dialogue.fountain.fdx diff --git a/tests/files/dual-dialogue.fountain.fdx b/tests/files/dual-dialogue.fountain.fdx new file mode 100644 index 0000000..16d022e --- /dev/null +++ b/tests/files/dual-dialogue.fountain.fdx @@ -0,0 +1,21 @@ + + + + + + + GIRL + + + Hey! + + + GUY + + + Hello! + + + + + -- cgit v1.2.3 From fd832267cf7b55b4d3cdc56e313eb2bec215cc55 Mon Sep 17 00:00:00 2001 From: michaelx386 Date: Fri, 18 Sep 2015 17:51:49 +0100 Subject: Parse files with BOM present; fixes #5 --- screenplain/main.py | 2 +- tests/files/utf-8-bom.fountain | 18 ++++++++++++++++ tests/files/utf-8-bom.fountain.fdx | 43 +++++++++++++++++++++++++++++++++++++ tests/files/utf-8-bom.fountain.html | 6 ++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/files/utf-8-bom.fountain create mode 100644 tests/files/utf-8-bom.fountain.fdx create mode 100644 tests/files/utf-8-bom.fountain.html diff --git a/screenplain/main.py b/screenplain/main.py index ac308b5..43e44a1 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -75,7 +75,7 @@ def main(args): ) if input_file: - input = codecs.open(input_file, 'r', 'utf-8') + input = codecs.open(input_file, 'r', 'utf-8-sig') else: input = codecs.getreader('utf-8')(sys.stdin) screenplay = fountain.parse(input) diff --git a/tests/files/utf-8-bom.fountain b/tests/files/utf-8-bom.fountain new file mode 100644 index 0000000..ddfe1f9 --- /dev/null +++ b/tests/files/utf-8-bom.fountain @@ -0,0 +1,18 @@ +EXT. SOMEWHERE - DAY + +GUY and GURL walk down the street. + +It's a sunny day. +Sunnier than normal. +Too sunny to be funny. + +GUY +So what's up? + +GURL +Nothing much. +Just thinking. +And you? + +GUY +Nothing. \ No newline at end of file diff --git a/tests/files/utf-8-bom.fountain.fdx b/tests/files/utf-8-bom.fountain.fdx new file mode 100644 index 0000000..9701f3a --- /dev/null +++ b/tests/files/utf-8-bom.fountain.fdx @@ -0,0 +1,43 @@ + + + + + + EXT. SOMEWHERE - DAY + + + GUY and GURL walk down the street. + + + It's a sunny day. + + Sunnier than normal. + + Too sunny to be funny. + + + GUY + + + So what's up? + + + GURL + + + Nothing much. + + + Just thinking. + + + And you? + + + GUY + + + Nothing. + + + diff --git a/tests/files/utf-8-bom.fountain.html b/tests/files/utf-8-bom.fountain.html new file mode 100644 index 0000000..70043dd --- /dev/null +++ b/tests/files/utf-8-bom.fountain.html @@ -0,0 +1,6 @@ +
EXT. SOMEWHERE - DAY
+

GUY and GURL walk down the street.

+

It's a sunny day.
Sunnier than normal.
Too sunny to be funny.

+

GUY

So what's up?

+

GURL

Nothing much.

Just thinking.

And you?

+

GUY

Nothing.

-- cgit v1.2.3 From efefca365ef9ab4413bb1ad8dc97e6b28a931202 Mon Sep 17 00:00:00 2001 From: Charney Kaye Date: Thu, 17 Sep 2015 21:29:50 -0400 Subject: PDF exports Scene Headings as Bold and Underlined; fixes #14 --- screenplain/export/pdf.py | 14 ++++++++++++-- screenplain/main.py | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 5f4380f..c5a9ee9 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -142,6 +142,15 @@ def add_paragraph(story, para, style): story.append(Paragraph(line.to_html(), style)) +def add_slug(story, para, style, is_strong): + for line in para.lines: + if is_strong: + html = '' + line.to_html() + '' + else: + html = line.to_html() + story.append(Paragraph(html, style)) + + def add_dialog(story, dialog): story.append(Paragraph(dialog.character.to_html(), character_style)) for parenthetical, line in dialog.blocks: @@ -221,7 +230,8 @@ def get_title_page_story(screenplay): return story -def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): +def to_pdf(screenplay, output_filename, is_strong): + template_constructor = DocTemplate story = get_title_page_story(screenplay) has_title_page = bool(story) @@ -236,7 +246,7 @@ def to_pdf(screenplay, output_filename, template_constructor=DocTemplate): centered_action_style if para.centered else action_style ) elif isinstance(para, Slug): - add_paragraph(story, para, slug_style) + add_slug(story, para, slug_style, is_strong) elif isinstance(para, Transition): add_paragraph(story, para, transition_style) elif isinstance(para, types.PageBreak): diff --git a/screenplain/main.py b/screenplain/main.py index 43e44a1..3d5e711 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -49,6 +49,15 @@ def main(args): 'not a complete HTML document.' ) ) + parser.add_option( + '--strong', + action='store_true', + dest='strong', + help=( + 'For PDF output, scene headings will appear ' + 'Bold and Underlined.' + ) + ) options, args = parser.parse_args(args) if len(args) >= 3: parser.error('Too many arguments') @@ -85,7 +94,7 @@ def main(args): if not output_file: sys.stderr.write("Can't write PDF to standard output") sys.exit(2) - to_pdf(screenplay, output_file) + to_pdf(screenplay, output_file, options.strong) else: if output_file: output = codecs.open(output_file, 'w', 'utf-8') -- cgit v1.2.3 From f52103e567f18c652b8b9f28bbe8a1d754a78471 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Mon, 12 Oct 2015 09:15:47 +0200 Subject: Restore template_constructor argument in to_pdf --- screenplain/export/pdf.py | 7 +++++-- screenplain/main.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index c5a9ee9..83f3474 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -230,8 +230,11 @@ def get_title_page_story(screenplay): return story -def to_pdf(screenplay, output_filename, is_strong): - template_constructor = DocTemplate +def to_pdf( + screenplay, output_filename, + template_constructor=DocTemplate, + is_strong=False, +): story = get_title_page_story(screenplay) has_title_page = bool(story) diff --git a/screenplain/main.py b/screenplain/main.py index 3d5e711..ba6fc10 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -94,7 +94,7 @@ def main(args): if not output_file: sys.stderr.write("Can't write PDF to standard output") sys.exit(2) - to_pdf(screenplay, output_file, options.strong) + to_pdf(screenplay, output_file, is_strong=options.strong) else: if output_file: output = codecs.open(output_file, 'w', 'utf-8') -- cgit v1.2.3 From d4a721b470a960e71a683b8ebff61b26c868d1cd Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 17 Nov 2015 08:49:32 +0100 Subject: Support non-alphanumeric character names with "@" Fixes #22 --- screenplain/parsers/fountain.py | 6 +++++- tests/fountain_test.py | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/screenplain/parsers/fountain.py b/screenplain/parsers/fountain.py index 9b0c6c7..3290dd2 100644 --- a/screenplain/parsers/fountain.py +++ b/screenplain/parsers/fountain.py @@ -128,7 +128,11 @@ class InputParagraph(object): return False character = self.lines[0] - if not character.isupper() or character.endswith(TWOSPACE): + if character.endswith(TWOSPACE): + return False + if character.startswith('@') and len(character) >= 2: + character = character[1:] + elif not character.isupper(): return False if paragraphs and isinstance(paragraphs[-1], Dialog): diff --git a/tests/fountain_test.py b/tests/fountain_test.py index 9a5d308..95a4a81 100644 --- a/tests/fountain_test.py +++ b/tests/fountain_test.py @@ -148,8 +148,10 @@ class DialogTests(TestCase): self.assertEquals(Dialog, type(dialog)) self.assertEquals(plain('SOME GUY'), dialog.character) - # Fountain would not be able to support a character named "23". We - # might need a syntax to force a character element. + + # Spec http://fountain.io/syntax#section-character: + # Character names must include at least one alphabetical character. + # "R2D2" works, but "23" does not. def test_nonalpha_character(self): paras = parse([ '23', @@ -157,6 +159,16 @@ class DialogTests(TestCase): ]) self.assertEquals([Action], [type(p) for p in paras]) + # Spec http://fountain.io/syntax#section-character: + # You can force a Character element by preceding it with the "at" symbol @. + def test_at_sign_forces_dialog(self): + paras = parse([ + '@McCLANE', + 'Yippee ki-yay', + ]) + self.assertEquals([Dialog], [type(p) for p in paras]) + self.assertEquals(plain('McCLANE'), paras[0].character) + def test_twospaced_line_is_not_character(self): paras = parse([ 'SCANNING THE AISLES... ', -- cgit v1.2.3 From fe3a3c714221288b81127beee389b0431cad8527 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 17 Nov 2015 08:54:16 +0100 Subject: Test that alphanumeric character names work --- tests/fountain_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/fountain_test.py b/tests/fountain_test.py index 95a4a81..fff1e48 100644 --- a/tests/fountain_test.py +++ b/tests/fountain_test.py @@ -148,6 +148,13 @@ class DialogTests(TestCase): self.assertEquals(Dialog, type(dialog)) self.assertEquals(plain('SOME GUY'), dialog.character) + def test_alphanumeric_character(self): + paras = parse([ + 'R2D2', + 'Bee-bop', + ]) + self.assertEquals([Dialog], [type(p) for p in paras]) + self.assertEquals(plain('R2D2'), paras[0].character) # Spec http://fountain.io/syntax#section-character: # Character names must include at least one alphabetical character. -- cgit v1.2.3 From b6dce18fbc850c4e8c39ac1405ac6261666697f1 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 26 Nov 2015 21:23:11 +0100 Subject: Possible to specify which CSS file to use Closes #24 --- screenplain/export/html.py | 13 ++++++++----- screenplain/main.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/screenplain/export/html.py b/screenplain/export/html.py index b987d81..625fcb4 100644 --- a/screenplain/export/html.py +++ b/screenplain/export/html.py @@ -166,12 +166,11 @@ class Formatter(object): def _read_file(filename): - path = os.path.join(os.path.dirname(__file__), filename) with open(path) as stream: return stream.read() -def convert(screenplay, out, bare=False): +def convert(screenplay, out, css_file=None, bare=False): """Convert the screenplay into HTML, written to the file-like object `out`. The output will be a complete HTML document unless `bare` is true. @@ -180,15 +179,19 @@ def convert(screenplay, out, bare=False): if bare: convert_bare(screenplay, out) else: - convert_full(screenplay, out) + convert_full( + screenplay, out, + css_file or os.path.join(os.path.dirname(__file__), 'default.css') + ) -def convert_full(screenplay, out): +def convert_full(screenplay, out, css_file): """Convert the screenplay into a complete HTML document, written to the file-like object `out`. """ - css = _read_file('default.css') + with open(css_file, 'r') as stream: + css = stream.read() out.write( '\n' '' diff --git a/screenplain/main.py b/screenplain/main.py index ba6fc10..4469489 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -49,6 +49,14 @@ def main(args): 'not a complete HTML document.' ) ) + parser.add_option( + '--css', + metavar='FILE', + help=( + 'For HTML output, inline the given CSS file in the HTML document ' + 'instead of the default.' + ) + ) parser.add_option( '--strong', action='store_true', @@ -109,7 +117,10 @@ def main(args): to_fdx(screenplay, output) elif format == 'html': from screenplain.export.html import convert - convert(screenplay, output, bare=options.bare) + convert( + screenplay, output, + css_file=options.css, bare=options.bare + ) finally: if output_file: output.close() -- cgit v1.2.3 From 73e442358e7f980adc98ccfe71c3d29c300e84be Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 1 Dec 2015 22:59:45 +0100 Subject: Bump version to 0.7.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81eda93..c5601f9 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from distutils.core import setup setup( name='screenplain', - version='0.6.0', + version='0.7.0', description='Convert text file to viewable screenplay.', author='Martin Vilcans', author_email='screenplain@librador.com', -- cgit v1.2.3 From 99abd9ad27b1c80909bb37e7cb21dedfc5046f40 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Tue, 1 Dec 2015 23:47:10 +0100 Subject: Support writing PDF to stdout --- screenplain/main.py | 56 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/screenplain/main.py b/screenplain/main.py index 4469489..770427c 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -98,32 +98,40 @@ def main(args): screenplay = fountain.parse(input) if format == 'pdf': - from screenplain.export.pdf import to_pdf - if not output_file: - sys.stderr.write("Can't write PDF to standard output") - sys.exit(2) - to_pdf(screenplay, output_file, is_strong=options.strong) + output_encoding = None else: - if output_file: - output = codecs.open(output_file, 'w', 'utf-8') + output_encoding = 'utf-8' + + if output_file: + if output_encoding: + output = codecs.open(output_file, 'w', output_encoding) + else: + output = open(output_file, 'wb') + else: + if output_encoding: + output = codecs.getwriter(output_encoding)(sys.stdout) else: - output = codecs.getwriter('utf-8')(sys.stdout) - try: - if format == 'text': - from screenplain.export.text import to_text - to_text(screenplay, output) - elif format == 'fdx': - from screenplain.export.fdx import to_fdx - to_fdx(screenplay, output) - elif format == 'html': - from screenplain.export.html import convert - convert( - screenplay, output, - css_file=options.css, bare=options.bare - ) - finally: - if output_file: - output.close() + output = sys.stdout + + try: + if format == 'text': + from screenplain.export.text import to_text + to_text(screenplay, output) + elif format == 'fdx': + from screenplain.export.fdx import to_fdx + to_fdx(screenplay, output) + elif format == 'html': + from screenplain.export.html import convert + convert( + screenplay, output, + css_file=options.css, bare=options.bare + ) + elif format == 'pdf': + from screenplain.export.pdf import to_pdf + to_pdf(screenplay, output, is_strong=options.strong) + finally: + if output_file: + output.close() if __name__ == '__main__': main(sys.argv[1:]) -- cgit v1.2.3 From 355b16004a57d5b23ef0e31adb818b5dabae3c86 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Wed, 13 Apr 2016 23:40:46 +0200 Subject: Replace two spaces with   in PDF output too --- screenplain/export/html.py | 6 ++---- screenplain/richstring.py | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/screenplain/export/html.py b/screenplain/export/html.py index 625fcb4..1f88e64 100644 --- a/screenplain/export/html.py +++ b/screenplain/export/html.py @@ -3,9 +3,6 @@ # http://www.opensource.org/licenses/mit-license.php from __future__ import with_statement -import sys -import re -import cgi import os import os.path @@ -60,7 +57,8 @@ def to_html(text): html = text.to_html() if html == '': return ' ' - return re.sub(' ', '  ', html) + else: + return html class Formatter(object): diff --git a/screenplain/richstring.py b/screenplain/richstring.py index 06adf50..9470c7d 100644 --- a/screenplain/richstring.py +++ b/screenplain/richstring.py @@ -109,7 +109,11 @@ class Segment(object): ordered_styles = self.get_ordered_styles() return ( ''.join(style.start_html for style in ordered_styles) + - cgi.escape(self.text).encode('ascii', 'xmlcharrefreplace') + + re.sub( + ' ', # two spaces + '  ', + cgi.escape(self.text).encode('ascii', 'xmlcharrefreplace'), + ) + ''.join(style.end_html for style in reversed(ordered_styles)) ) -- cgit v1.2.3 From e76e044308a8d3b5fa93bc9a2657897c480bb782 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 14 Apr 2016 00:10:15 +0200 Subject: Show multiple spaces in a row as is in HTML & PDF Not sure if FDX output should use   Let's keep using spaces for now. Closes #30 --- screenplain/richstring.py | 10 +++++++--- tests/files/indentation.fountain | 11 +++++++++++ tests/files/indentation.fountain.fdx | 24 ++++++++++++++++++++++++ tests/files/indentation.fountain.html | 6 ++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/files/indentation.fountain create mode 100644 tests/files/indentation.fountain.fdx create mode 100644 tests/files/indentation.fountain.html diff --git a/screenplain/richstring.py b/screenplain/richstring.py index 9470c7d..e24db1a 100644 --- a/screenplain/richstring.py +++ b/screenplain/richstring.py @@ -45,7 +45,11 @@ class RichString(object): return self.segments[-1].text.endswith(string) def to_html(self): - return ''.join(seg.to_html() for seg in self.segments) + html = ''.join(seg.to_html() for seg in self.segments) + if html.startswith(' '): + return ' ' + html[1:] + else: + return html def __eq__(self, other): return ( @@ -110,8 +114,8 @@ class Segment(object): return ( ''.join(style.start_html for style in ordered_styles) + re.sub( - ' ', # two spaces - '  ', + ' +', # at least two spaces + lambda m: ' ' * (len(m.group(0)) - 1) + ' ', cgi.escape(self.text).encode('ascii', 'xmlcharrefreplace'), ) + ''.join(style.end_html for style in reversed(ordered_styles)) diff --git a/tests/files/indentation.fountain b/tests/files/indentation.fountain new file mode 100644 index 0000000..1640032 --- /dev/null +++ b/tests/files/indentation.fountain @@ -0,0 +1,11 @@ +EXT. INDENTATION TEST + + Four spaces + + Three spaces + + Two spaces + + One space + +No spaces diff --git a/tests/files/indentation.fountain.fdx b/tests/files/indentation.fountain.fdx new file mode 100644 index 0000000..958417f --- /dev/null +++ b/tests/files/indentation.fountain.fdx @@ -0,0 +1,24 @@ + + + + + + EXT. INDENTATION TEST + + + Four spaces + + + Three spaces + + + Two spaces + + + One space + + + No spaces + + + diff --git a/tests/files/indentation.fountain.html b/tests/files/indentation.fountain.html new file mode 100644 index 0000000..491b515 --- /dev/null +++ b/tests/files/indentation.fountain.html @@ -0,0 +1,6 @@ +
EXT. INDENTATION TEST
+

    Four spaces

+

   Three spaces

+

  Two spaces

+

 One space

+

No spaces

-- cgit v1.2.3 From 006f03ff4490f67cb6afd401ca55c16ea61cbf1c Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 23 Feb 2017 00:09:39 +0100 Subject: Get rid of confusing PDF frame width calculation --- screenplain/export/pdf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 83f3474..bdb7011 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -31,15 +31,16 @@ from screenplain import types lines_per_page = 55 characters_per_line = 61 +character_width = 1.0 / 10 * inch # Courier pitch is 10 chars/inch frame_height = 12 * lines_per_page -frame_width = characters_per_line * 72 / 10.0 # Courier pitch is 10 chars/inch +frame_width = characters_per_line * character_width + page_width, page_height = pagesizes.letter left_margin = 1.5 * inch right_margin = page_width - left_margin - frame_width top_margin = 1 * inch bottom_margin = page_height - top_margin - frame_height -character_width = 1.0 / 10 * inch default_style = ParagraphStyle( 'default', -- cgit v1.2.3 From da23ebd22d32398f649fb41ecb1ad7e93e223390 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 23 Feb 2017 00:20:10 +0100 Subject: Get rid of literal 12 for font size/spacing --- screenplain/export/pdf.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index bdb7011..02ffd63 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -29,10 +29,12 @@ from screenplain.types import ( ) from screenplain import types +font_size = 12 +line_height = 12 lines_per_page = 55 characters_per_line = 61 character_width = 1.0 / 10 * inch # Courier pitch is 10 chars/inch -frame_height = 12 * lines_per_page +frame_height = line_height * lines_per_page frame_width = characters_per_line * character_width page_width, page_height = pagesizes.letter @@ -45,8 +47,8 @@ bottom_margin = page_height - top_margin - frame_height default_style = ParagraphStyle( 'default', fontName='Courier', - fontSize=12, - leading=12, + fontSize=font_size, + leading=line_height, spaceBefore=0, spaceAfter=0, leftIndent=0, @@ -60,7 +62,7 @@ centered_style = ParagraphStyle( # Screenplay styles character_style = ParagraphStyle( 'character', default_style, - spaceBefore=12, + spaceBefore=line_height, leftIndent=19 * character_width, keepWithNext=1, ) @@ -76,7 +78,7 @@ parenthentical_style = ParagraphStyle( ) action_style = ParagraphStyle( 'action', default_style, - spaceBefore=12, + spaceBefore=line_height, ) centered_action_style = ParagraphStyle( 'centered-action', action_style, @@ -84,14 +86,14 @@ centered_action_style = ParagraphStyle( ) slug_style = ParagraphStyle( 'slug', default_style, - spaceBefore=12, - spaceAfter=12, + spaceBefore=line_height, + spaceAfter=line_height, keepWithNext=1, ) transition_style = ParagraphStyle( 'transition', default_style, - spaceBefore=12, - spaceAfter=12, + spaceBefore=line_height, + spaceAfter=line_height, alignment=TA_RIGHT, ) @@ -124,7 +126,7 @@ class DocTemplate(BaseDocTemplate): ) def handle_pageBegin(self): - self.canv.setFont('Courier', 12, leading=12) + self.canv.setFont('Courier', font_size, leading=line_height) if self.has_title_page: page = self.page # self.page is 0 on first page else: @@ -197,7 +199,9 @@ def get_title_page_story(screenplay): title_story = [] title_height = sum(( add_lines(title_story, 'Title', title_style), - add_lines(title_story, 'Credit', centered_style, space_before=12), + add_lines( + title_story, 'Credit', centered_style, space_before=line_height + ), add_lines(title_story, 'Author', centered_style), add_lines(title_story, 'Authors', centered_style), add_lines(title_story, 'Source', centered_style), @@ -206,8 +210,12 @@ def get_title_page_story(screenplay): lower_story = [] lower_height = sum(( add_lines(lower_story, 'Draft date', default_style), - add_lines(lower_story, 'Contact', contact_style, space_before=12), - add_lines(lower_story, 'Copyright', centered_style, space_before=12), + add_lines( + lower_story, 'Contact', contact_style, space_before=line_height + ), + add_lines( + lower_story, 'Copyright', centered_style, space_before=line_height + ), )) if not title_story and not lower_story: -- cgit v1.2.3 From d9eb1a980798ff54ac9cd81ff1821f78aa57156b Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 23 Feb 2017 00:40:21 +0100 Subject: PDF output: No space between lines in multi-line Action paragraphs --- screenplain/export/pdf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 02ffd63..6955616 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -141,8 +141,10 @@ class DocTemplate(BaseDocTemplate): def add_paragraph(story, para, style): - for line in para.lines: - story.append(Paragraph(line.to_html(), style)) + story.append(Paragraph( + '
'.join(line.to_html() for line in para.lines), + style + )) def add_slug(story, para, style, is_strong): -- cgit v1.2.3