kintone＋html2canvas＋jsPDFでPDF出力時にレイアウトが崩れる（内容によって高さが変動）問題について

質問本文
kintoneのレコード詳細画面に「PDF出力」ボタンを追加し、
html2canvas＋jsPDFを使ってレコード内容をPDF化するカスタマイズを作っています。

■実現したいこと
A4縦サイズのPDFに、受注連絡票のような表を出力したい

上左右に10mm、下に0mmの余白で整ったPDFを出したい

内容の長さに関係なく、レイアウトが毎回同じになるようにしたい

■現在の状況
表はHTMLテンプレートで生成し、html2canvas() → jsPDF().addImage()でPDF化

表の一部（たとえば工事概要や備考など）がレコードによって内容量が違う

その結果、HTML全体の高さが可変となり、PDF内でのレイアウトがずれてしまう（場合によっては1ページに収まらなかったり、文字が潰れる）

■試したこと
html2canvasのscale指定で高解像度化 → 効果は限定的

スタイルでmin-heightやoverflow: hiddenなど指定 → テンプレートの高さは一定化できず

jsPDFのサイズ指定や倍率調整 → 内容によってPDFの縮尺が毎回変動してしまう

■聞きたいこと
html2canvas＋jsPDFで、内容量によらず 常に 同じレイアウト・同じ倍率・同じ見た目のPDF を出力するにはどうすればよいか？

html2canvasでHTMLの高さを強制的に固定する（たとえばA4 297mmに相当するピクセル数に）ことは可能か？

または、他にこの目的に適した方法・ライブラリがあるか？

■補足
使用環境：kintoneカスタマイズ（JavaScript）

外部ライブラリ：html2canvas 1.4.1、jsPDF 2.5.1

表示用HTMLのテンプレートと出力コードは既にできています（希望があれば一部コードも掲載します）

■望むゴール
どんなデータでも、整ったA4 PDFが一発で出力されること（＝見た目が安定すること）

■現在のコード

```
(function () {
  'use strict';

  // レコード詳細画面が表示されたときに実行
  kintone.events.on('app.record.detail.show', function (event) {
    console.log('レコード詳細画面表示イベント発火', event);

    // レコードデータを取得
    const record = event.record;

    // 右上レコードメニューを取得
    const recordMenu = document.querySelector('.gaia-argoui-app-toolbar-menu') ||
                       document.querySelector('.gaia-argoui-app-record-menu') ||
                       document.querySelector('.record-gaia .gaia-argoui-toolbar-menu');
    const header = kintone.app.record.getHeaderMenuSpaceElement();

    // ボタンの共通スタイル
    const buttonStyle = {
      backgroundColor: '#4CAF50',
      color: 'white',
      padding: '8px 16px',
      border: 'none',
      borderRadius: '4px',
      cursor: 'pointer',
      fontSize: '14px',
      fontWeight: '500',
      display: 'inline-flex',
      alignItems: 'center',
      gap: '6px',
      boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
      transition: 'background-color 0.2s, transform 0.1s',
      marginRight: '8px',
      marginTop: '4px'
    };

    // ホバー時のスタイル関数
    const applyHoverStyles = (button) => {
      button.onmouseover = () => {
        button.style.backgroundColor = '#45a049';
        button.style.transform = 'scale(1.05)';
      };
      button.onmouseout = () => {
        button.style.backgroundColor = '#4CAF50';
        button.style.transform = 'scale(1)';
      };
    };

    // HTMLテンプレート生成関数
    const generateHtmlContent = () => {
      const customerName = record['tokuisakirup']?.value || record['tokuisakinew']?.value || '━━━━━━━━━━━━━━━━';
      const siteName = record['genbarup']?.value || record['genbanew']?.value || '━━━━━━━━━━━━━━━━';
      const siteAddress = record['jyusyorup']?.value || record['jyusyonew']?.value || '━━━━━━━━━━━━━━━━';
      const contractDate = record['keiyakubi']?.value || '━━━━━━━━';
      const constructionStart = record['kouki1']?.value || '━━━━━━━━';
      const constructionEnd = record['kouki2']?.value || '━━━━━━━━';
      const amountExcludingTax = record['zeinuki']?.value || '━━━━━━━━';
      const amountIncludingTax = record['zeikomi']?.value || '━━━━━━━━';
      const orderType = record['jyuri']?.value || '━━━━━━━━━━━━━━━━';
      const paymentTerms = record['jyouken']?.value || '━━━━━━━━━━━━━━━━';
      const constructionOverview = record['koujigaiyou']?.value || '━━━━━━━━━━━━━━━━';
      const remarks = record['bikou']?.value || '━━━━━━━━━━━━━━━━';
      const siteAgent = record['dairinin']?.value || '━━━━━━━━';
      const chiefEngineer = record['syunin']?.value || '━━━━━━━━';
      const supervisingEngineer = record['kanri']?.value || '━━━━━━━━';
      const salesRepresentative = record['eigyotanto']?.value || '━━━━━━━━';

      // デバッグ用ログ
      console.log('取得した値:', {
        customerName,
        siteName,
        siteAddress,
        contractDate,
        constructionStart,
        constructionEnd,
        amountExcludingTax,
        amountIncludingTax,
        orderType,
        paymentTerms,
        constructionOverview,
        remarks,
        siteAgent,
        chiefEngineer,
        supervisingEngineer,
        salesRepresentative
      });

      return `
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>受注連絡票</title>
  <style>
    body {
      font-family: 'Arial', sans-serif;
      padding: 15mm 15mm 0 15mm; /* 上左右10mm、下0mm */
      line-height: 1.6;
      box-sizing: border-box;
      width: 190mm; /* A4幅(210mm) - 左右余白(20mm) */
      margin: 0 auto;
    }
    h1 {
      text-align: center;
      font-size: 24px;
      margin-bottom: 20px;
      border-bottom: 2px solid #000;
      padding-bottom: 10px;
    }
    .date {
      text-align: right;
      margin-bottom: 10px;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 15px;
    }
    td, th {
      border: 1px solid #000;
      padding: 8px;
      font-size: 14px;
    }
    .section-title {
      background-color: #f0f0f0;
      font-weight: bold;
      text-align: center;
    }
    .no-border {
      border: none;
    }
    .approval-box {
      display: flex;
      justify-content: space-between;
      margin-top: 30px;
    }
    .approval-box div {
      width: 24%;
      text-align: center;
      border: 1px solid #000;
      padding: 20px;
    }
  </style>
