VisualforceページのPDF化〜Contentへ保存

えらいひと「スマホから見たVisualforceページを**見た目そのままスマホからPDF化**したい」
ぼく「なんでそういうこと言うの？」

※昔に書いたものなのでふんわりしています

## そもそもできるのか？
VisualforceのPDF化は`renderAs="pdf"`でてきますが、制限が多く見た目そのままとはならなかったため、別の手段を探すことに……。
いろいろ調べてみると↓のようにやればできるとかなんとか。

1. html2canvasにCanvasに表示したWebページを画像化
1. jsPDFでPDF化

## とりあえずやってみる
#### Visualforce
```html
<apex:page showHeader="false" sidebar="false" applyBodyTag="false">
    <head>
        <!-- viewport -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"/>
        <!-- inclide -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
        <title>このページをPDF化するよ</title>
    </head>

    <body>
        <nav>
            <button type="button" id="btn" onclick="convertPdf();">PDF作成！</button>
        </nav>
        <section>
            <div id="page_1">
                <p>吾輩は猫である。</p>
            </div>
        </section>
        <section>
            <div id="page_2">
                <p>おはようございます</p>
                <p>こんにちは</p>
                <p>こんばんは</p>
                <p>おやすみなさい</p>
            </div>
        </section>
    </body>
</apex:page>
```
![スクリーンショット 2022-12-21 14.33.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3075648/92e691ba-8b9a-ba04-8d3d-e9c171e6c55d.png)
PDF化したいVisualforceはこんな感じで作りました。

#### JavaScript
```javascript
/* デバイスピクセル比 */
var device_pixel = 2.5;
/* キャプチャ情報を入れるリスト */
var captureImages;
/* html2canvas options */
var canvasOptions = {scale: device_pixel};

/* keys */
const KEY_URL = 'url';
const KEY_WIDTH = 'width';
const KEY_HEIGHT = 'height';
const FILE_NAME = 'ファイル.pdf';

/**
 * キャプチャ処理
 * @param element DOM要素
 */
var capture = async (element) => {
  await html2canvas(element, canvasOptions).then(canvas => {
      let captureImage = new Map();
      captureImage.set(KEY_URL, canvas.toDataURL('image/jpeg'));
      captureImage.set(KEY_WIDTH, canvas.width / device_pixel);
      captureImage.set(KEY_HEIGHT, canvas.height / device_pixel);
      captureImages.push(captureImage);
  });
}

/**
 * PDF作成
 * @param format jsPDF format
 */
function createPdf(format) {

  const { jsPDF } = window.jspdf;
  let pdf = new jsPDF({orientation: 'p', unit: 'px', format: format});

  /* キャプチャ数だけループ */
  for (let i = 0; i < captureImages.length; i++) {
    /* 1ページ目以降ページ追加 */
    if (i != 0) pdf.addPage(format);
    
    let c = captureImages[i];
    pdf.addImage(c.get(KEY_URL), 'JPEG', 0, 0, c.get(KEY_WIDTH), c.get(KEY_HEIGHT));
  }
  pdf.setProperties({title: FILE_NAME});
  return pdf;
}

/**
 * PDF作成
 */
function convertPdf() {

  /* リスト作成 */
  captureImages = new Array();
  /* windowサイズを800pxで計算する */
  canvasOptions['windowWidth'] = 800;

  (async () => {
    /* ページ分キャプチャ */
    let width = height = 0;
    let pages = document.querySelectorAll("[id^='page']");
    for (let i = 0; i < pages.length; i++) {
      await capture(pages[i]);

      /* 縦幅を一番大きいものに合わせる */
      let c = captureImages[i];
      height = (height < c.get(KEY_HEIGHT)) ? c.get(KEY_HEIGHT) : height;
    }
    width = captureImages[0].get(KEY_WIDTH);

    /* PDF format */
    const PDF_FORMAT = [width, height];
    let pdf = createPdf(PDF_FORMAT);
    window.open(pdf.output('bloburl',{filename: FILE_NAME}),'_blank');
  })();
}
```

