Convert a screenplay‑flavoured Markdown file into a properly formatted PDF (monospaced US Letter layout) using a lightweight Python script (no LaTeX / no external formatter required).
- Scene headings (
### INT. LOCATION - TIME) - Action paragraphs (plain text)
- Character cues (
@CHARACTER) - Parentheticals inside dialogue lines (
(whispering)) - Dialogue (lines following a character cue until a blank line)
- Transitions (
>> CUT TO:) – right aligned - Shot headings (
! CLOSE ON) - Forced page breaks (
---) – always start a new page - Bold scene headings (auto bold font variant; simulated if bold face missing)
- Block comments starting with
//are ignored - Block quotes / notes starting with single
>are ignored (except>>transitions) - Adjustable font, size, and transition right margin
- Optional shot list export (
--shot-list markdown|csv) capturing scenes & shot headings with a first action summary snippet - Shot list PDF export (
--shot-list-pdf) including optional landscape layout - Entity inventory (characters, locations, objects) appended to shot list outputs (
--entities) - Landscape shot list PDF option for wider columns (
--shot-list-landscape) - Razor‑sharp vector screenplay PDF rendering (
--vectorvia ReportLab) instead of raster images - Final Cut Pro timeline export (FCPXML) with scene markers & shot keyword ranges (
--fcpxml)
Requires Python 3.8+ and Pillow.
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install Pillow(If Pillow is already installed you can skip.)
python screenmd2pdf.py INPUT.md OUTPUT.pdf [options]| Option | Default | Description |
|---|---|---|
--title "My Title" |
(Derived from input filename) | Title printed centered on each page. |
--font /path/to/mono.ttf |
Auto-detected / built-in default | Use a specific TTF monospace font (e.g., DejaVuSansMono.ttf, Courier). |
--size 12 |
12 | Font size (points). |
--break-style page |
page | (Currently page breaks for --- are always forced; this remains for forward compatibility.) |
--transition-right 1.0 |
1.0 | Right margin in inches for right‑aligned transitions. Smaller values pull transitions further left. |
--shot-list markdown |
(none) | Write a shot list beside the PDF. Pass markdown (table) or csv. Filename auto-derived next to output PDF. |
--shot-list-pdf shots.pdf |
(none) | Generate a shot list as a PDF (table layout). |
--shot-list-landscape |
off | Use landscape orientation for shot list PDF (more horizontal room, less wrapping). |
--entities |
off | Include entity inventory (characters, locations, objects) in shot list (text/CSV/PDF). |
--vector |
off | Use vector text PDF rendering (requires reportlab) for crisp non-blurry output. |
--fcpxml script.fcpxml |
(none) | Export a minimal Final Cut Pro XML with scene markers & shot keyword ranges. |
--wpm 160 |
160 | Words per minute reading rate to estimate timing for FCPXML. |
--fps 25 |
25 | Frame rate used for FCPXML timecode (24/25/30 etc). |
python screenmd2pdf.py example_screenplay.md script.pdfGenerate a structured list of all scene headings and shot headings (lines starting with !) in script order. Each row includes:
- Type:
SCENEorSHOT - Heading: The raw heading text (e.g.
INT. KITCHEN - DAY,CLOSE ON) - Summary: First non-empty action line following that heading (trimmed to ~120 chars)
python screenmd2pdf.py example_screenplay.md script.pdf --shot-list markdown
# or CSV
python screenmd2pdf.py example_screenplay.md script.pdf --shot-list shots.csv
# or PDF (table rendered with wrapping)
python screenmd2pdf.py example_screenplay.md script.pdf --shot-list-pdf shots.pdfProduces (example):
| Type | Heading | Summary |
| ----- | --------------------- | --------------------------------- |
| SCENE | INT. KITCHEN - DAY | The sun burns through the mist... |
| SHOT | CLOSE ON | A detail description. |Add --shot-list-landscape to widen columns (less wrapping) and --entities to append an inventory section:
python screenmd2pdf.py example_screenplay.md script.pdf --shot-list-pdf shots.pdf --shot-list-landscape --entitiesEntity inventory groups: Characters, Locations (slugline location portion), and Objects/Props (heuristic uppercase tokens from action/shot text minus stop words).
Filenames:
- Markdown:
<output>_shots.md - CSV:
<output>_shots.csv - PDF: custom path you supply (e.g.
shots.pdf)
If no descriptive action follows a heading, the summary cell is left blank.
Pass --entities with --shot-list to append an "Entity Inventory" section (Markdown) or extra rows (CSV) after the table.
Generate a lightweight FCPXML you can import into Final Cut Pro to bootstrap your edit with chapter markers for scenes and searchable keyword ranges for shots.
python screenmd2pdf.py example_screenplay.md script.pdf --fcpxml script.fcpxmlWhat you get:
- Each scene heading becomes a marker at its estimated start time (uses cumulative estimated durations of preceding blocks).
- Each shot heading (
! ...) becomes a keyword range labeledSHOT:HEADINGspanning its estimated duration. - Durations are heuristically estimated from word counts at a configurable words-per-minute rate (
--wpm, default 160). Minimum 1s per text block. - Timecode is expressed at the chosen frame rate (
--fps, default 25) for compatibility with your project settings.
Usage with custom pacing & frame rate:
python screenmd2pdf.py example_screenplay.md script.pdf --fcpxml script.fcpxml --wpm 180 --fps 24Importing:
- In Final Cut Pro choose File > Import > XML… and select the generated
.fcpxml. - A new Event containing a Project named after the script title appears; markers & keyword ranges are visible in the timeline / index.
Notes / Limitations:
- The export represents the entire script as a single gap clip with markers/keywords (no media). Replace sections with actual footage as you assemble.
- Timing is approximate; adjust by moving markers once you have real durations.
- Only scene and shot headings are exported; dialogue/action aren’t broken into ranges (could be extended in future).
### INT. KITCHEN - DAY # Scene heading
Plain action text forms action paragraphs.
@ALEX # Character cue (leading '@' is stripped)
(whispering) # Optional parenthetical
We can't keep pretending...
@JORDAN
We stopped being normal...
>> CUT TO: # Transition (auto uppercased + colon if missing)
! CLOSE ON # Shot heading
A detail description.
--- # Forced page break- Blank line separates blocks (scene, action, dialogue groups, etc.).
- Dialogue ends at the first blank line after a character cue (excluding parentheticals).
- Multiple consecutive parentheticals are allowed.
- Lines starting with
//are removed before parsing. - Lines starting with
>(single) are treated as notes and removed;>>is reserved for transitions.
- Paper: US Letter 8.5" × 11".
- Margins / indents (approx):
- Scene & Action: Left 1.5", Right 1".
- Character: Left 3.5".
- Dialogue: Left 2.5", Right 2.5".
- Parenthetical: Left 3.0".
- Transition: Right aligned (right margin adjustable with
--transition-right).
- Monospaced layout using chosen / detected font.
- Scene headings rendered in bold (if a bold TTF variant of the chosen font is found; otherwise a simulated bold via overdraw).
The script tries to locate a bold variant of the supplied (or auto-detected) font using common filename patterns (e.g. replacing Regular with Bold). If it cannot find one, it simulates bold by drawing the heading text twice with a very slight horizontal offset. This keeps dependencies minimal while giving headings visual weight.
--- always forces a new page regardless of --break-style. The option is retained for future flexibility (line / space rendering modes could be re-enabled later).
By default pages are rasterized images (simple, but fonts can look slightly soft depending on viewer scaling). Use --vector to enable a pure text PDF (via ReportLab) for razor‑sharp rendering and selectable/copyable text:
pip install reportlab
python screenmd2pdf.py example_screenplay.md script.pdf --vectorNotes:
- Bold scene headings use a detected bold TTF variant if available, else regular weight.
- Vector mode currently ignores
--break-style(page breaks always forced) just like raster mode. - Raster mode remains the default to avoid extra dependency.
### INT. KITCHEN - DAY
@ALEX
(quietly)
Morning.
@JORDAN
Morning.
>> CUT TO:
### EXT. GARDEN - LATER
The sun burns through the mist.
---
### INT. OFFICE - DAY
@ALEX
We made it.Run:
python screenmd2pdf.py sample.md sample.pdf| Issue | Cause | Fix |
|---|---|---|
PDF shows @ before names |
Cached old PDF / not re-run | Re-run command; ensure you saved the Markdown. |
| Transition missing | Line started with single > not >> |
Use >> CUT TO:. |
| Font looks different | System font not found | Supply explicit --font pointing to a monospace TTF. |
| Page break not happening | Used ---- (4 dashes) or spaces |
Use exactly --- on its own line. |
Ideas you can add:
- Scene numbering
- Title page (detect first heading / metadata block)
- Automatic continued markers (CONT'D)
- Fountain format import
- Revision colors / A-pages
MIT (add a LICENSE file if distributing).
python screenmd2pdf.py my_script.md my_script.pdf --font /Library/Fonts/Courier\ New.ttf --size 12 --transition-right 1.0Enjoy writing!