diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a0515..8115fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [0.0.6](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.5...v0.0.6) (2021-10-28) +### Changes +- __fixed__: messed up tags on last release... +- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work. + +## [0.0.5](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.4...v0.0.5) (2021-10-28) +### Changes +- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work. + ## [0.0.4](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.3...v0.0.4) (2021-10-18) ### Changes - __BREAKING__: Constructor of Generator now takes second parameter - an `Options` object diff --git a/src/XML/common.ts b/src/XML/common.ts index b7e6079..c7d262e 100644 --- a/src/XML/common.ts +++ b/src/XML/common.ts @@ -1,25 +1,30 @@ export class baseXMLBUilder { protected _data: string[] = []; + private tab: string = " "; + private indent: number = 0; protected putTag(name: string, attr: {[key: string]: string}, contents?: (() => void) | string) { - const attrStr = - Object.entries(attr) + const attrStr = (() => { + const a = Object.entries(attr) .map(([key, value]) => {return `${key}="${value}"`;}) .join(' '); + return a ? " " + a : ""; + })(); if (!contents) { - this._data.push(`<${name} ${attrStr}/>`); + this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr} />`); return; } - this._data.push(`<${name} ${attrStr}>`); + this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr}>`); if (typeof contents === "function") { + this.indent++; contents(); - this._data.push(``); + this.indent-- + this._data.push(`${this.tab.repeat(this.indent)}`); } else { - const tmp: string[] = [contents, ``]; - this._data[this._data.length - 1] += tmp.join(""); + this._data[this._data.length - 1] += `${contents}`; } } diff --git a/src/XML/genXML.ts b/src/XML/genXML.ts index 7aa135c..a642ad7 100644 --- a/src/XML/genXML.ts +++ b/src/XML/genXML.ts @@ -2,12 +2,17 @@ import path from "path"; import {Generator} from "../baseGenerator"; import {xmlBuilder} from "./helpersXML"; +// https://fcp.co/final-cut-pro/tutorials/1912-demystifying-final-cut-pro-xmls-by-philip-hodgetts-and-gregory-clarke export class XML extends Generator { generate(): string { const builder = new xmlBuilder(); - builder.buildContext(path.parse(this.clipName).name, this.frameRate, this.cuts[this.cuts.length - 1].end, () => { + const [width, height] = (() => { + const split = this.resolution?.split('x'); + return split ? split.map(parseInt) : [null, null]; + })(); + builder.buildContext(path.parse(this.clipName).name, width, height, this.frameRate, this.cuts[this.cuts.length - 1].end, () => { this.cuts.forEach((cut) => { - builder.putClipitem(this.clipName, cut.start, cut.end, 720, 1270); // FIXME: needs resolution!!!!!!! + builder.putClipitem(this.clipName, cut.start, cut.end); }); }); return builder.data; diff --git a/src/XML/helpersXML.ts b/src/XML/helpersXML.ts index 286b883..c5f3508 100644 --- a/src/XML/helpersXML.ts +++ b/src/XML/helpersXML.ts @@ -5,24 +5,35 @@ export class xmlBuilder extends baseXMLBUilder { private clipitemIDs: string[] = []; private timeBase: number = -1; private duration: string = ""; - public buildContext(name: string, frameRate: number, duration: number, l: () => void) { + private registeredFile: boolean = false; + private width: number | null = -1; + private height: number | null = -1; + public buildContext(name: string, width: number | null, height: number | null, frameRate: number, duration: number, l: () => void) { + this.width = width; + this.height = height; this.timeBase = frameRate; this.duration = (frameRate * duration).toFixed(0); - this._data.push(""); this.putTag("xmeml", {version: this.conf.version}, () => { this.putTag("project", {}, () => { this.putTag("name", {}, name); this.putTag("children", {}, () => { this.putTag("sequence", {id: this.conf.sequenceID}, () => { this.putTag("name", {}, name); - this.putTag("duration", {}, this.duration); + this.putTag("duration", {}, this.duration); // FIXME: Duration is only up to the end of the last cut this.putTag("rate", {}, () => { this.putTag("timebase", {}, this.timeBase.toString()); - this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); + this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); // FIXME: ??? }); this.putTag("media", {}, () => { this.putTag("video", {}, () => { this.putTag("track", {}, l); + this.putTag("format", {}, () => { + this.putTag("samplecharacteristics", {}, () => { + this.height && this.putTag("height", {}, this.height.toString()); + this.width && this.putTag("width", {}, this.width.toString()) + this.putTag("pixelaspectratio", {}, this.conf.pixelAspectRatio); + }) + }); }); this.putTag("audio", {}, () => { this.putTag("track", {}, () => { @@ -33,51 +44,44 @@ export class xmlBuilder extends baseXMLBUilder { }); }); + this.putTag("timecode", {}, () => { + this.putTag("rate", {}, () => { + this.putTag("timebase", {}, this.timeBase.toString()); + this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); + }); + this.putTag("string", {}, this.conf.zeroTimecode); // why??? + this.putTag("frame", {}, this.conf.zeroFrame); + this.putTag("displayformat", {}, this.conf.displayFormat); + }); }); }); }); }); } - public putClipitem(filePath: string, start: number, end: number, height: number, width: number) { + public putClipitem(filePath: string, start: number, end: number) { const rate = () => { this.putTag("timebase", {}, this.timeBase.toString()); this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); }; - - - this.putTag("clipitem", { - frameBlend: this.putBool(this.conf.frameBlend), - id: this.genID(), - }, () => { + this.putTag("clipitem", {frameBlend: this.putBool(this.conf.frameBlend), id: this.genID(), }, () => { this.putTag("media", {}, () => { this.putTag("video", {}, () => { this.putTag("samplecharacteristics", {}, () => { - this.putTag("height", {}, height.toString()); - this.putTag("width", {}, width.toString()); - }); - }); - }); - this.putTag("file", {id: this.conf.fileID}, () => { - this.putTag("pathurl", {}, filePath); - this.putTag("name", {}, path.basename(filePath)); - this.putTag("rate", {}, rate); - this.putTag("duration", {}, this.duration); - this.putTag("timecode", {}, () => { - this.putTag("rate", {}, rate); - this.putTag("string", {}, this.conf.zeroTimecode); // why??? - this.putTag("frame", {}, this.conf.zeroFrame); - this.putTag("displayformat", {}, this.conf.displayFormat); - this.putTag("media", {}, () => { - this.putTag("video", {}); - this.putTag("audio", {}); + this.height && this.putTag("height", {}, this.height.toString()); + this.width && this.putTag("width", {}, this.width.toString()) }); }); }); + this.putTag("file", {id: this.conf.fileID}, this.genFile(filePath, rate)); this.putTag("name", {}, filePath); this.putTag("rate", {}, rate); - this.putTag("start", {}, (start * this.timeBase).toFixed()); - this.putTag("end", {}, (end * this.timeBase).toFixed()); + this.putTag("rate", {}, rate); // XXX: no idea. Original did this and original works so ¯\_(ツ)_/¯ + const startF = start * this.timeBase; + const endF = end * this.timeBase; + this.putTag("duration", {}, (endF - startF).toFixed()); + this.putTag("start", {}, startF.toFixed()); + this.putTag("end", {}, endF.toFixed()); }); } @@ -91,6 +95,32 @@ export class xmlBuilder extends baseXMLBUilder { return id; } + private genFile(filePath: string, rate: () => void): (() => void) | undefined { + /* + XXX: this looks weird, but it is being passed to a function with an optional parameter, + so it's expecting another function or undefined. + */ + if (!this.registeredFile) { + this.registeredFile = true; + return () => { + this.putTag("pathurl", {}, filePath); + this.putTag("name", {}, path.basename(filePath)); + this.putTag("rate", {}, rate); + this.putTag("duration", {}, this.duration); + this.putTag("timecode", {}, () => { + this.putTag("rate", {}, rate); + this.putTag("string", {}, this.conf.zeroTimecode); // why??? + this.putTag("frame", {}, this.conf.zeroFrame); + this.putTag("displayformat", {}, this.conf.displayFormat); + }); + this.putTag("media", {}, () => { + this.putTag("video", {}); + this.putTag("audio", {}); + }); + }; + } + } + private conf = { version: "4", @@ -101,6 +131,7 @@ export class xmlBuilder extends baseXMLBUilder { zeroTimecode: "00:00:00:00", zeroFrame: "0", displayFormat: "NDF", + pixelAspectRatio: 1.0.toFixed(1) } as const; } diff --git a/src/baseGenerator.ts b/src/baseGenerator.ts index e9bcc22..bcab3bf 100644 --- a/src/baseGenerator.ts +++ b/src/baseGenerator.ts @@ -7,6 +7,7 @@ export abstract class Generator { protected frameRate: number; protected clipName: string; protected options: Options; + protected resolution: string | null; constructor(project: Project, options: Options) { @@ -20,6 +21,7 @@ export abstract class Generator { } this.frameRate = this.project.frameRate; this.clipName = this.layer.sourceFile; + this.resolution = this.project.resolution; }