`captureImage`の`url`にキャプチャした画像が入ります。画像サイズは`canvas`のサイズはデバイスピクセル比で割って計算します。
デバイスピクセル比は解像度とPDF化したときのファイルサイズの兼ね合いから2と3の間をとって2.5としました。

```javascript
window.open(pdf.output('bloburl',{filename: FILE_NAME}),'_blank');
```

「PDF作成！」ボタンを押すとWebページをキャプチャして作成したPDFファイルが開く仕組み。

## ぼく「できました！！」

えらいひと「PDF化したのを自動でファイルに保存して欲しい」
ぼく「なんでそういうこと言うの？」

## Contentへ保存できるように修正
えらいひとに言われたのでPDF化したファイルを開く処理からPDF化したファイルをContentにブチ込む処理へ修正します。泣いてはいけません。

#### Visualforceを修正
```html
<apex:page showHeader="false" sidebar="false" applyBodyTag="false" controller="PdfController">
    <!-- 〜〜略〜〜 -->
    <body>
        <nav>
            <button type="button" id="btn" onclick="convertPdf('{! recordId}');">PDF作成！</button>
        </nav>
        <!-- 〜〜略〜〜 -->
    </body>
</apex:page>
```
コントローラーを追加して`convertPdf`にレコードIdを渡します。

#### JavaScriptを修正
```javascript
/* RemortAction */
var remoteAction = function(recordId, blob) {
  PdfController.insertPdf(recordId, blob, function(result, event) {
      if (event.status) console.log("success!");
  });
};

/**
 * PDF作成
 */
function convertPdf(recordId) {

  /* リスト作成 */
  captureImages = new Array();
  /* windowサイズを800pxで計算する */
  canvasOptions['windowWidth'] = 800;

  (async () => {
    /* ページ分キャプチャ */
    let width = height = 0;
    let pages = document.querySelectorAll("[id^='page']");
    for (let i = 0; i < pages.length; i++) {
      await capture(pages[i]);

      /* 縦幅を一番大きいものに合わせる */
      let c = captureImages[i];
      height = (height < c.get(KEY_HEIGHT)) ? c.get(KEY_HEIGHT) : height;
    }
    width = captureImages[0].get(KEY_WIDTH);

    /* PDF format(余白を100px持たせる) */
    const PDF_FORMAT = [width, height + 100];
    let pdf = createPdf(PDF_FORMAT);
    //window.open(pdf.output('bloburl',{filename: FILE_NAME}),'_blank');

    /* RemortAction */
    let blob = pdf.output('datauristring',{filename: FILE_NAME});
    remoteAction(recordId, blob);
  })();
}
```
RemortActionを呼び出す処理を追加します。

#### Apexコントローラーを作成
```Apex
public class PdfController {
    /* 〜〜略〜〜 */
    
    @RemoteAction
    public static String insertPdf(Id recordId, String b) {
        
        ContentVersion cv = new ContentVersion();
        cv.ContentLocation = 'S';
        cv.PathOnClient = 'file.pdf';
        cv.Title = 'ファイル';
        // data:application/pdf;base64,バイナリ→base64エンコード済みの文字列　の形でくる
        // base64でデコードしてからいれる
        cv.VersionData = EncodingUtil.base64Decode(b.substringAfter(','));
        insert cv;

        // レコードへの関連
        ContentDocumentLink cdl = new ContentDocumentLink();
        cdl.ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId;
        cdl.LinkedEntityId = recordId;
        cdl.ShareType = 'V';
        insert cdl;
    }
}
```

RemoteActionでApexを呼び出し、Contentを作成します。

## えらいひと「あっ、やっぱりいいや。これなしで」

ぼく「なんでそういうこと言うの？？？？？」