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
JavaScript
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;
}
}