propresenter-parser
Version:
Parses ProPresenter 4, 5, and 6 files to extract the data, and can build ProPresenter 5 and 6 files
240 lines (239 loc) • 11.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.v6Parser = void 0;
const fast_xml_parser_1 = require("fast-xml-parser");
const js_base64_1 = require("js-base64");
const Utils = __importStar(require("../utils"));
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 fast_xml_parser_1.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 === null || arrangementsXml === void 0 ? void 0 : arrangementsXml.RVSongArrangement) {
arrangements = this.getArrangements(arrangementsXml.RVSongArrangement, slideGroups);
}
return { properties, slideGroups, arrangements };
}
getProperties(xmlDoc) {
var _a, _b, _c, _d, _e, _f;
return {
CCLIArtistCredits: (_a = xmlDoc['@CCLIArtistCredits']) !== null && _a !== void 0 ? _a : '',
CCLIAuthor: (_b = xmlDoc['@CCLIAuthor']) !== null && _b !== void 0 ? _b : '',
CCLICopyrightYear: (_c = xmlDoc['@CCLICopyrightYear']) !== null && _c !== void 0 ? _c : '',
CCLIDisplay: xmlDoc['@CCLIDisplay'],
CCLIPublisher: (_d = xmlDoc['@CCLIPublisher']) !== null && _d !== void 0 ? _d : '',
CCLISongNumber: (_e = xmlDoc['@CCLISongNumber']) !== null && _e !== void 0 ? _e : '',
CCLISongTitle: (_f = xmlDoc['@CCLISongTitle']) !== null && _f !== void 0 ? _f : '',
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 = js_base64_1.Base64.decode(str['#text']);
}
else if (str['@rvXMLIvarName'] === 'RTFData') {
rtfData = js_base64_1.Base64.decode(str['#text']);
}
else if (str['@rvXMLIvarName'] === 'WinFlowData') {
winFlowData = js_base64_1.Base64.decode(str['#text']);
}
else if (str['@rvXMLIvarName'] === 'WinFontData') {
winFontData = js_base64_1.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;
}
}
exports.v6Parser = v6Parser;