10
6

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 3 years have passed since last update.

HTMLからPDF出力機能を作りました

Last updated at Posted at 2020-08-07

課題定義

  • HTMLページをPDFで出力する機能を作りたい(ページの一部出力可能)
  • HTMLページに画像があり
  • フロントエンドでやること

調査の結果

PDF出力機能を開発する時にいくつか問題が発生しました。解決するため、いくつか記事を参考しまた。
やっとPDF出力機能が出来ましたので、共有します。

ライブラリーを選ぶ

まずにPDFのライブラリーです。
https://github.com/MrRio/jsPDF
このライブラリーの目的はPDF作ることです。

PDFライブラリーがありましたが、このライブラリーでPDFに直接HTMLエレメントが追加出来ません。
→ 中間段階が必要になります。

こちらです:
HTMLエレメント → HTMLCanvasにする → 画像にする → PDFファイルに追加

HTMLエレメントからHTMLCanvasにする時、html2canvasライブラリーを使います。
https://github.com/niklasvh/html2canvas

HTMLCanvasから画像にする時、canvas.toDataURL()関数を使います。

簡単なコードを作りましょう

こちらです


function htmlElementToPDF(element: HTMLElement) {
  html2canvas(element, {}).then((canvas: HTMLCanvasElement) => {
    const imgData = canvas.toDataURL('image/jpeg', 1.0);
    const canvasImageWidth = canvas.width;
    const canvasImageHeight = canvas.height;

    const pdf = new jsPDF('p', 'pt',  [canvasImageWidth, canvasImageHeight]);

    pdf.addImage(imgData, 'JPG', 0, 0, canvasImageWidth, canvasImageHeight);

    pdf.save('sample.pdf');
  });
}

解読:

  • html2canvasはelementからcanvasにする
  • canvas.toDataURL('image/jpeg', 1.0)はcanvasから画像作る
  • pdf.addImage(imgData, 'JPG', 0, 0, canvasImageWidth, canvasImageHeight)は画像をPDFに追加する

簡単なコードなのでいくつか問題があります

  • elementの高さは長くてもPDFは1ページになりますので、見た目が正しくない
  • canvas.toDataURLの時にCORSエラー発生するかもしれません(あとで説明する)

ちょっと複雑なコードでPDFページングします

  • Elementの高さは長いすぎる時に、PDFの一つページが足りなく、PDFページ追加必要です。
function htmlElementToPDF(element: HTMLElement) {
  html2canvas(element, {}).then((canvas: HTMLCanvasElement) => {
    const imgData = canvas.toDataURL('image/jpeg', 1.0);
    
    const pdfWidth = canvas.width;
    const pdfHeight = canvas.width * 1.5;

    const canvasImageWidth = canvas.width;
    const canvasImageHeight = canvas.height;

    const pdf = new jsPDF('p', 'pt',  [pdfWidth, pdfHeight]);

    // PDFに追加した分をここに入れます
    let filledImageHeight = 0;
  
    while (true) {
      // PDFに画像を追加する時に、追加した分を外したいので、-filledImageHeightにする
      pdf.addImage(imgData, 'JPG', -filledImageHeight, 0, canvasImageWidth, canvasImageHeight);
      filledImageHeight += canvasImageHeight;

      // 全部画像追加された場合、PDF出力完了です。
      if (filledImageHeight >= canvasImageHeight) {
        break;
      }

      // PDFページが足りないため、一つページを追加する
      pdf.addPage([pdfWidth, pdfHeight]);
    }

    pdf.save('sample.pdf');
  });
}

解読

  • コードに付けました

良いこと

  • elementの高さは長くてもPDFページがどんどん追加されます。

問題

canvas.toDataURLの時にCORSエラー発生するかもしれません

原因は html2canvasでエレメントにある画像をダウンロードしますが、画像のドメインは今のドメインじゃありせん(s3やgoogledriveなど)。まずに以下のオプションをためてください

html2canvas(element, {allowTaint : false, useCORS: true})

それで出来たらいいですが、出来ない場合が結構あります。(画像はs3リンックなら、出来ません。https://stackoverflow.com/questions/51317126/aws-s3-with-html2canvas-cors-issue-with-multiple-browsers/51354027

出来なければ、自分のサーバーで画像をダウンロードして、フロントエンに渡すれて解決出来ます。(直接にS3リンク使わず、バックエンドのAPIを用意すること)

Elementにある画像がたくさんがある時に、Canvasエレメントが重いすぎて、PDFに追加する時に、最後に一部出力出来ないこと (これはライブラリーのせいと思います)

これもよく発生する問題です。解決できる為に、エレメントを分けて、Canvasにすれば解決できると思います。

こんな感じ

async function htmlElementToPDF(element: HTMLElement) {

  // canvasを取る
  const canvasElements: HTMLCanvasElement[] = [];
  for (let index = 0; index < element.children.length; index++) {
    const item = element.children[index] as HTMLElement;
    canvasElements.push(await html2canvas(item, {}));
  }

  // PDF Create
  let pdfWidth = 0;
  canvasElements.forEach(canvas => {
    if (canvas.width > pdfWidth) {
      pdfWidth = canvas.width;
    }
  });
  const pdfHeight = (pdfWidth * 1.5);
  const pdf = new jsPDF('p', 'pt',  [pdfWidth, pdfHeight]);

  // Add canvas into pdf
  // PDFページにどこまでにデーターを入れたのか、この変数に値を入れます。
  let currentHeightIndex = 0;
  canvasElements.forEach((canvas) => {
    const imgData = canvas.toDataURL('image/jpeg', 1.0);
    const canvasImageWidth = canvas.width;
    const canvasImageHeight = canvas.height;

    // PDFに追加されていない高さの変数です 
    let unFilledImageHeight = canvas.height;

    while (true) {
      if (currentHeightIndex > 0) {
        // currentHeightIndex > 0 の場合、画像を最初に追加されますので、currentHeightIndexから追加する
        pdf.addImage(imgData, 'JPG', 0, currentHeightIndex, canvasImageWidth, canvasImageHeight);
      } else {
        // currentHeightIndex = 0 の場合、画像の追加の途中かもしれませんので、追加された部分を外す
        const filledImageHeight = canvas.height - unFilledImageHeight;
        pdf.addImage(imgData, 'JPG', 0, -filledImageHeight, canvasImageWidth, canvasImageHeight);
      }

      if ((pdfHeight - currentHeightIndex) > unFilledImageHeight) {
        // 全部の画像は追加されたのでBreakして、PDFの残り部分を覚える
        currentHeightIndex += unFilledImageHeight;
              
        break;
      } else {
        // PDFに追加された高さを更新します
        unFilledImageHeight -= (pdfHeight - currentHeightIndex);
              
        // PDFページがたりないので、次のPDFページにします。
        pdf.addPage([pdfWidth, pdfHeight]);

        // 新しいページですので、一番上にデータ追加します。
        currentHeightIndex = 0;
      }
    }
  });

  pdf.save('sample.pdf');

}

その他

  • コードをちょっと修正すると、PDDのMargin設定できると思いますが、この記事には書いていません。
10
6
1

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
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?