JS генерация PDF в Firebase Cloud Function

213
views

Для генерации PDF существует множество JavaScript библиотек, одной из лучших можно считать pdfMake. Её можно использовать как на клиенте, так и на сервере. Но минифицированный бандл библиотеки весит больше мегабайта. Именно поэтому мы поговорим о том, как использовать эту библиотеку в cloud function.

Инициализация и импорт

Прежде всего внутри папки function устанавливаем библиотеку:

npm install pdfmake

В js файле функции заимпортим ее и бандл шрифтов Roboto, который идет с библиотекой.

const Printer = require('pdfmake');
const fonts = require('pdfmake/build/vfs_fonts.js');

Бандл с шрифтами необходим для инициализации класса принтера. Важно заметить, что в cloud functions нет возможности указать относительный путь к шрифтам, именно поэтому нам нужен бандл с шрифтами, который идет с библиотекой. Использовать его мы будем таким образом:

const fontDescriptors = {
  Roboto: {
    normal: new Buffer(fonts.pdfMake.vfs['Roboto-Regular.ttf'], 'base64'),
    bold: new Buffer(fonts.pdfMake.vfs['Roboto-Medium.ttf'], 'base64'),
    italics: new Buffer(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
    bolditalics: new Buffer(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
  }
};

Следовательно, когда стартует функция, шрифты уже будут в ней.

Тело эндпоинта для генерации PDF

exports.generatePdf = functions.https.onRequest(async (req, res) => {
  if (request.method !== "GET") {
    response.send(405, 'HTTP Method ' + request.method + ' not allowed');
    return null;
  }

  // Код генерации PDF будет тут и возвращать результат в response.send();

});

Дальнейший код мы будем писать в тело этой функции.

Инициализируем инстанс класса принтера с вышеупомянутыми шрифтами, а так же создадим переменную, которая будет собирать чанки pdf файла при его генерации.

const printer = new Printer(fontDescriptors);
const chunks = [];

Теперь для генерации pdf, библиотеке нужно передать объект, описывающий структуру документа. Подробно с синтаксисом и функционалом можно ознакомиться в документации библиотеки.

Мы для примера сделаем простой статичный шаблон.

const docDefinition = {
  content: [
    // if you don't need styles, you can use a simple string to define a paragraph
    'This is a standard paragraph, using default style',

    // using a { text: '...' } object lets you set styling properties
    { text: 'This paragraph will have a bigger font', fontSize: 15 },

    // if you set the value of text to an array instead of a string, you'll be able
    // to style any part individually
    {
      text: [
        'This paragraph is defined as an array of elements to make it possible to ',
        { text: 'restyle part of it and make it bigger ', fontSize: 15 },
        'than the rest.'
      ]
    }
  ]
};

Обратите внимание, что генерировать этот шаблон можно динамически, подтянув необходимые данные например из базы firebase или взяв из параметров url запроса.

Также при желании можно добавить параметры документа (формат, ориентацию) и мета-описание документа ( авторство, описание, тайтл ).

Остается лишь передать шаблон в генератор.

const pdfDoc = printer.createPdfKitDocument(docDefinition);

Вызвав данный метод у принтера имеются колбэки, которые вызываются при генерации pdf файла. Их мы будем использовать для создания итогового blob файла, который отдадим клиенту в респонсе.

pdfDoc.on('data', (chunk) => {
  chunks.push(chunk);
});
pdfDoc.on('end', () => {
  var result = Buffer.concat(chunks);
  response.setHeader('Content-Type', 'application/pdf');
  response.setHeader('Content-disposition', 'attachment; filename=report.pdf');
  response.send(result);
});
pdfDoc.on('error', (err) => {
  response.status(501).send(err);
});
pdfDoc.end();

При успехе мы задаем хедеры ответа: тип респонса application/pdf и имя файла. Затем возвращаем его клиенту. При ошибке рендеринга можно вернуть 501 ошибку пользователю.

В результате у нас функция будет иметь следующий вид:

const Printer = require('pdfmake');
const fonts = require('pdfmake/build/vfs_fonts.js');

const fontDescriptors = {
  Roboto: {
    normal: new Buffer(fonts.pdfMake.vfs['Roboto-Regular.ttf'], 'base64'),
    bold: new Buffer(fonts.pdfMake.vfs['Roboto-Medium.ttf'], 'base64'),
    italics: new Buffer(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
    bolditalics: new Buffer(fonts.pdfMake.vfs['Roboto-Italic.ttf'], 'base64'),
  }
};

exports.generatePdf = functions.https.onRequest(async (req, res) => {
  if (request.method !== "GET") {
    response.send(405, 'HTTP Method ' + request.method + ' not allowed');
    return null;
  }

  const printer = new Printer(fontDescriptors);
  const chunks = [];

  const docDefinition = {
    content: [
      // if you don't need styles, you can use a simple string to define a paragraph
      'This is a standard paragraph, using default style',

      // using a { text: '...' } object lets you set styling properties
      {
        text: 'This paragraph will have a bigger font',
        fontSize: 15
      },

      // if you set the value of text to an array instead of a string, you'll be able
      // to style any part individually
      {
        text: [
          'This paragraph is defined as an array of elements to make it possible to ',
          {
            text: 'restyle part of it and make it bigger ',
            fontSize: 15
          },
          'than the rest.'
        ]
      }
    ]
  };

  const pdfDoc = printer.createPdfKitDocument(docDefinition);

  pdfDoc.on('data', (chunk) => {
    chunks.push(chunk);
  });
  pdfDoc.on('end', () => {
    var result = Buffer.concat(chunks);
    response.setHeader('Content-Type', 'application/pdf');
    response.setHeader('Content-disposition', 'attachment; filename=report.pdf');
    response.send(result);
  });
  pdfDoc.on('error', (err) => {
    response.status(501).send(err);
  });
  pdfDoc.end();

});

Таким образом вызвав cloud function generatePdf будет открываться сразу pdf файл. При желании можно сделать загрузку этого файла в сторедж и возвращать в респонсе ссылку на сохраненный файл.

Подписывайтесь

Для получения уведомлений о новых публикациях подписывайтесь на мой блог или страницы в соц. сетях: Twitter, Facebook.

Подписаться на блог по эл. почте