Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
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)
*
Expand Down
27 changes: 27 additions & 0 deletions test/integration/embed-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
52 changes: 52 additions & 0 deletions test/unit/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});