UNPKG

propresenter-parser

Version:

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

202 lines (201 loc) 9.28 kB
import { XMLParser } from 'fast-xml-parser'; import { Base64 } from 'js-base64'; import * as Utils from '../utils'; export class v6Parser { parse(fileContent) { const alwaysArray = [ 'RVPresentationDocument.array', 'RVPresentationDocument.array.RVSlideGrouping', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide.array.RVTextElement', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide.array.RVImageElement', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide.array.RVBezierPathElement', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide.array.RVShapeElement', 'RVPresentationDocument.array.RVSlideGrouping.array.RVDisplaySlide.array.RVHTMLShapeElement', 'RVPresentationDocument.array.RVSongArrangement', 'RVPresentationDocument.array.RVSongArrangement.array.NSString', ]; const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@', parseAttributeValue: true, isArray: (_name, jPath) => alwaysArray.includes(jPath), }); const parsedDoc = xmlParser.parse(fileContent); if (parsedDoc.RVPresentationDocument['@versionNumber'] !== 600) { throw new Error(`Expected a ProPresenter 6 file with versionNumber="600" but got versionNumber="${parsedDoc.RVPresentationDocument['@versionNumber']}"`); } const properties = this.getProperties(parsedDoc.RVPresentationDocument); let slideGroups = []; const groupsXml = parsedDoc.RVPresentationDocument.array.find((el) => el['@rvXMLIvarName'] === 'groups'); if (groupsXml) { slideGroups = this.getSlideGroups(groupsXml.RVSlideGrouping); } let arrangements = []; const arrangementsXml = parsedDoc.RVPresentationDocument.array.find((el) => el['@rvXMLIvarName'] === 'arrangements'); if (arrangementsXml?.RVSongArrangement) { arrangements = this.getArrangements(arrangementsXml.RVSongArrangement, slideGroups); } return { properties, slideGroups, arrangements }; } getProperties(xmlDoc) { return { CCLIArtistCredits: xmlDoc['@CCLIArtistCredits'] ?? '', CCLIAuthor: xmlDoc['@CCLIAuthor'] ?? '', CCLICopyrightYear: xmlDoc['@CCLICopyrightYear'] ?? '', CCLIDisplay: xmlDoc['@CCLIDisplay'], CCLIPublisher: xmlDoc['@CCLIPublisher'] ?? '', CCLISongNumber: xmlDoc['@CCLISongNumber'] ?? '', CCLISongTitle: xmlDoc['@CCLISongTitle'] ?? '', backgroundColor: Utils.normalizeColorToRgbObj(xmlDoc['@backgroundColor']), buildNumber: xmlDoc['@buildNumber'], category: xmlDoc['@category'], chordChartPath: xmlDoc['@chordChartPath'], docType: xmlDoc['@docType'], drawingBackgroundColor: xmlDoc['@drawingBackgroundColor'], height: xmlDoc['@height'], lastDateUsed: new Date(xmlDoc['@lastDateUsed']), notes: xmlDoc['@notes'], os: xmlDoc['@os'], resourcesDirectory: xmlDoc['@resourcesDirectory'], selectedArrangementID: xmlDoc['@selectedArrangementID'], usedCount: xmlDoc['@usedCount'], versionNumber: xmlDoc['@versionNumber'], width: xmlDoc['@width'], }; } getSlideGroups(groupsXmlArr) { const groupsArr = []; for (const group of groupsXmlArr) { groupsArr.push({ groupColor: Utils.normalizeColorToRgbObj(group['@color']), groupId: group['@uuid'], groupLabel: group['@name'], slides: this.getSlidesForGroup(group.array.RVDisplaySlide), }); } return groupsArr; } getSlidesForGroup(slidesXmlArr) { const slidesArr = []; for (const slide of slidesXmlArr) { let textElements = []; const xmlDisplayElements = slide.array.find((s) => s['@rvXMLIvarName'] === 'displayElements'); if (xmlDisplayElements.RVTextElement) { textElements = this.getTextElementsForSlide(xmlDisplayElements.RVTextElement); } const highlightColor = slide['@highlightColor'] === '' ? null : Utils.normalizeColorToRgbObj(slide['@highlightColor']); slidesArr.push({ backgroundColor: Utils.normalizeColorToRgbObj(slide['@backgroundColor']), chordChartPath: slide['@chordChartPath'], drawingBackgroundColor: slide['@drawingBackgroundColor'], enabled: slide['@enabled'], highlightColor, hotKey: slide['@hotKey'], id: slide['@UUID'], label: slide['@label'], notes: slide['@notes'], textElements, }); } return slidesArr; } getTextElementsForSlide(textElementXmlArr) { const textElementArr = []; for (const txt of textElementXmlArr) { let plainText = ''; let rtfData = ''; let winFlowData = ''; let winFontData = ''; txt.NSString.forEach((str) => { if (str['@rvXMLIvarName'] === 'PlainText') { plainText = Base64.decode(str['#text']); } else if (str['@rvXMLIvarName'] === 'RTFData') { rtfData = Base64.decode(str['#text']); } else if (str['@rvXMLIvarName'] === 'WinFlowData') { winFlowData = Base64.decode(str['#text']); } else if (str['@rvXMLIvarName'] === 'WinFontData') { winFontData = Base64.decode(str['#text']); } }); const textProps = Utils.getTextPropsFromRtf(rtfData); textElementArr.push({ adjustsHeightToFit: txt['@adjustsHeightToFit'], bezelRadius: txt['@bezelRadius'], displayDelay: txt['@displayDelay'], displayName: txt['@displayName'], drawingFill: txt['@drawingFill'], fillColor: Utils.normalizeColorToRgbObj(txt['@fillColor']), fromTemplate: txt['@fromTemplate'], id: txt['@UUID'], locked: txt['@locked'], opacity: txt['@opacity'], persistent: txt['@persistent'], revealType: txt['@revealType'], rotation: txt['@rotation'], source: txt['@source'], typeID: txt['@typeID'], verticalAlignment: txt['@verticalAlignment'], fontName: textProps.font, textColor: textProps.color, textSize: textProps.size, plainText, rtfData, winFlowData, winFontData, outline: { color: Utils.normalizeColorToRgbObj(txt.dictionary.NSColor['#text']), size: txt.dictionary.NSNumber['#text'], enabled: txt['@drawingStroke'], }, position: this.getPosition(txt.RVRect3D['#text']), textShadow: this.getShadow(txt.shadow['#text'], txt['@drawingShadow']), }); } return textElementArr; } getPosition(positionStr) { const positionParts = positionStr .replace(/[{}]/g, '') .split(' ') .map((n) => parseInt(n, 10)); return { x: positionParts[0], y: positionParts[1], z: positionParts[2], width: positionParts[3], height: positionParts[4], }; } getShadow(shadowStr, enabled) { const pattern = new RegExp('^(\\d+)\\|(' + Utils.patternRgbaStrAsString + ')\\|\\{(-?\\d(?:\\.\\d+)?), (-?\\d(?:\\.\\d+)?)\\}$'); const match = pattern.exec(shadowStr); const radius = parseInt(match[1], 10); const color = Utils.normalizeColorToRgbObj(match[2]); const offsetX = parseFloat(match[3]); const offsetY = parseFloat(match[4]); const angle = (Math.atan2(offsetX, offsetY) * 180) / Math.PI; const length = Math.round(Math.hypot(offsetX, offsetY)); return { angle, color, enabled, length, radius }; } getArrangements(arrangementsXml, slideGroups) { const arrangementsArr = []; for (const arrangement of arrangementsXml) { const groupOrder = arrangement.array.NSString.map((groupIdStr) => { return { groupId: groupIdStr, groupLabel: slideGroups.find((g) => g.groupId === groupIdStr).groupLabel, }; }); arrangementsArr.push({ label: arrangement['@name'], color: Utils.normalizeColorToRgbObj(arrangement['@color']), groupOrder, }); } return arrangementsArr; } }