1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-12

課題

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

準備

chrome-aws-lambda

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

日本語フォント

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

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

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

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

実装

早速ですが、以下のコードになりました。

  • puppeteerを起動し、PDFファイル化したいページに遷移(引数でloadの完了待ちを指定)
  • フォントは/.fontsフォルダに格納しておき、パス指定でロード
  • PDF化した結果の文字データをレスポンスにbase64で書き出し
  • 使用メモリの設定は大きめに(最低512MB)
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タグ経由にて書き出し
    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側でやることは下記の2つですが、場合によってはクライアント側の作り込みと、AWSの各種設定が必要です。

  1. chrome-aws-lambdaをLayer化して利用
  2. PDF化した結果の文字データをレスポンスにbase64で書き出し

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

最後に

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?