</head>
<body>
  <div class="date">日付：${new Date().toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
  <h1>受　注　連　絡　票</h1>
  <table>
    <tr>
      <th>顧 客 名</th>
      <td colspan="3">${customerName}</td>
    </tr>
    <tr>
      <th>現 場 名</th>
      <td colspan="3">${siteName}</td>
    </tr>
    <tr>
      <th>現場住所</th>
      <td colspan="3">${siteAddress}</td>
    </tr>
    <tr>
      <th>契 約 日</th>
      <td>${contractDate}</td>
      <th>工　期</th>
      <td>${constructionStart} ～ ${constructionEnd}</td>
    </tr>
    <tr>
      <th>請負金額</th>
      <td colspan="3">¥${amountExcludingTax} (税抜)　¥${amountIncludingTax} (税込)</td>
    </tr>
    <tr>
      <th>受注形態</th>
      <td colspan="3">${orderType}</td>
    </tr>
    <tr>
      <th>支払条件</th>
      <td colspan="3">${paymentTerms}</td>
    </tr>
    <tr>
      <th>工事概要</th>
      <td colspan="3" style="height: 100px;">${constructionOverview}</td>
    </tr>
    <tr>
      <th>備考</th>
      <td colspan="3" style="height: 100px;">${remarks}</td>
    </tr>
    <tr>
      <th>現場代理人</th>
      <td>${siteAgent}</td>
      <th>主任技術者</th>
      <td>${chiefEngineer}</td>
    </tr>
    <tr>
      <th>監理技術者</th>
      <td>${supervisingEngineer}</td>
      <th>営業担当者</th>
      <td>${salesRepresentative}</td>
    </tr>
  </table>
</body>
</html>
      `;
    };

    // レイアウト確認ボタンを作成
    const previewButton = document.createElement('button');
    previewButton.id = 'previewButton';
    previewButton.textContent = 'レイアウト確認';
    Object.assign(previewButton.style, buttonStyle);
    applyHoverStyles(previewButton);

    // レイアウト確認ボタンのクリック処理
    previewButton.onclick = function () {
      console.log('レイアウト確認ボタンがクリックされました', record);
      const htmlContent = generateHtmlContent();
      const popup = window.open('', '受注連絡票プレビュー', 'width=800,height=600,scrollbars=yes,resizable=yes');
      if (!popup) {
        alert('ポップアップがブロックされました。ブラウザの設定でポップアップを許可してください。');
        console.error('ポップアップウィンドウが開けませんでした');
        return;
      }
      popup.document.write(htmlContent);
      popup.document.close();
      console.log('ポップアップウィンドウにHTMLを表示しました');
    };

    // PDF出力ボタンを作成
    const pdfButton = document.createElement('button');
    pdfButton.id = 'pdfButton';
    pdfButton.textContent = 'PDF出力';
    Object.assign(pdfButton.style, buttonStyle);
    applyHoverStyles(pdfButton);

    // PDF出力ボタンのクリック処理
    pdfButton.onclick = async function () {
      console.log('PDF出力ボタンがクリックされました', record);
      try {
        // HTMLを一時的な要素にレンダリング
        const htmlContent = generateHtmlContent();
        const tempDiv = document.createElement('div');
        tempDiv.style.position = 'absolute';
        tempDiv.style.left = '-9999px';
        tempDiv.innerHTML = htmlContent;
        document.body.appendChild(tempDiv);

        // html2canvasでキャンバスに変換
        const canvas = await html2canvas(tempDiv, {
          scale: 2, // 高解像度でレンダリング
          useCORS: true,
          logging: true
        });

        // デバッグ用にキャンバスサイズをログ
        console.log('キャンバスサイズ:', { width: canvas.width, height: canvas.height });

        // jsPDFでPDF生成
        const { jsPDF } = window.jspdf;
        const pdf = new jsPDF({
          orientation: 'portrait',
          unit: 'mm',
          format: 'a4'
        });

        const imgData = canvas.toDataURL('image/png');
        const imgProps = pdf.getImageProperties(imgData);
        const pdfWidth = pdf.internal.pageSize.getWidth();
        const pdfHeight = pdf.internal.pageSize.getHeight();
        const marginTop = 15; // 上10mm
        const marginBottom = 0; // 下0mm
        const marginHorizontal = 15; // 左右10mm
        const contentWidth = pdfWidth - 2 * marginHorizontal;
        const contentHeight = (imgProps.height * contentWidth) / imgProps.width;

        // デバッグ用に余白とコンテンツサイズをログ
        console.log('PDF設定:', {
          pdfWidth,
          pdfHeight,
          marginTop,
          marginBottom,
          marginHorizontal,
          contentWidth,
          contentHeight,
          totalHeight: contentHeight + marginTop + marginBottom
        });

        // コンテンツがページ高さを超える場合、縮小して1ページに収める
        if (contentHeight + marginTop + marginBottom > pdfHeight) {
          const scaleFactor = (pdfHeight - marginTop - marginBottom) / contentHeight;
          console.log('スケール適用:', { scaleFactor });
          pdf.addImage(imgData, 'PNG', marginHorizontal, marginTop, contentWidth * scaleFactor, contentHeight * scaleFactor);
        } else {
          pdf.addImage(imgData, 'PNG', marginHorizontal, marginTop, contentWidth, contentHeight);
        }

        // PDFをダウンロード
        pdf.save('受注連絡票.pdf');

        // 一時要素を削除
        document.body.removeChild(tempDiv);
        console.log('PDF生成およびダウンロードが完了しました');
      } catch (error) {
        console.error('PDF生成エラー:', error);
        alert('PDF生成中にエラーが発生しました: ' + error.message);
      }
    };

    // ボタンを右上レコードメニューに追加
    try {
      if (recordMenu) {
        console.log('右上メニューが見つかりました:', recordMenu);
        // 既存のボタンを削除
        const existingPdfButton = document.getElementById('pdfButton');
        const existingPreviewButton = document.getElementById('previewButton');
        if (existingPdfButton) existingPdfButton.remove();
        if (existingPreviewButton) existingPreviewButton.remove();
        // PDF出力ボタンを先に追加（レイアウト確認の左に）
        recordMenu.insertBefore(pdfButton, recordMenu.firstChild);
        // レイアウト確認ボタンを追加
        recordMenu.insertBefore(previewButton, recordMenu.firstChild);
      } else {
        console.warn('右上メニューが見つかりませんでした。ヘッダーにフォールバックします。');
        if (header) {
          const existingPdfButton = document.getElementById('pdfButton');
          const existingPreviewButton = document.getElementById('previewButton');
          if (existingPdfButton) existingPdfButton.remove();
          if (existingPreviewButton) existingPreviewButton.remove();
          header.appendChild(pdfButton);
          header.appendChild(previewButton);
        } else {
          console.error('ヘッダーメニュースペースも見つかりませんでした');
          alert('エラー: ボタンを配置できません。管理者に連絡してください。');
        }
      }
    } catch (error) {
      console.error('ボタン配置エラー:', error);
      if (header) {
        const existingPdfButton = document.getElementById('pdfButton');
        const existingPreviewButton = document.getElementById('previewButton');
        if (existingPdfButton) existingPdfButton.remove();
        if (existingPreviewButton) existingPreviewButton.remove();
        header.appendChild(pdfButton);
        header.appendChild(previewButton);
      } else {
        console.error('フォールバック失敗:', error);
        alert('エラー: ボタンを配置できません。管理者に連絡してください。');
      }
    }

    return event;
  });
})();
```