AzureFunctions + puppeteer でPDF出力

## 概要
AzureFunctions + puppeteerでPDFを出力する。
生成したPDFはAzureStorageに保存する。

## How to
「AzureFunctions + puppeteer」でスクショを撮る方法は以下を参照。
https://anthonychu.ca/post/azure-functions-headless-chromium-puppeteer-playwright/


## コード
実際に動かしたコードをちょっと変えています（大人の事情）。
動作確認してないので、動かないかもです。参考程度にご覧ください。
ガーと書いたので、ツッコミポイントは多いですが、スルーしてください。

```ts
import * as puppeteer from "puppeteer";
import { BlobServiceClient, BlockBlobClient } from "@azure/storage-blob";
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import * as util from "util";
import { jsPDF } from "jspdf";
import { promises as fs } from "fs";
import { exec } from "child_process";
import imageSize from "image-size";

const execAsync = util.promisify(exec);
const AZURE_STORAGE_CONNECTION_STRING ="接続文字";

/**
 * Azure Function上にフォントファイルを設置する
 * https://github.com/Azure/azure-functions-host/issues/4883#issuecomment-683380515
 */
const fontUpdate = async () => {
  const fontPath = process.env.FONT_PATH || "/home/.fonts";
  try {
    await fs.stat(fontPath);
    return;
  } catch {}

  // ln -s /home/site/wwwroot/fonts $FONT_PATH
  await execAsync(`ln -s /home/site/wwwroot/fonts ${fontPath}`);
};

const getBlockBlobClient = (name: string) => {
  const blobServiceClient = BlobServiceClient.fromConnectionString(
    AZURE_STORAGE_CONNECTION_STRING
  );
  const containerClient = blobServiceClient.getContainerClient("pdf");
  return containerClient.getBlockBlobClient(`${name}.pdf`);
};

const gotoPdfPage = async (
  page: puppeteer.Page
) => {
  // スクショ撮るページ
  const url = `https://www.google.com/?hl=ja`;
  await Promise.all([
    page.waitForNavigation({ waitUntil: ["load", "networkidle2"] }),
    page.goto(url),
  ]);
};

const screenShotPdf = async (page: puppeteer.Page) => {
  const image = (await page.screenshot({
    type: "jpeg",
    quality: 85,
    fullPage: true,
  })) as Buffer;
  return image;
};

const createPdf = (image: Buffer, name: string) => {
  const size = imageSize(image);

  const pdf = new jsPDF({
    format: "a4",
    unit: "mm",
    orientation: "p",
  });
  const ratio = pdf.internal.pageSize.height;

  pdf.addImage(
    image,
    "JPEG",
    0,
    0,
    size.width * ratio,
    size.height * ratio,
    "",
    "FAST"
  );
  const pdfPath = `/tmp/${name}.pdf`;
  pdf.save(pdfPath);

  return pdfPath;
};

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const name = req.query?.name;

  if (name) {
    try {
      const pdfBlob = getBlockBlobClient(name);

      // 既に存在する場合は何もしない
      if (await pdfBlob.exists()) {
        context.res = {
          status: 200,
          body: "already exists",
        };
        return;
      }

      await fontUpdate();

      const browser = await puppeteer.launch({
        args: [
          "--no-sandbox",
          "--disable-setuid-sandbox",
          "--disable-web-security",
        ],
      });

      const page = await browser.newPage();
      await gotoPdfPage(page, name);

      const image = await screenShotPdf(page);

      const pdfPath = createPdf(image,name);

      pdfBlob.uploadFile(pdfPath);

      browser.close();
    } catch (e) {
      context.res = {
        status: 400,
        body: `error: ${e}`,
      };
      context.done();
      return;
    }

    context.res = {
      status: 200,
      body: "success",
    };
    context.done();
    return;
  } else {
    context.res = {
      status: 400,
      body: `name:${name}`,
    };
    context.done();
    return;
  }
};

export = httpTrigger;
```

### 大雑把な流れ
1. AzureStorageに既に存在するかチェックする
2. フォントを設定する
3. ヘッドレスブラウザを起動
4. ページに移動
5. ページのスクリーンショットを取得
6. スクショの画像をPDFのA4サイズにリサイズしてPDFに貼り付ける
7. AzureStorageにPDFをアップロード

## 詰まりポイント
### 日本語のフォントを読み込む必要あり
https://github.com/Azure/azure-functions-host/issues/4883#issuecomment-683380515

[`postinstall`](https://github.com/horihiro/azure-functions-puppeteer-node-cjkfont/blob/master/package.json#L7) で [`postinstall.sh`](https://github.com/horihiro/azure-functions-puppeteer-node-cjkfont/blob/master/postinstall.sh#L5)を動かします。

### スクショの画像サイズ取得
画像のサイズを取得してA4サイズとの比率を計算する必要がありました。
この画像サイズを取得するのに詰まりました。
便利なライブラリ[image-size](https://www.npmjs.com/package/image-size)を使用しました。
使いやすいです。

### browserのオプション
以下のオプションを設定しないと、出力できませんでした。

```ts
      const browser = await puppeteer.launch({
        args: [
          "--no-sandbox",
          "--disable-setuid-sandbox",
          "--disable-web-security",
        ],
      });
```

### 一時ファイルの保存場所
一時的にファイルを保存したい場合、`/tmp/*`に保存する。
AzureFunctionsの仕様？