LoginSignup
1
1

More than 3 years have passed since last update.

PdfMakeを使ったPDF作成APIをZeit Nowで動かしたメモ

Last updated at Posted at 2020-04-07

概要

PDFをサーバサイドで作成し、ダウンロードするAPIを作成した。
ローカルで動いていたものがデプロイ時に動かないなどのトラブルがあったが、
最終的には成功できた。

作成したソース

ローカル環境

  • node:13.12.0
  • Now CLI 17.1.1

フォルダ構成

- api
  - pdf.ts
- fonts
  - ipaexg.ttf
  - ipaexm.ttf
- src
  - pages
    - index.tsx
    - PdfArea.tsx
- .babelrc.js
- next-env.d.ts
- next.config.js
- now.json
- package.json
- tsconfig.json

ハマった点

APIをsrc/pages/api/pdf.tsに書いていたとき、ローカルで動かすときには成功し、デプロイしたときにエラーとなる現象が発生した。
デプロイ時にはfontsフォルダが見えなくてファイルが開けない問題が発生していた。
apiはソース通りのフォルダ構成ではなく、ラムダ環境にコンパイル済で配置されていたことが原因と考えられる。
apiファイルの場所を、src/pages/apiから直下のapiに移し、now.jsonにincluceFilesを追加することで解決した。

now.json
{
  "version": 2,
  "functions": {
    "api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}

incluceFilesを使うときは、nextの仕組みのpages/apiは使えない模様。
以下では、コンパイルは通るが、フォルダがアップロードされなかった。

now.jsonのうまくいかなかった例
{
  "version": 2,
  "functions": {
    "src/pages/api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}

ファイル

src/pages/index.tsx
import Head from 'next/head'
import * as React from 'react'
import { NextPage } from 'next'
import PdfArea from './PdfArea'

const Home: NextPage = () => {
  return (
    <div className="container">
      <main>
        <PdfArea />
      </main>
    </div>
  )
}

export default Home
src/pages/PdfArea.tsx
import React, { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import scenarioModule, {
  useScenario,
  usePdf
} from '../store/modules/scenarioModule'

const makePdf = async (scenario, dispatch) => {
  const pdf = await (
    await fetch('/api/pdf', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify(scenario)
    })
  ).text()
  dispatch(scenarioModule.actions.setPdf(pdf))
}

const PdfArea: React.FC = () => {
  const scenario = useScenario()
  const pdf = usePdf()
  const dispatch = useDispatch()

  if (!scenario) {
    return <div>読込失敗</div>
  }
  return (
    <>
      <button onClick={(e) => makePdf(scenario, dispatch)}>PDFを作る</button>
      {pdf !== '' && (
        <a href={pdf} download="scenario.pdf">
          作成したPDFをダウンロード
        </a>
      )}
    </>
  )
}

export default PdfArea
api/pdf.ts
import { NextApiRequest, NextApiResponse } from 'next'
import path from 'path'
import PdfPrinter from 'pdfmake'
import { Scenario } from '../src/store/modules/scenarioModule'

function createPdfBinary(pdfDoc, callback) {
  const baseDir = 'fonts/'
  const gPath = path.resolve(baseDir + 'ipaexg.ttf')
  const mPath = path.resolve(baseDir + 'ipaexm.ttf')
  const fontDescriptors = {
    IPASerif: {
      normal: mPath,
      bold: mPath,
      italics: mPath,
      bolditalics: mPath
    },
    IPAGothic: {
      normal: gPath,
      bold: gPath,
      italics: gPath,
      bolditalics: gPath
    }
  }
  const printer = new PdfPrinter(fontDescriptors)
  const doc = printer.createPdfKitDocument(pdfDoc)
  const chunks = []
  doc.on('data', function(chunk) {
    chunks.push(chunk)
  })
  doc.on('end', function() {
    const result = Buffer.concat(chunks)
    callback('data:application/pdf;base64,' + result.toString('base64'))
  })
  doc.end()
}

export default (req: NextApiRequest, res: NextApiResponse) => {
  const scenario: Scenario = req.body
  const docDefinition = {
    content: [
      { text: scenario.title, fontSize: 55, font: 'IPASerif' },
    ],
    defaultStyle: {
      font: 'IPAGothic',
      alignment: 'center'
    }
  }
  createPdfBinary(docDefinition, (binary) => {
    res.setHeader('Content-Type', 'application/json')
    res.status(200).send(binary)
  })
}
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ],
    },
    "target": "es6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
next.config.js
const { resolve } = require('path')
const withOffline = require('next-offline')

const nextConfig = {
  webpack: (config) => {
    // src ディレクトリをエイリアスのルートに設定
    config.resolve.alias['~'] = resolve(__dirname, 'src')
    return config
  },
  // manifest設定
  target: 'serverless',
  transformManifest: (manifest) => ['/'].concat(manifest),
  generateInDevMode: true,
  workboxOpts: {
    swDest: 'static/service-worker.js',
    runtimeCaching: [
      {
        urlPattern: /^https?.*/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'https-calls',
          networkTimeoutSeconds: 15,
          expiration: {
            maxEntries: 150,
            maxAgeSeconds: 30 * 24 * 60 * 60 // 1 month
          },
          cacheableResponse: {
            statuses: [0, 200]
          }
        }
      }
    ]
  }
}

// PWA に対応
module.exports = withOffline(nextConfig)
.babelrc.js
// ローカルの開発サーバー側の SSR 時と クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれるエラーの対応
module.exports = {
  presets: ['next/babel'],
  plugins: [
    ['styled-components', { ssr: true, displayName: true, preprocess: false }]
  ]
}

参考

including-additional-files
pdfmake
IPAex フォント ダウンロードページ
IPAex フォント ダウンロードページ
Zeit Now の具体的な Tips 集
Javascript で PDF を作成する

1
1
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
1