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