Created
December 21, 2019 23:10
-
-
Save jeffmikels/1ac21b330608c19167739b5e2c88362d to your computer and use it in GitHub Desktop.
Generate ProPresenter6 Document
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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', ' ') | |
# 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