diff --git a/src/embed.ts b/src/embed.ts index aeac2738..d6ddc6f8 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -436,6 +436,58 @@ class Embed { return this.call('getMIDI').then(data => new Uint8Array(data as [number])); } + /** + * Convert the displayed score to PDF + * + * Exports the currently loaded score as a PDF file. The PDF is generated client-side + * and includes all pages of the score with proper page layout. + * + * @param options - Export options: + * - `parts`: Filter to specific parts (array of part UUIDs). If not specified, all parts are included. + * - `isConcertPitch`: Export in concert pitch instead of transposed (default: false) + * - `multiMeasuresRests`: Merge consecutive empty measures, useful for individual parts (default: false) + * - `outlineColoredNotes`: Outline colored notes for B&W printing (default: false) + * @returns A promise that resolves with a Uint8Array containing the PDF data + * @throws {TypeError} If options parameter is invalid + * @throws {Error} If no score is loaded or conversion fails + * + * @example + * // Get PDF as Uint8Array + * const pdfData = await embed.getPDF(); + * const blob = new Blob([pdfData], { type: 'application/pdf' }); + * + * @example + * // Export specific parts with options + * const parts = await embed.getParts(); + * const violinPdf = await embed.getPDF({ + * parts: [parts[0].uuid], + * multiMeasuresRests: true, + * isConcertPitch: true + * }); + */ + getPDF(options?: { + /** Filter to specific parts (array of part UUIDs) */ + parts?: string[]; + /** Export in concert pitch instead of transposed (default: false) */ + isConcertPitch?: boolean; + /** Merge consecutive empty measures (default: false) */ + multiMeasuresRests?: boolean; + /** Outline colored notes for B&W printing (default: false) */ + outlineColoredNotes?: boolean; + }): Promise { + return new Promise((resolve, reject) => { + options = options || {}; + if (typeof options !== 'object') { + return reject(new TypeError('Options must be an object')); + } + this.call('getPDF', options) + .then(data => { + resolve(new Uint8Array(data as [number])); + }) + .catch(reject); + }); + } + /** * Get the metadata of the score (for scores hosted on Flat) * diff --git a/test/integration/embed-integration.js b/test/integration/embed-integration.js index e241c7cf..d1aaaaf2 100644 --- a/test/integration/embed-integration.js +++ b/test/integration/embed-integration.js @@ -480,6 +480,33 @@ describe('Integration - Embed', () => { }); }); + describe('PDF export #full', () => { + it('should export in PDF (no options)', done => { + const { embed } = createEmbedForScoreId(PUBLIC_SCORE); + + embed.getPDF().then(pdf => { + assert.ok(pdf instanceof Uint8Array); + assert.ok(pdf.length > 0); + // PDF magic bytes: %PDF + assert.equal(pdf[0], 0x25); // % + assert.equal(pdf[1], 0x50); // P + assert.equal(pdf[2], 0x44); // D + assert.equal(pdf[3], 0x46); // F + done(); + }); + }); + + it('should export in PDF with concert pitch option', done => { + const { embed } = createEmbedForScoreId(PUBLIC_SCORE); + + embed.getPDF({ isConcertPitch: true }).then(pdf => { + assert.ok(pdf instanceof Uint8Array); + assert.ok(pdf.length > 0); + done(); + }); + }); + }); + describe('MIDI import/export', () => { it('shoud load a MIDI file in a blank embed', done => { var container = document.createElement('div'); diff --git a/test/unit/embed.js b/test/unit/embed.js index f27c0cd8..1629b873 100644 --- a/test/unit/embed.js +++ b/test/unit/embed.js @@ -173,4 +173,56 @@ describe('Unit - Embed tests', () => { container.removeChild(embed.element); }); }); + + describe('getPDF method', () => { + it('should have getPDF method available', () => { + const container = document.getElementById('container'); + const embed = new Flat.Embed(container, { score: '1234' }); + assert.equal(typeof embed.getPDF, 'function'); + container.removeChild(embed.element); + }); + + it('should reject when options is not an object', done => { + const container = document.getElementById('container'); + const embed = new Flat.Embed(container, { score: '1234' }); + embed.getPDF('invalid').catch(err => { + assert.ok(err instanceof TypeError); + assert.equal(err.message, 'Options must be an object'); + container.removeChild(embed.element); + done(); + }); + }); + + it('should accept valid options object', () => { + const container = document.getElementById('container'); + const embed = new Flat.Embed(container, { score: '1234' }); + // This should not throw - the actual call will fail since no score is loaded, + // but the options validation should pass + const promise = embed.getPDF({ + result: 'Uint8Array', + parts: ['uuid-1', 'uuid-2'], + isConcertPitch: true, + multiMeasuresRests: true, + outlineColoredNotes: true, + }); + assert.ok(promise instanceof Promise); + container.removeChild(embed.element); + }); + + it('should accept empty options object', () => { + const container = document.getElementById('container'); + const embed = new Flat.Embed(container, { score: '1234' }); + const promise = embed.getPDF({}); + assert.ok(promise instanceof Promise); + container.removeChild(embed.element); + }); + + it('should accept undefined options', () => { + const container = document.getElementById('container'); + const embed = new Flat.Embed(container, { score: '1234' }); + const promise = embed.getPDF(); + assert.ok(promise instanceof Promise); + container.removeChild(embed.element); + }); + }); });