Skip to content

Instantly share code, notes, and snippets.

@jeffmikels
Created December 21, 2019 23:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeffmikels/1ac21b330608c19167739b5e2c88362d to your computer and use it in GitHub Desktop.
Save jeffmikels/1ac21b330608c19167739b5e2c88362d to your computer and use it in GitHub Desktop.
Generate ProPresenter6 Document
#!/usr/bin/env python3
import os
import re
import base64
import json
import uuid
import html
# This script generates a ProPresenter file from a slide object
# example ProPresenter6 RTF
# {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf500
# {\fonttbl\f0\fnil\fcharset0 Muli-ExtraBold
# \f1\fnil\fcharset0 Muli-Black
# }
# {\colortbl
# \red255\green255\blue255
# \red252\green253\blue198
# }
# {\*\expandedcolortbl
# \cssrgb\c99170\c98851\c81579
# }
# \deftab720
# \pard\pardeftab720\qc\partightenfactor0
# \f0\b\fs240 \cf1 FAITH HAS TO BE
# \f1 \cf2 \ul \ulc2 IN
# \f0 \cf1 \ulnone SOMEONE OR SOMETHING
# }
#
# template rtf
# {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf500
# {\fonttbl\f0\fnil\fcharset0 GothamUltra;\f1\fnil\fcharset0 GothamBold;\f2\fnil\fcharset0 GothamBlack;
# \f3\fnil\fcharset0 Muli-ExtraBold;\f4\fnil\fcharset0 Muli-BlackItalic;}
# {\colortbl;\red255\green255\blue255;\red22\green19\blue18;\red54\green147\blue162;\red13\green50\blue119;
# }
# {\*\expandedcolortbl;;\cssrgb\c11584\c9649\c8648;\cssrgb\c25517\c63849\c69693;\cssrgb\c4431\c26819\c54059;
# }
# \margl1440\margr1440\vieww25480\viewh17060\viewkind0
# \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
#
# \f0\b\fs370 \cf2 HEADER \
#
# \f1\fs220 \cf0 TEXT\
#
# \f2\fs280 \cf3 BLANK \
#
# \f3\fs200 \cf0 BIBLE\
#
# \f4\i\fs120 \cf4 reference}
class Style:
def __init__(self, font, size, color):
self.font = font
self.size = size
self.color = color
headerStyle = Style('GothamUltra', 300,(327,233,231))
textStyle = Style('GothamBold', 220,(255,255,255))
blankStyle = Style('GothamBlack', 280,(117,195,207))
quotationStyle = Style('Muli-ExtraBold', 166,(255,255,255))
refStyle = Style('Muli-BlackItalic', 120,(152,198,254))
TARGET_SLIDE_CHARS = 350
SLIDE_GRACE_CHARS = 50
IGNORE_ANNOUNCEMENTS = True
VMIX_LOWER3_INPUT = 16
class ProPresenterSlide:
notes = ''
title = ''
text = ''
ref = '' # used for quote citations
background = ''
continues = False
allowSpanning = False
def __init__(self):
pass
def map(self):
return {
'notes': self.notes,
'title': self.title,
'text': self.text,
'ref':self.ref,
'continues':self.continues,
'allow_spanning': self.allowSpanning,
'background':self.background
}
def render(self):
rtf = self.toRTF()
template = r'''
<RVDisplaySlide UUID="[UUID1]" backgroundColor="0 0 0 1" chordChartPath="" drawingBackgroundColor="false" enabled="true" highlightColor="0 0 0 0" hotKey="" label="" notes="[NOTES]" socialItemCount="1">
[CUES]
<array rvXMLIvarName="displayElements">
<RVTextElement UUID="[UUID4]" additionalLineFillHeight="0.000000" adjustsHeightToFit="false" bezelRadius="0.000000" displayDelay="0.000000" displayName="Default" drawLineBackground="false" drawingFill="false" drawingShadow="true" drawingStroke="false" fillColor="1 1 1 1" fromTemplate="false" lineBackgroundType="0" lineFillVerticalOffset="0.000000" locked="false" opacity="1.000000" persistent="false" revealType="0" rotation="0.000000" source="" textSourceRemoveLineReturnsOption="false" typeID="0" useAllCaps="false" verticalAlignment="0">
<RVRect3D rvXMLIvarName="position">{34 35 0 1853 1011}</RVRect3D>
<shadow rvXMLIvarName="shadow">5.000000|0 0 0 1|{4, -4}</shadow>
<dictionary rvXMLIvarName="stroke">
<NSColor rvXMLDictionaryKey="RVShapeElementStrokeColorKey">0 0 0 1</NSColor>
<NSNumber hint="integer" rvXMLDictionaryKey="RVShapeElementStrokeWidthKey">0</NSNumber>
</dictionary>
<NSString rvXMLIvarName="RTFData">[RTF]</NSString>
</RVTextElement>
</array>
</RVDisplaySlide>
'''
clear_background = r'''
<array rvXMLIvarName="cues">
<RVClearCue UUID="[UUID5]" actionType="2" delayTime="0.000000" displayName="Clear Bkg" enabled="false" timeStamp="0.000000"/>
</array>
'''
use_background = r'''
<array rvXMLIvarName="cues"/>
<RVMediaCue UUID="[UUID2]" actionType="0" alignment="4" behavior="1" dateAdded="" delayTime="0.000000" displayName="" enabled="false" nextCueUUID="" rvXMLIvarName="backgroundMediaCue" tags="" timeStamp="0.000000">
<RVImageElement UUID="[UUID3]" bezelRadius="0.000000" displayDelay="0.000000" displayName="ImageElement" drawingFill="false" drawingShadow="false" drawingStroke="false" fillColor="" flippedHorizontally="false" flippedVertically="false" format="JPEG image" fromTemplate="false" imageOffset="{0, 0}" locked="false" manufactureName="" manufactureURL="" opacity="1.000000" persistent="false" rotation="0.000000" rvXMLIvarName="element" scaleBehavior="3" scaleSize="{1, 1}" source="file://[IMAGEPATH]" typeID="0">
<RVRect3D rvXMLIvarName="position">{0 0 0 0 0}</RVRect3D>
<shadow rvXMLIvarName="shadow">0.000000|0 0 0 0.3333333333333333|{4, -4}</shadow>
<dictionary rvXMLIvarName="stroke">
<NSColor rvXMLDictionaryKey="RVShapeElementStrokeColorKey">0 0 0 1</NSColor>
<NSNumber hint="float" rvXMLDictionaryKey="RVShapeElementStrokeWidthKey">1.000000</NSNumber>
</dictionary>
</RVImageElement>
</RVMediaCue>
'''
# are we using a background image?
if self.background != '':
template = template.replace('[CUES]', use_background)
template = template.replace('[IMAGEPATH]', html_attrib_safe(os.path.realpath(self.background)))
else:
template = template.replace('[CUES]', clear_background)
# generate uuid values
for key in [1,2,3,4,5]:
template = template.replace(f'[UUID{key}]', str(uuid.uuid1()).upper())
# handle notes
template = template.replace('[NOTES]', html_attrib_safe(self.notes))
return template.replace('[RTF]', b64(rtf))
def toRTF(self):
# is the expandedcolortbl really needed?
# colors are indexed by 1 automatically
# old expanded color table
# {\*\expandedcolortbl;;\cssrgb\c100000\c0\c0;\cssrgb\c99170\c98851\c81579;\cssrgb\c14862\c100000\c0;\csgenericrgb\c53492\c72421\c99521;}
rtf_head_template = r'''{\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf500
{\fonttbl\f0\fnil\fcharset0 [HEADER_FONT];\f1\fnil\fcharset0 [TEXT_FONT];\f2\fnil\fcharset0 [BLANK_FONT];
\f3\fnil\fcharset0 [QUOTATION_FONT];\f4\fnil\fcharset0 [REFERENCE_FONT];}
{\colortbl;[COLOR_TEXT];[COLOR_HEADER];[COLOR_BLANK];[COLOR_QUOTATION];
[COLOR_REF];}
{\*\expandedcolortbl;;[CSS_COLOR_HEADER];[CSS_COLOR_BLANK];[CSS_COLOR_QUOTATION];
[CSS_COLOR_REF];}
\deftab720
[PAR_TEXT]}'''
rtf_center_par = '\\pard\\pardeftab720\\qc\\partightenfactor0\n'
rtf_left_par = '\\pard\\pardeftab720\\partightenfactor0\n'
rtf_right_par = '\\pard\\pardeftab720\\qr\\partightenfactor0\n'
# spans work like this fontsettings colorsettings kerningsettings underlinesettings text
# i.e. font 0 bold italic size 370 (=185pt), color 2 underline underline color 3
# \f0\b\i\fs370 \cf2 \ul \ulc3 Whatever text you want
# \f1 \cf1 \ulnone BELIEVED.\
# \f3 \cf4 \kerning1\expnd-24\expndtw-120
# force a newline with a \ and a literal newline
# configure the heading
rtf = rtf_head_template
rtf = rtf.replace('[TEXT_FONT]', textStyle.font)
rtf = rtf.replace('[COLOR_TEXT]', rtfcolor(textStyle.color))
rtf = rtf.replace('[CSS_COLOR_TEXT]', cssrgb(textStyle.color))
rtf = rtf.replace('[BLANK_FONT]', blankStyle.font)
rtf = rtf.replace('[COLOR_BLANK]', rtfcolor(blankStyle.color))
rtf = rtf.replace('[CSS_COLOR_BLANK]', cssrgb(blankStyle.color))
rtf = rtf.replace('[HEADER_FONT]', headerStyle.font)
rtf = rtf.replace('[COLOR_HEADER]', rtfcolor(headerStyle.color))
rtf = rtf.replace('[CSS_COLOR_HEADER]', cssrgb(headerStyle.color))
rtf = rtf.replace('[QUOTATION_FONT]', quotationStyle.font)
rtf = rtf.replace('[COLOR_QUOTATION]', rtfcolor(quotationStyle.color))
rtf = rtf.replace('[CSS_COLOR_QUOTATION]', cssrgb(quotationStyle.color))
rtf = rtf.replace('[REFERENCE_FONT]', refStyle.font)
rtf = rtf.replace('[COLOR_REF]', rtfcolor(refStyle.color))
rtf = rtf.replace('[CSS_COLOR_REF]', cssrgb(refStyle.color))
# configure the paragraphs
parts = [rtf_center_par]
# add a header if needed
if self.title != '':
if self.text != '':
parts.append(f'\\f0\\fs{headerStyle.size} \\cf2 {utf_rtf(self.title)}\\\n')
else:
parts.append(f'\\f0\\fs{headerStyle.size} \\cf2 {utf_rtf(self.title)}')
# handle slides that span over multiple slides
if self.allowSpanning:
parts.append(rtf_left_par)
parts.append(f'\\f3\\fs{quotationStyle.size} \\cf4 \\kerning1\\expnd-24\\expndtw-120 {utf_rtf(self.text)}')
if not slide.continues:
parts.append('\\\n')
parts.append(rtf_right_par)
parts.append(f'\\f4\\fs{refStyle.size} \\cf5 {utf_rtf(self.ref)}')
else:
if slide.text != '':
tdata = re.split(r'_(.*?)_', slide.text)
# contents of a blank will always be at odd indicies
for i in range(len(tdata)):
isblank = i % 2 == 1
if tdata[i] == '':
continue
if isblank:
parts.append(f'\\f2\\fs{blankStyle.size} \\cf3 \\ul \\ulc3 {utf_rtf(tdata[i].upper())}')
else:
parts.append(f'\\f2\\fs{textStyle.size} \\cf1 \\ulnone {utf_rtf(tdata[i])}')
return rtf.replace('[PAR_TEXT]', '\n'.join(parts))
class ProPresenterDoc:
def __init__(self, slides, id=0):
self.id = id
self.slides = slides
def json(self):
retval = {
'slides': []
}
for slide in self.slides:
retval['slides'].append(slide.map())
return json.dumps(retval)
def render(self):
template = r'''<?xml version="1.0"?>
<RVPresentationDocument CCLIArtistCredits="" CCLIAuthor="" CCLICopyrightYear="" CCLIDisplay="false" CCLIPublisher="" CCLISongNumber="" CCLISongTitle="" backgroundColor="" buildNumber="16245" category="Presentation" chordChartPath="" docType="0" drawingBackgroundColor="false" height="1080" lastDateUsed="2019-08-23T13:25:13-04:00" notes="" os="2" resourcesDirectory="" selectedArrangementID="" usedCount="0" uuid="[UUID]" versionNumber="600" width="1920">
<RVTimeline duration="0.000000" loop="false" playBackRate="0.000000" rvXMLIvarName="timeline" selectedMediaTrackIndex="0" timeOffset="0.000000">
<array rvXMLIvarName="timeCues"/>
<array rvXMLIvarName="mediaTracks"/>
</RVTimeline>
<array rvXMLIvarName="groups">
<RVSlideGrouping color="0.2637968361377716 0.2637968361377716 0.2637968361377716 1" name="Group" uuid="[UUID2]">
<array rvXMLIvarName="slides">
[SLIDES]
</array>
</RVSlideGrouping>
</array>
<array rvXMLIvarName="arrangements"/>
</RVPresentationDocument>'''
template = template.replace('[UUID]', str(uuid.uuid1()).upper())
template = template.replace('[UUID2]', str(uuid.uuid1()).upper())
slidexml = []
for slide in self.slides:
slidexml.append(slide.render())
return template.replace('[SLIDES]', '\n'.join(slidexml))
# helper functions
def b64(s):
return base64.b64encode(s.encode('UTF-8')).decode('UTF-8')
def smart_quotes(s):
"""Takes a string and returns it with dumb quotes, single and double,
replaced by smart quotes. Accounts for the possibility of HTML tags
within the string."""
# Find dumb double quotes coming directly after letters or punctuation,
# and replace them with right double quotes.
s = re.sub(r'([a-zA-Z0-9.,?!;:\'\"])"', r'\1”', s)
# Find any remaining dumb double quotes and replace them with
# left double quotes.
s = s.replace('"', '“')
# Reverse: Find any SMART quotes that have been (mistakenly) placed around HTML
# attributes (following =) and replace them with dumb quotes.
s = re.sub(r'=“(.*?)”', r'="\1"', s)
# Follow the same process with dumb/smart single quotes
s = re.sub(r"([a-zA-Z0-9.,?!;:\"\'])'", r'\1’', s)
s = s.replace("'", '‘')
s = re.sub(r'=‘(.*?)’', r"='\1'", s)
return s
def html_attrib_safe(s):
# first encode entities
tmp = html.escape(s)
# now fix carriage returns and line feeds
tmp = tmp.replace('\r\n', '\n')
tmp = tmp.replace('\r', '\n')
tmp = tmp.replace('\n', '&#10;')
# now remove all unsafe chars
chars = []
for char in tmp:
if ord(char) < 32:
continue
if ord(char) > 127:
char = f'&#{ord(char)};'
chars.append(char)
return ''.join(chars)
def utf_rtf(s, altchar = '?'):
accum = []
for char in s:
if ord(char) > 127:
char = '\\u' + str(ord(char)) + altchar
accum.append(char)
retval = ''.join(accum)
retval = retval.replace('\n', '\\\n')
return retval
def rtfcolor(tuple):
return '\\red{}\\green{}\\blue{}'.format(*tuple)
def cssrgb(tuple):
l = []
for i in tuple:
l.append((i * 100000) // 255)
return '\\cssrgb\\c{}\\c{}\\c{}'.format(*l)
##################################
## MAIN BEGINS HERE
##################################
#
# load_template()
# exit()
if __name__ == '__main__':
slides = []
main_image = 'main.jpg'
background_image = 'background.jpg'
# slide with main image and no text
slide = ProPresenterSlide()
slide.background = main_image
slides.append(slide)
# slide with background image and some text
slide = ProPresenterSlide()
slide.background = background_image
slide.title = 'hello'
slide.text = 'world'
slides.append(slide)
# slide with long text, blank background spanning multiple slides
slide = ProPresenterSlide()
slide.title = 'hello2'
slide.text = 'world2' + ' ... with some long text after it ' * 100
slide.ref = 'quotation reference'
slide.allowSpanning = True
slides.append(slide)
doc = ProPresenterDoc(slides)
outfile = f'SAMPLE.pro6'
towrite = doc.render()
with open(outfile, 'w') as f:
f.write(towrite)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment