Lambdaを使ったPDFファイル作成とダウンロード

## 課題

Lambdaを利用したSPA(Vue)アプリケーションにおいて、ユーザーに必要な情報をPDFファイルにまとめてダウンロードさせる必要が生じました。
「Lambda」と「PDF作成」で検索すると、`chrome-aws-lambda`を利用してPDFファイル作成を行えば実現できそうなことがわかったのですが、PDFファイルを生成して直接ダウンロードさせるようなコードは見つからなかったため調査・作成することにしました。

## 準備

### chrome-aws-lambda

Puppeteer を使ってPDFファイル作成を行うため、まずは chrome-aws-lambda をLambda Layerとして登録しました。
詳細な手順は以下の記事にありましたので、それを参考に進めています。

https://dev.classmethod.jp/articles/run-headless-chrome-puppeteer-on-aws-lambda/

### 日本語フォント

デフォルトの環境だと日本語が文字化けしてしまうようです。
参考にしたコードではURL参照でロードしているものもありましたが、プロダクトとしては自前で抱えておきたいので、一旦使用するフォントをダウンロードしておきます。
今回はGoogleのWebフォント「Noto Sans CJK JP」を使用しました。

https://github.com/minoryorg/Noto-Sans-CJK-JP

補足）全てのフォントを含めるとサイズが大きいので、使用するフォントのみに絞った方が良さそうです

### PDFファイル出力するWebページ

PDFファイルの元となるWebページを準備しました。
ページの初期処理でHTTPリクエストにより必要な情報を取得する実装でも問題ない気がしましたが、今回はクエリ文字列にて引き渡すようにしています。（後述のコードでは単純化したのでわからない部分です）
ユーザーのブラウザとは違うセッションとなるので、この点考慮が必要です。

## 実装

早速ですが、以下のコードになりました。
- puppeteerを起動し、PDFファイル化したいページに遷移（引数でloadの完了待ちを指定）
- フォントは/.fontsフォルダに格納しておき、パス指定でロード
- PDF化した結果の文字データをレスポンスにbase64で書き出し
- 使用メモリの設定は大きめに（最低512MB）

```javascript
const chromium = require('chrome-aws-lambda')
const createBrowser = async () => {
  const fontPath = `${process.env.LAMBDA_TASK_ROOT}/.fonts/NotoSansMonoJP-Regular.otf`
  await chromium.font(fontPath)
  
  return chromium.puppeteer.launch({
    args: chromium.args,
    defaultViewport: chromium.defaultViewport,
    executablePath: await chromium.executablePath,
    headless: chromium.headless,
    ignoreHTTPSErrors: true,
  })
}

exports.handler = async (event) => {
  let browser = null
  try {    
    browser = await createBrowser()
    const page = await browser.newPage()
    
    // ここにPDF化したいページのURLを設定
    const url = event.queryStringParameters.url
    
    await page.goto(url, { waitUntil: 'load', timeout: 0 })
    
    const contentType = "application/pdf"
    const buffer = await page.pdf({
      format: 'A4',
      displayHeaderFooter: false,
      printBackground: true,
      preferCSSPageSize: true,
      margin: { top: '1.5cm', bottom: '1.5cm', right: '1.5cm', left: '1.5cm' },
    })
    
    return {
        "statusCode": 200,
        "headers":{
            "Access-Control-Allow-Origin" : "*",
            "Access-Control-Allow-Methods": "*",
            "Access-Control-Expose-Headers" : "Content-Disposition",
            "Content-Type": contentType,
            "Content-Disposition": "attachment; filename=" + encodeURI("サンプル.pdf")
        },
        "isBase64Encoded": true,
        "body": buffer.toString("base64")
    }
      
  } catch (error) {
    console.log(error)

  } finally {
    if (browser !== null) {
      await browser.close()
    }
  }
  
}
```

## 実装（Vue+axios）

- Acceptヘッダを付与してリクエストする
- 結果Blobとしてaタグ経由にて書き出し

```javascript
    async download(jwtToken, { url, data, params }) {
      const headers = {}
      headers['Content-Type'] = 'application/json'
      headers['Accept'] = 'application/pdf'
      headers['Authorization'] = 'Bearer ' + jwtToken
      
      const getFileNameFromHeader = (content, defaultName='download.pdf') => {
        const regex = content.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
        if (regex === null) return defaultName
        return decodeURI(regex[1]) || defaultName
      }
      return axios
        .get(url, {
          headers,
          data,
          params,
          responseType: 'blob'
        })
        .then(({headers, data}) => {
          const contentDisposition = headers['content-disposition'] || ''
          const fileName = getFileNameFromHeader(contentDisposition)
    
          const link = document.createElement('a')
          const downloadUrl = window.URL.createObjectURL(new Blob([data]))
          link.href = downloadUrl
          link.setAttribute('download', fileName) //any other extension
          document.body.appendChild(link)
          link.click()
          link.remove()
        })
    }
```

### APIGateway

- 設定のバイナリメディアタイプに`application/pdf`を指定しておく

## まとめ

Lambda側でやることは下記の２つですが、場合によってはクライアント側の作り込みと、AWSの各種設定が必要です。
1. `chrome-aws-lambda`をLayer化して利用
1. PDF化した結果の文字データをレスポンスにbase64で書き出し

実装当初はダウンロード時に壊れたPDFファイルとなってしまい、リクエストヘッダのAccept設定・APIGatewayのバイナリメディアタイプ設定が必要なことがわかるまで、何が間違っているのか試行錯誤で苦労しました…

## 最後に

Lambda実装はpage.pdfで戻ってきたデータをbase64で書き出すだけで、意外に簡単に実装できました。
単純な内容のPDFファイル作成でも10秒程度かかったりするので、SnapStart的な仕組みが欲しいです。
LambdaでPDFファイルを作成する方法について色々調べた結果の実装ですが、他に簡単な方法があればコメントにて教えて頂けると助かります。