UNPKG

propresenter-parser

Version:

Parses ProPresenter 4, 5, and 6 files to extract the data, and can build ProPresenter 5 and 6 files

247 lines (246 loc) 10.8 kB
import { XMLBuilder } from 'fast-xml-parser'; import { Base64 } from 'js-base64'; import { IProTransitionType } from '../shared.model'; import * as Utils from '../utils'; export class v6Builder { xmlBuilder; options; winFontData = `<?xml version="1.0" encoding="utf-16"?><RVFont xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ProPresenter.Common"><Kerning>0</Kerning><LineSpacing>0</LineSpacing><OutlineColor xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Windows.Media"><d2p1:A>255</d2p1:A><d2p1:B>0</d2p1:B><d2p1:G>0</d2p1:G><d2p1:R>0</d2p1:R><d2p1:ScA>1</d2p1:ScA><d2p1:ScB>0</d2p1:ScB><d2p1:ScG>0</d2p1:ScG><d2p1:ScR>0</d2p1:ScR></OutlineColor><OutlineWidth>0</OutlineWidth><Variants>Normal</Variants></RVFont>`; defaultTransitionObj = { '@rvXMLIvarName': 'transitionObject', '@transitionType': IProTransitionType.None, '@transitionDirection': 0, '@transitionDuration': 1, '@motionEnabled': false, '@motionDuration': 0, '@motionSpeed': 0, '@groupIndex': 0, '@orderIndex': 0, '@slideBuildAction': 0, '@slideBuildDelay': 0, }; constructor(options) { this.xmlBuilder = new XMLBuilder({ attributeNamePrefix: '@', format: true, ignoreAttributes: false, processEntities: false, suppressUnpairedNode: false, suppressBooleanAttributes: false, unpairedTags: ['array', 'RVTransition'], }); this.options = options; const defaultProperties = { CCLIArtistCredits: '', CCLIAuthor: '', CCLIDisplay: false, CCLIPublisher: '', category: 'Song', notes: '', height: 720, width: 1280, }; this.options.properties = { ...defaultProperties, ...this.options.properties }; const defaultSlideTextFormatting = { textColor: { r: 255, g: 255, b: 255 }, textPadding: 20, fontName: 'Arial', textSize: 60, textShadow: { angle: 135, color: { r: 0, g: 0, b: 0 }, enabled: false, length: 7, radius: 10, }, }; this.options.slideTextFormatting = { ...defaultSlideTextFormatting, ...this.options.slideTextFormatting, }; } build() { const documentObj = { '?xml': { '@version': '1.0', '@encoding': 'utf-8', }, RVPresentationDocument: { '@CCLIArtistCredits': this.options.properties.CCLIArtistCredits, '@CCLIAuthor': this.options.properties.CCLIAuthor, '@CCLICopyrightYear': this.options.properties.CCLICopyrightYear ?? '', '@CCLIDisplay': this.options.properties.CCLIDisplay, '@CCLIPublisher': this.options.properties.CCLIPublisher, '@CCLISongNumber': this.options.properties.CCLISongNumber ?? '', '@CCLISongTitle': this.options.properties.CCLISongTitle, '@category': this.options.properties.category, '@notes': this.options.properties.notes, '@lastDateUsed': Utils.getIsoDateString(), '@height': this.options.properties.height, '@width': this.options.properties.width, '@backgroundColor': '0 0 0 1', '@buildNumber': 6016, '@chordChartPath': '', '@docType': 0, '@drawingBackgroundColor': false, '@resourcesDirectory': '', '@selectedArrangementID': '', '@os': 1, '@usedCount': 0, '@versionNumber': 600, RVTransition: this.getTransitions(), RVTimeline: { '@rvXMLIvarName': 'timeline', '@timeOffset': 0, '@duration': 0, '@selectedMediaTrackIndex': 0, '@loop': false, array: [{ '@rvXMLIvarName': 'timeCues' }, { '@rvXMLIvarName': 'mediaTracks' }], }, array: [ { '@rvXMLIvarName': 'groups', RVSlideGrouping: this.buildSlideGroups(), }, { '@rvXMLIvarName': 'arrangements', RVSongArrangement: [], }, ], }, }; return this.xmlBuilder.build(documentObj).trim(); } getTransitions() { if (this.options.transitions) { const transitionsCopy = { ...this.defaultTransitionObj }; transitionsCopy['@transitionDuration'] = this.options.transitions.duration; transitionsCopy['@transitionType'] = this.options.transitions.type; return transitionsCopy; } return this.defaultTransitionObj; } buildSlideGroups() { const xmlSlideGroups = []; for (const group of this.options.slideGroups) { xmlSlideGroups.push({ '@name': group.label, '@uuid': Utils.getUniqueID(), '@color': Utils.normalizeColorToRgbaString(group.groupColor ?? '0 0 0 0'), array: { '@rvXMLIvarName': 'slides', RVDisplaySlide: this.buildSlidesForGroup(group), }, }); } return xmlSlideGroups; } buildSlidesForGroup(thisGroup) { const xmlSlides = []; for (const slide of thisGroup.slides) { let highlightColor = '0 0 0 0'; let label = ''; let text; if (typeof slide === 'string') { text = slide; } else { highlightColor = Utils.normalizeColorToRgbaString(slide.slideColor ?? highlightColor); label = slide.label ?? ''; text = slide.text; } xmlSlides.push({ '@backgroundColor': '0 0 0 0', '@highlightColor': highlightColor, '@drawingBackgroundColor': false, '@enabled': true, '@hotKey': '', '@label': label, '@notes': '', '@UUID': Utils.getUniqueID(), '@chordChartPath': '', array: [ { '@rvXMLIvarName': 'cues' }, { '@rvXMLIvarName': 'displayElements', RVTextElement: [this.buildTextElement(text)], }, ], }); } return xmlSlides; } buildTextElement(text) { const rtfText = Utils.formatRtf(text, this.options.slideTextFormatting.fontName, this.options.slideTextFormatting.textSize, Utils.normalizeColorToRgbObj(this.options.slideTextFormatting.textColor)); return { '@displayName': 'Default', '@UUID': Utils.getUniqueID(), '@typeID': 0, '@displayDelay': 0, '@locked': false, '@persistent': 0, '@fromTemplate': false, '@opacity': 1, '@source': '', '@bezelRadius': 0, '@rotation': 0, '@drawingFill': false, '@drawingShadow': this.options.slideTextFormatting.textShadow.enabled, '@drawingStroke': false, '@fillColor': '1 1 1 0', '@adjustsHeightToFit': false, '@verticalAlignment': 0, '@revealType': 0, RVRect3D: { '@rvXMLIvarName': 'position', '#text': this.getTextElementPosition(), }, shadow: { '@rvXMLIvarName': 'shadow', '#text': this.getElementShadow() }, dictionary: { '@rvXMLIvarName': 'stroke', NSColor: { '@rvXMLDictionaryKey': 'RVShapeElementStrokeColorKey', '#text': '0 0 0 1', }, NSNumber: { '@rvXMLDictionaryKey': 'RVShapeElementStrokeWidthKey', '@hint': 'double', '#text': 0, }, }, NSString: [ { '@rvXMLIvarName': 'PlainText', '#text': Base64.encode(text) }, { '@rvXMLIvarName': 'RTFData', '#text': Base64.encode(rtfText) }, { '@rvXMLIvarName': 'WinFlowData', '#text': Base64.encode(this.getWinFlowDocument(text)) }, { '@rvXMLIvarName': 'WinFontData', '#text': Base64.encode(this.winFontData) }, ], }; } getTextElementPosition() { const posX = this.options.slideTextFormatting.textPadding; const posY = this.options.slideTextFormatting.textPadding; const width = this.options.properties.width - this.options.slideTextFormatting.textPadding * 2; const height = this.options.properties.height - this.options.slideTextFormatting.textPadding * 2; return `{${posX} ${posY} 0 ${width} ${height}}`; } getElementShadow() { const radius = this.options.slideTextFormatting.textShadow.radius; const color = Utils.normalizeColorToRgbaString(this.options.slideTextFormatting.textShadow.color); const angle = this.options.slideTextFormatting.textShadow.angle; const length = this.options.slideTextFormatting.textShadow.length; const posX = Math.sin(angle * (Math.PI / 180)) * length; const posY = Math.cos(angle * (Math.PI / 180)) * length; return `${radius}|${color}|{${posX}, ${posY}}`; } getWinFlowDocument(text) { const fontName = this.options.slideTextFormatting.fontName; const size = this.options.slideTextFormatting.textSize; const hexColor = Utils.normalizeColorToHex(this.options.slideTextFormatting.textColor); const paragraphs = text .split(/[\n\r]/g) .filter((line) => line !== '') .map((line) => `<Paragraph Margin="0,0,0,0" TextAlignment="Center" FontFamily="${fontName}" FontSize="${size}"><Run FontFamily="${fontName}" FontSize="${size}" Foreground="#${hexColor}FF" Block.TextAlignment="Center">${line}</Run></Paragraph>`) .join(''); return `<FlowDocument TextAlignment="Center" PagePadding="5,0,5,0" AllowDrop="True" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">${paragraphs}</FlowDocument>`; } }