mirror of
https://github.com/astefanutti/decktape.git
synced 2024-09-17 17:37:13 +03:00
chore: pass options argument around
This commit is contained in:
parent
7ba704f82b
commit
8d102c9115
506
decktape.js
506
decktape.js
@ -50,23 +50,23 @@ parser.script('decktape').options({
|
|||||||
default : 0,
|
default : 0,
|
||||||
help : 'Duration in milliseconds between the page has loaded and starting to export slides',
|
help : 'Duration in milliseconds between the page has loaded and starting to export slides',
|
||||||
},
|
},
|
||||||
urlLoadTimeout: {
|
urlLoadTimeout : {
|
||||||
full: 'url-load-timeout',
|
full : 'url-load-timeout',
|
||||||
metavar: '<ms>',
|
metavar : '<ms>',
|
||||||
default: 60000,
|
default : 60000,
|
||||||
help: 'Timeout in milliseconds to use when waiting for the initial URL to load',
|
help : 'Timeout in milliseconds to use when waiting for the initial URL to load',
|
||||||
},
|
},
|
||||||
pageLoadTimeout: {
|
pageLoadTimeout : {
|
||||||
full: 'page-load-timeout',
|
full : 'page-load-timeout',
|
||||||
metavar: '<ms>',
|
metavar : '<ms>',
|
||||||
default: 20000,
|
default : 20000,
|
||||||
help: 'Timeout in milliseconds to use when waiting for the slide deck page to load',
|
help : 'Timeout in milliseconds to use when waiting for the slide deck page to load',
|
||||||
},
|
},
|
||||||
bufferTimeout: {
|
bufferTimeout : {
|
||||||
full: 'buffer-timeout',
|
full : 'buffer-timeout',
|
||||||
metavar: '<ms>',
|
metavar : '<ms>',
|
||||||
default: 30000,
|
default : 30000,
|
||||||
help: 'Timeout in milliseconds to use when waiting for a slide to finish buffering (set to 0 to disable)',
|
help : 'Timeout in milliseconds to use when waiting for a slide to finish buffering (set to 0 to disable)',
|
||||||
},
|
},
|
||||||
screenshots : {
|
screenshots : {
|
||||||
default : false,
|
default : false,
|
||||||
@ -249,10 +249,10 @@ process.on('unhandledRejection', error => {
|
|||||||
|
|
||||||
page
|
page
|
||||||
.on('console', async msg => {
|
.on('console', async msg => {
|
||||||
const args = msg.args().length
|
const args = msg.args().length
|
||||||
? await Promise.all(msg.args().map(arg => arg.executionContext().evaluate(obj => obj, arg)))
|
? await Promise.all(msg.args().map(arg => arg.executionContext().evaluate(obj => obj, arg)))
|
||||||
: [msg.text()];
|
: [msg.text()];
|
||||||
console.log(...args.map(arg => color(msg.type())(util.format(arg))));
|
console.log(...args.map(arg => color(msg.type())(util.format(arg))));
|
||||||
})
|
})
|
||||||
.on('requestfailed', request => {
|
.on('requestfailed', request => {
|
||||||
// do not output warning for cancelled requests
|
// do not output warning for cancelled requests
|
||||||
@ -271,10 +271,10 @@ process.on('unhandledRejection', error => {
|
|||||||
// TODO: improve message when reading file locally
|
// TODO: improve message when reading file locally
|
||||||
.then(response => console.log('Loading page finished with status: %s', response.status()))
|
.then(response => console.log('Loading page finished with status: %s', response.status()))
|
||||||
.then(delay(options.loadPause))
|
.then(delay(options.loadPause))
|
||||||
.then(_ => createPlugin(page))
|
.then(_ => createPlugin(page, plugins, options))
|
||||||
.then(plugin => configurePlugin(plugin)
|
.then(plugin => configurePlugin(plugin)
|
||||||
.then(_ => configurePage(plugin, page))
|
.then(_ => configurePage(page, plugin, options))
|
||||||
.then(_ => exportSlides(plugin, page, pdf))
|
.then(_ => exportSlides(page, plugin, pdf, options))
|
||||||
.then(async context => {
|
.then(async context => {
|
||||||
await writePdf(options.filename, pdf);
|
await writePdf(options.filename, pdf);
|
||||||
console.log(chalk.green(`\nPrinted ${chalk.bold('%s')} slides`), context.exportedSlides);
|
console.log(chalk.green(`\nPrinted ${chalk.bold('%s')} slides`), context.exportedSlides);
|
||||||
@ -286,265 +286,265 @@ process.on('unhandledRejection', error => {
|
|||||||
browser.close();
|
browser.close();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
async function writePdf(filename, pdf) {
|
async function loadAvailablePlugins(pluginsPath) {
|
||||||
const pdfDir = path.dirname(filename);
|
const plugins = await fs.promises.readdir(pluginsPath);
|
||||||
try {
|
const entries = await Promise.all(plugins.map(async pluginPath => {
|
||||||
fs.accessSync(pdfDir, fs.constants.F_OK);
|
const [, plugin] = pluginPath.match(/^(.*)\.js$/);
|
||||||
} catch {
|
if (plugin && (await fs.promises.stat(path.join(pluginsPath, pluginPath))).isFile()) {
|
||||||
fs.mkdirSync(pdfDir, { recursive: true });
|
return [plugin, await import(`./plugins/${pluginPath}`)];
|
||||||
}
|
}
|
||||||
fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false }));
|
}));
|
||||||
}
|
return Object.fromEntries(entries.filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAvailablePlugins(pluginsPath) {
|
async function createPlugin(page, plugins, options) {
|
||||||
const plugins = await fs.promises.readdir(pluginsPath);
|
let plugin;
|
||||||
const entries = await Promise.all(plugins.map(async pluginPath => {
|
if (!options.command || options.command === 'automatic') {
|
||||||
const [, plugin] = pluginPath.match(/^(.*)\.js$/);
|
plugin = await createActivePlugin(page, plugins, options);
|
||||||
if (plugin && (await fs.promises.stat(path.join(pluginsPath, pluginPath))).isFile()) {
|
if (!plugin) {
|
||||||
return [plugin, await import (`./plugins/${pluginPath}`)];
|
console.log('No supported DeckTape plugin detected, falling back to generic plugin');
|
||||||
}
|
plugin = plugins['generic'].create(page, options);
|
||||||
}));
|
|
||||||
return Object.fromEntries(entries.filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createPlugin(page) {
|
|
||||||
let plugin;
|
|
||||||
if (!options.command || options.command === 'automatic') {
|
|
||||||
plugin = await createActivePlugin(page);
|
|
||||||
if (!plugin) {
|
|
||||||
console.log('No supported DeckTape plugin detected, falling back to generic plugin');
|
|
||||||
plugin = plugins['generic'].create(page, options);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
plugin = plugins[options.command].create(page, options);
|
|
||||||
if (!await plugin.isActive()) {
|
|
||||||
throw Error(`Unable to activate the ${plugin.getName()} DeckTape plugin for the address: ${options.url}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log(chalk.cyan(`${chalk.bold('%s')} plugin activated`), plugin.getName());
|
} else {
|
||||||
return plugin;
|
plugin = plugins[options.command].create(page, options);
|
||||||
}
|
if (!await plugin.isActive()) {
|
||||||
|
throw Error(`Unable to activate the ${plugin.getName()} DeckTape plugin for the address: ${options.url}`);
|
||||||
async function createActivePlugin(page) {
|
|
||||||
for (let id in plugins) {
|
|
||||||
if (id === 'generic') continue;
|
|
||||||
const plugin = plugins[id].create(page, options);
|
|
||||||
if (await plugin.isActive()) return plugin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(chalk.cyan(`${chalk.bold('%s')} plugin activated`), plugin.getName());
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
async function configurePage(plugin, page) {
|
async function createActivePlugin(page, plugins, options) {
|
||||||
if (!options.size) {
|
for (let id in plugins) {
|
||||||
options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 };
|
if (id === 'generic') continue;
|
||||||
}
|
const plugin = plugins[id].create(page, options);
|
||||||
await page.setViewport(options.size);
|
if (await plugin.isActive()) return plugin;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function configurePlugin(plugin) {
|
async function configurePage(page, plugin, options) {
|
||||||
if (typeof plugin.configure === 'function') {
|
if (!options.size) {
|
||||||
await plugin.configure();
|
options.size = typeof plugin.size === 'function' ? await plugin.size() : { width: 1280, height: 720 };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await page.setViewport(options.size);
|
||||||
|
}
|
||||||
|
|
||||||
async function exportSlides(plugin, page, pdf) {
|
async function configurePlugin(plugin) {
|
||||||
const context = {
|
if (typeof plugin.configure === 'function') {
|
||||||
progressBarOverflow : 0,
|
await plugin.configure();
|
||||||
currentSlide : 1,
|
}
|
||||||
exportedSlides : 0,
|
}
|
||||||
pdfFonts : {},
|
|
||||||
pdfXObjects : {},
|
async function exportSlides(page, plugin, pdf, options) {
|
||||||
totalSlides : await plugin.slideCount(),
|
const context = {
|
||||||
};
|
progressBarOverflow : 0,
|
||||||
// TODO: support a more advanced "fragment to pause" mapping
|
currentSlide : 1,
|
||||||
// for special use cases like GIF animations
|
exportedSlides : 0,
|
||||||
// TODO: support plugin optional promise to wait until a particular mutation
|
pdfFonts : {},
|
||||||
// instead of a pause
|
pdfXObjects : {},
|
||||||
|
totalSlides : await plugin.slideCount(),
|
||||||
|
};
|
||||||
|
// TODO: support a more advanced "fragment to pause" mapping
|
||||||
|
// for special use cases like GIF animations
|
||||||
|
// TODO: support plugin optional promise to wait until a particular mutation
|
||||||
|
// instead of a pause
|
||||||
|
if (options.slides && !options.slides[context.currentSlide]) {
|
||||||
|
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
|
||||||
|
} else {
|
||||||
|
await pause(options.pause);
|
||||||
|
await exportSlide(page, plugin, pdf, context, options);
|
||||||
|
}
|
||||||
|
const maxSlide = options.slides ? Math.max(...Object.keys(options.slides)) : Infinity;
|
||||||
|
let hasNext = await hasNextSlide(plugin, context);
|
||||||
|
while (hasNext && context.currentSlide < maxSlide) {
|
||||||
|
await nextSlide(plugin, context);
|
||||||
|
await pause(options.pause);
|
||||||
if (options.slides && !options.slides[context.currentSlide]) {
|
if (options.slides && !options.slides[context.currentSlide]) {
|
||||||
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
|
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
|
||||||
} else {
|
} else {
|
||||||
await pause(options.pause);
|
await exportSlide(page, plugin, pdf, context, options);
|
||||||
await exportSlide(plugin, page, pdf, context);
|
|
||||||
}
|
}
|
||||||
const maxSlide = options.slides ? Math.max(...Object.keys(options.slides)) : Infinity;
|
hasNext = await hasNextSlide(plugin, context);
|
||||||
let hasNext = await hasNextSlide(plugin, context);
|
|
||||||
while (hasNext && context.currentSlide < maxSlide) {
|
|
||||||
await nextSlide(plugin, context);
|
|
||||||
await pause(options.pause);
|
|
||||||
if (options.slides && !options.slides[context.currentSlide]) {
|
|
||||||
process.stdout.write('\r' + await progressBar(plugin, context, { skip: true }));
|
|
||||||
} else {
|
|
||||||
await exportSlide(plugin, page, pdf, context);
|
|
||||||
}
|
|
||||||
hasNext = await hasNextSlide(plugin, context);
|
|
||||||
}
|
|
||||||
// Flush consolidated fonts
|
|
||||||
Object.values(context.pdfFonts).forEach(({ ref, font }) => {
|
|
||||||
pdf.context.assign(ref, pdf.context.flateStream(font.write({ type: 'ttf', hinting: true })));
|
|
||||||
});
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
// Flush consolidated fonts
|
||||||
|
Object.values(context.pdfFonts).forEach(({ ref, font }) => {
|
||||||
|
pdf.context.assign(ref, pdf.context.flateStream(font.write({ type: 'ttf', hinting: true })));
|
||||||
|
});
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
async function exportSlide(plugin, page, pdf, context) {
|
async function exportSlide(page, plugin, pdf, context, options) {
|
||||||
process.stdout.write('\r' + await progressBar(plugin, context));
|
process.stdout.write('\r' + await progressBar(plugin, context));
|
||||||
|
|
||||||
const buffer = await page.pdf({
|
const buffer = await page.pdf({
|
||||||
width : options.size.width,
|
width : options.size.width,
|
||||||
height : options.size.height,
|
height : options.size.height,
|
||||||
printBackground : true,
|
printBackground : true,
|
||||||
pageRanges : '1',
|
pageRanges : '1',
|
||||||
displayHeaderFooter : false,
|
displayHeaderFooter : false,
|
||||||
timeout : options.bufferTimeout,
|
timeout : options.bufferTimeout,
|
||||||
});
|
});
|
||||||
await printSlide(pdf, await PDFDocument.load(buffer, { parseSpeed: ParseSpeeds.Fastest }), context);
|
await printSlide(pdf, await PDFDocument.load(buffer, { parseSpeed: ParseSpeeds.Fastest }), context);
|
||||||
context.exportedSlides++;
|
context.exportedSlides++;
|
||||||
|
|
||||||
if (options.screenshots) {
|
if (options.screenshots) {
|
||||||
for (let resolution of options.screenshotSizes || [options.size]) {
|
for (let resolution of options.screenshotSizes || [options.size]) {
|
||||||
await page.setViewport(resolution);
|
await page.setViewport(resolution);
|
||||||
// Delay page rendering to wait for the resize event to complete,
|
// Delay page rendering to wait for the resize event to complete,
|
||||||
// e.g. for impress.js (may be needed to be configurable)
|
// e.g. for impress.js (may be needed to be configurable)
|
||||||
await pause(1000);
|
await pause(1000);
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: path.join(options.screenshotDirectory, options.filename
|
path: path.join(options.screenshotDirectory, options.filename
|
||||||
.replace('.pdf', `_${context.currentSlide}_${resolution.width}x${resolution.height}.${options.screenshotFormat}`)),
|
.replace('.pdf', `_${context.currentSlide}_${resolution.width}x${resolution.height}.${options.screenshotFormat}`)),
|
||||||
fullPage: false,
|
fullPage: false,
|
||||||
omitBackground: true,
|
omitBackground: true,
|
||||||
});
|
});
|
||||||
await page.setViewport(options.size);
|
await page.setViewport(options.size);
|
||||||
await pause(1000);
|
await pause(1000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printSlide(pdf, slide, context) {
|
||||||
|
const duplicatedEntries = [];
|
||||||
|
const [page] = await pdf.copyPages(slide, [0]);
|
||||||
|
pdf.addPage(page);
|
||||||
|
// Traverse the page to consolidate duplicates
|
||||||
|
parseResources(page.node);
|
||||||
|
// And delete all the collected duplicates
|
||||||
|
duplicatedEntries.forEach(ref => pdf.context.delete(ref));
|
||||||
|
|
||||||
|
function parseResources(dictionary) {
|
||||||
|
const resources = dictionary.get(PDFName.Resources);
|
||||||
|
if (resources.has(PDFName.XObject)) {
|
||||||
|
const xObject = resources.get(PDFName.XObject);
|
||||||
|
xObject.entries().forEach(entry => parseXObject(entry, xObject));
|
||||||
|
}
|
||||||
|
if (resources.has(PDFName.Font)) {
|
||||||
|
resources.get(PDFName.Font).entries().forEach(parseFont);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printSlide(pdf, slide, context) {
|
function parseXObject([name, entry], xObject) {
|
||||||
const duplicatedEntries = [];
|
const object = page.node.context.lookup(entry);
|
||||||
const [page] = await pdf.copyPages(slide, [0]);
|
const subtype = object.dict.get(PDFName.of('Subtype'));
|
||||||
pdf.addPage(page);
|
if (subtype === PDFName.of('Image')) {
|
||||||
// Traverse the page to consolidate duplicates
|
const digest = crypto.createHash('SHA1').update(object.contents).digest('hex');
|
||||||
parseResources(page.node);
|
if (!context.pdfXObjects[digest]) {
|
||||||
// And delete all the collected duplicates
|
context.pdfXObjects[digest] = entry;
|
||||||
duplicatedEntries.forEach(ref => pdf.context.delete(ref));
|
|
||||||
|
|
||||||
function parseResources(dictionary) {
|
|
||||||
const resources = dictionary.get(PDFName.Resources);
|
|
||||||
if (resources.has(PDFName.XObject)) {
|
|
||||||
const xObject = resources.get(PDFName.XObject);
|
|
||||||
xObject.entries().forEach(entry => parseXObject(entry, xObject));
|
|
||||||
}
|
|
||||||
if (resources.has(PDFName.Font)) {
|
|
||||||
resources.get(PDFName.Font).entries().forEach(parseFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseXObject([name, entry], xObject) {
|
|
||||||
const object = page.node.context.lookup(entry);
|
|
||||||
const subtype = object.dict.get(PDFName.of('Subtype'));
|
|
||||||
if (subtype === PDFName.of('Image')) {
|
|
||||||
const digest = crypto.createHash('SHA1').update(object.contents).digest('hex');
|
|
||||||
if (!context.pdfXObjects[digest]) {
|
|
||||||
context.pdfXObjects[digest] = entry;
|
|
||||||
} else {
|
|
||||||
xObject.set(name, context.pdfXObjects[digest]);
|
|
||||||
duplicatedEntries.push(entry);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
parseResources(object.dict);
|
xObject.set(name, context.pdfXObjects[digest]);
|
||||||
|
duplicatedEntries.push(entry);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function parseFont([_, entry]) {
|
|
||||||
const object = page.node.context.lookup(entry);
|
|
||||||
const subtype = object.get(PDFName.of('Subtype'));
|
|
||||||
// See "Introduction to Font Data Structures" from PDF specification
|
|
||||||
if (subtype === PDFName.of('Type0')) {
|
|
||||||
// TODO: properly support composite fonts with multiple descendants
|
|
||||||
const descendant = page.node.context.lookup(object.get(PDFName.of('DescendantFonts')).get(0));
|
|
||||||
if (descendant.get(PDFName.of('Subtype')) === PDFName.of('CIDFontType2')) {
|
|
||||||
const descriptor = page.node.context.lookup(descendant.get(PDFName.of('FontDescriptor')));
|
|
||||||
const ref = descriptor.get(PDFName.of('FontFile2'));
|
|
||||||
const file = page.node.context.lookup(ref);
|
|
||||||
if (!file) {
|
|
||||||
// The font has already been processed and removed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bytes = decodePDFRawStream(file).decode();
|
|
||||||
const font = Font.create(Buffer.from(bytes), { type: 'ttf', hinting: true });
|
|
||||||
// Some fonts happen to have no metadata, which is required by fonteditor
|
|
||||||
if (!font.data.name) {
|
|
||||||
font.data.name = {};
|
|
||||||
}
|
|
||||||
// PDF font name does not contain sub family on Windows 10,
|
|
||||||
// so a more robust key is computed from the font metadata
|
|
||||||
const id = descriptor.get(PDFName.of('FontName')).value() + ' - ' + fontMetadataKey(font.data.name);
|
|
||||||
if (context.pdfFonts[id]) {
|
|
||||||
const f = context.pdfFonts[id].font;
|
|
||||||
font.data.glyf.forEach((g, i) => {
|
|
||||||
if (g.contours && g.contours.length > 0) {
|
|
||||||
if (!f.data.glyf[i] || !f.data.glyf[i].contours || f.data.glyf[i].contours.length === 0) {
|
|
||||||
mergeGlyph(f, i, g);
|
|
||||||
}
|
|
||||||
} else if (g.compound) {
|
|
||||||
if (!f.data.glyf[i] || typeof f.data.glyf[i].compound === 'undefined') {
|
|
||||||
mergeGlyph(f, i, g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
descriptor.set(PDFName.of('FontFile2'), context.pdfFonts[id].ref);
|
|
||||||
duplicatedEntries.push(ref);
|
|
||||||
} else {
|
|
||||||
context.pdfFonts[id] = { ref: ref, font: font };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function mergeGlyph(font, index, glyf) {
|
|
||||||
if (font.data.glyf.length <= index) {
|
|
||||||
for (let i = font.data.glyf.length; i < index; i++) {
|
|
||||||
font.data.glyf.push({ contours: Array(0), advanceWidth: 0, leftSideBearing: 0 });
|
|
||||||
}
|
|
||||||
font.data.glyf.push(glyf);
|
|
||||||
} else {
|
|
||||||
font.data.glyf[index] = glyf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fontMetadataKey(font) {
|
|
||||||
const keys = ['fontFamily', 'fontSubFamily', 'fullName', 'preferredFamily', 'preferredSubFamily', 'uniqueSubFamily'];
|
|
||||||
return Object.entries(font)
|
|
||||||
.filter(([key, _]) => keys.includes(key))
|
|
||||||
.reduce((r, [k, v], i) => r + (i > 0 ? ',' : '') + k + '=' + v, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasNextSlide(plugin, context) {
|
|
||||||
if (typeof plugin.hasNextSlide === 'function') {
|
|
||||||
return await plugin.hasNextSlide();
|
|
||||||
} else {
|
} else {
|
||||||
return context.currentSlide < context.totalSlides;
|
parseResources(object.dict);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseFont([_, entry]) {
|
||||||
|
const object = page.node.context.lookup(entry);
|
||||||
|
const subtype = object.get(PDFName.of('Subtype'));
|
||||||
|
// See "Introduction to Font Data Structures" from PDF specification
|
||||||
|
if (subtype === PDFName.of('Type0')) {
|
||||||
|
// TODO: properly support composite fonts with multiple descendants
|
||||||
|
const descendant = page.node.context.lookup(object.get(PDFName.of('DescendantFonts')).get(0));
|
||||||
|
if (descendant.get(PDFName.of('Subtype')) === PDFName.of('CIDFontType2')) {
|
||||||
|
const descriptor = page.node.context.lookup(descendant.get(PDFName.of('FontDescriptor')));
|
||||||
|
const ref = descriptor.get(PDFName.of('FontFile2'));
|
||||||
|
const file = page.node.context.lookup(ref);
|
||||||
|
if (!file) {
|
||||||
|
// The font has already been processed and removed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bytes = decodePDFRawStream(file).decode();
|
||||||
|
const font = Font.create(Buffer.from(bytes), { type: 'ttf', hinting: true });
|
||||||
|
// Some fonts happen to have no metadata, which is required by fonteditor
|
||||||
|
if (!font.data.name) {
|
||||||
|
font.data.name = {};
|
||||||
|
}
|
||||||
|
// PDF font name does not contain sub family on Windows 10,
|
||||||
|
// so a more robust key is computed from the font metadata
|
||||||
|
const id = descriptor.get(PDFName.of('FontName')).value() + ' - ' + fontMetadataKey(font.data.name);
|
||||||
|
if (context.pdfFonts[id]) {
|
||||||
|
const f = context.pdfFonts[id].font;
|
||||||
|
font.data.glyf.forEach((g, i) => {
|
||||||
|
if (g.contours && g.contours.length > 0) {
|
||||||
|
if (!f.data.glyf[i] || !f.data.glyf[i].contours || f.data.glyf[i].contours.length === 0) {
|
||||||
|
mergeGlyph(f, i, g);
|
||||||
|
}
|
||||||
|
} else if (g.compound) {
|
||||||
|
if (!f.data.glyf[i] || typeof f.data.glyf[i].compound === 'undefined') {
|
||||||
|
mergeGlyph(f, i, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
descriptor.set(PDFName.of('FontFile2'), context.pdfFonts[id].ref);
|
||||||
|
duplicatedEntries.push(ref);
|
||||||
|
} else {
|
||||||
|
context.pdfFonts[id] = { ref: ref, font: font };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function mergeGlyph(font, index, glyf) {
|
||||||
|
if (font.data.glyf.length <= index) {
|
||||||
|
for (let i = font.data.glyf.length; i < index; i++) {
|
||||||
|
font.data.glyf.push({ contours: Array(0), advanceWidth: 0, leftSideBearing: 0 });
|
||||||
|
}
|
||||||
|
font.data.glyf.push(glyf);
|
||||||
|
} else {
|
||||||
|
font.data.glyf[index] = glyf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nextSlide(plugin, context) {
|
function fontMetadataKey(font) {
|
||||||
context.currentSlide++;
|
const keys = ['fontFamily', 'fontSubFamily', 'fullName', 'preferredFamily', 'preferredSubFamily', 'uniqueSubFamily'];
|
||||||
return plugin.nextSlide();
|
return Object.entries(font)
|
||||||
|
.filter(([key, _]) => keys.includes(key))
|
||||||
|
.reduce((r, [k, v], i) => r + (i > 0 ? ',' : '') + k + '=' + v, '');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: add progress bar, duration, ETA and file size
|
async function hasNextSlide(plugin, context) {
|
||||||
async function progressBar(plugin, context, { skip } = { skip : false }) {
|
if (typeof plugin.hasNextSlide === 'function') {
|
||||||
const cols = [];
|
return await plugin.hasNextSlide();
|
||||||
const index = await plugin.currentSlideIndex();
|
} else {
|
||||||
cols.push(`${skip ? 'Skipping' : 'Printing'} slide `);
|
return context.currentSlide < context.totalSlides;
|
||||||
cols.push(`#${index}`.padEnd(8));
|
|
||||||
cols.push(' (');
|
|
||||||
cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3));
|
|
||||||
cols.push('/');
|
|
||||||
cols.push(context.totalSlides || ' ?');
|
|
||||||
cols.push(') ...');
|
|
||||||
// erase overflowing slide fragments
|
|
||||||
cols.push(' '.repeat(Math.max(context.progressBarOverflow - Math.max(index.length + 1 - 8, 0), 0)));
|
|
||||||
context.progressBarOverflow = Math.max(index.length + 1 - 8, 0);
|
|
||||||
return cols.join('');
|
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
async function nextSlide(plugin, context) {
|
||||||
|
context.currentSlide++;
|
||||||
|
return plugin.nextSlide();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePdf(filename, pdf) {
|
||||||
|
const pdfDir = path.dirname(filename);
|
||||||
|
try {
|
||||||
|
fs.accessSync(pdfDir, fs.constants.F_OK);
|
||||||
|
} catch {
|
||||||
|
fs.mkdirSync(pdfDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filename, await pdf.save({ addDefaultPage: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add progress bar, duration, ETA and file size
|
||||||
|
async function progressBar(plugin, context, { skip } = { skip: false }) {
|
||||||
|
const cols = [];
|
||||||
|
const index = await plugin.currentSlideIndex();
|
||||||
|
cols.push(`${skip ? 'Skipping' : 'Printing'} slide `);
|
||||||
|
cols.push(`#${index}`.padEnd(8));
|
||||||
|
cols.push(' (');
|
||||||
|
cols.push(`${context.currentSlide}`.padStart(context.totalSlides ? context.totalSlides.toString().length : 3));
|
||||||
|
cols.push('/');
|
||||||
|
cols.push(context.totalSlides || ' ?');
|
||||||
|
cols.push(') ...');
|
||||||
|
// erase overflowing slide fragments
|
||||||
|
cols.push(' '.repeat(Math.max(context.progressBarOverflow - Math.max(index.length + 1 - 8, 0), 0)));
|
||||||
|
context.progressBarOverflow = Math.max(index.length + 1 - 8, 0);
|
||||||
|
return cols.join('');
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user