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

# 課題定義
- 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()`関数を使います。

# 簡単なコードを作りましょう
こちらです

```ts

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ページ追加必要です。

```ts
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など）。まずに以下のオプションをためてください

```ts
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にすれば解決できると思います。

こんな感じ

```ts
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設定できると思いますが、この記事には書いていません。