PDFをポップアップで表示する方法:ブラウザでの一括ダウンロードを防ぐ実践テクニック

ポップアップでPDFを表示しておきながら、ユーザーがブラウザから一括ダウンロードを開始しないように制御するには、見た目と機能を分離し、サーバーサイドとクライアントサイドの両方で対策を講じる必要があります。
この記事では、実際に使える技術スタックとコード例を交えて、以下の3つの観点から解説します。

  1. PDFをポップアップ(モーダル)で表示させる実装
  2. 一括ダウンロードを防ぐサーバーサイド設定
  3. クライアント側でのダウンロード抑制とユーザー体験の向上

1. PDFをポップアップ(モーダル)で表示させる実装

1-1. 直接ブラウザのビューワーを利用する場合

HTML5 の <embed><object> 要素を使うと、PDF をページ内に埋め込み、ユーザーがインタラクションしやすくなります。
ただし、ほとんどのブラウザは「open in new tab」や「download」コマンドを無効にできません。
そこで、モーダル(ポップアップ) を使って PDF を表示することで、ブラウザの「タブに移動」機能を抑制します。

<!-- Bootstrap 5 を例に -->
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#pdfModal">PDFを読む</button>

<div class="modal fade" id="pdfModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-xl modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">PDFドキュメント</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="閉じる"></button>
      </div>
      <div class="modal-body" style="height:80vh; overflow:hidden;">
        <embed src="/pdfs/sample.pdf" type="application/pdf" width="100%" height="100%" />
      </div>
    </div>
  </div>
</div>
  • カスタム CSS を使ってモーダルの高さやスクロール挙動を調整することで、モバイルでも快適に閲覧できます。
  • embed 要素はブラウザが PDF を埋め込めない場合にはリンクにフォールバックしますが、モーダル内にリンクを表示させることでダウンロードを促さない設計にします。

1-2. JavaScript ベースの PDF ビューワーを利用する場合

Google Chrome など一部のブラウザは PDF をインラインで表示できますが、他のブラウザでは外部アプリが起動する可能性があります。
そこで、オープンソースの PDF.js を利用すると、ブラウザ依存なく一貫した表示が可能です。

# 例: npm で pdfjs-dist を取得
npm install pdfjs-dist
<!-- PDF.js 用ライブラリを読み込む -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>

<div id="pdfViewer" style="height:80vh;"></div>
<button id="openPdfBtn" class="btn btn-primary">PDFを読む</button>

<script>
  const url = '/pdfs/sample.pdf'; // PDFのパス
  const openBtn = document.getElementById('openPdfBtn');
  const viewerContainer = document.getElementById('pdfViewer');

  function renderPDF() {
    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise.then(pdf => {
      // ページは1ページだけ表示する例
      pdf.getPage(1).then(page => {
        const viewport = page.getViewport({ scale: 1.5 });
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        canvas.height = viewport.height;
        canvas.width = viewport.width;
        viewerContainer.innerHTML = ''; // 以前の描画をクリア
        viewerContainer.append(canvas);
        const renderContext = {
          canvasContext: context,
          viewport: viewport
        };
        page.render(renderContext);
      });
    });
  }

  openBtn.addEventListener('click', () => {
    // モーダルではなく、ダイアログ風に表示(もしくは既存の modal を開く)
    // ここでは簡易的にブラウザのダイアログを使用
    const dlg = document.createElement('dialog');
    dlg.setAttribute('open', ''); // open 属性で表示
    dlg.style.width = '90%';
    dlg.style.height = '90%';
    dlg.append(viewerContainer);
    document.body.append(dlg);
    renderPDF();

    dlg.addEventListener('click', () => dlg.close());
  });
</script>
  • PDF.js は PDF の内容をキャンバス上にレンダリングします。
  • モーダルの中に直接描画された Canvas は、download ボタンがないため、ユーザーが簡単にダウンロードする手段が無くなります。
  • 必要に応じて、pdfjsLib.getDocument のオプションで CDN から取得した CMap を設定し、文字化けを防止します。

1-3. 外部サービスを利用する方法(Google Drive Viewer, Microsoft Office Online など)

外部サービスを利用すると、サーバー側で PDF を処理する手間が省けます。ただし、ダウンロードの可否は外部側の設定に依存するため、ダウンロード制御は難しくなります。

<iframe
  src="https://drive.google.com/file/d/FILE_ID/preview"
  width="640"
  height="480"
  allow="autoplay"></iframe>
  • Google Drive の preview では、下部に「ダウンロード」ボタンが非表示になっています。
  • ただし、URL パラメータや Google の設定次第で download が有効になるケースがあるため、完全に防げるわけではありません。

2. 一括ダウンロードを防ぐサーバーサイド設定

ファイルを「inline」で返却し、Content-Disposition: inline ヘッダーで埋め込みを推奨します。
さらに、Cache-ControlVary ヘッダーで適切なキャッシュ制御を行うと、ユーザーがブラウザで「全てダウンロード」を実行したとき、キャッシュから取得される確率が下がります。

2-1. Apache / Nginx の設定例

Apache (.htaccess)

<FilesMatch "\.(pdf)$">
  # 1. inline 表示
  Header set Content-Disposition "inline"

  # 2. キャッシュ制御 (1日)
  Header set Cache-Control "public, max-age=86400"

  # 3. CDN・プロキシがキャッシュを尊重しないように
  Header set Vary "Accept-Encoding"
</FilesMatch>

Nginx

location ~* \.pdf$ {
  add_header Content-Disposition "inline";
  add_header Cache-Control "public, max-age=86400";
  add_header Vary "Accept-Encoding";
}

2-2. サーバーサイド言語で動的に PDF を返す場合

<?php
// file.php?id=123 でPDFを取得
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="report.pdf"');
header('Cache-Control: public, max-age=86400');
header('Vary: Accept-Encoding');

// 認証処理(例: 認証済みかチェック)
// ファイルを読み込み
readfile('/path/to/report.pdf');
?>
  • Content-Disposition: inline を設定すると、ブラウザは「開く/表示」モードを優先します。
  • filename を指定しなくても構いませんが、既存のファイル名を挿入するとユーザーがファイル名を把握しやすくなります。

2-3. Token ベースでアクセス制御

大規模サイトでは、URL に一意のトークンを付与し、一定時間で失効させる方式が有効です。
トークンを有効にするときのみ PDF を返却し、無効化したら再発行を要求します。

# Flask で例示
from flask import Flask, send_file, abort, request
import secrets
import os

app = Flask(__name__)
TOKEN_DB = {}  # 実際は DB へ

@app.route('/download')
def download():
    token = request.args.get('token')
    if not token or token not in TOKEN_DB:
        abort(403)
    # 有効期限チェック
    if TOKEN_DB[token] < datetime.utcnow():
        abort(403)
    file_path = '/path/to/report.pdf'
    return send_file(file_path, as_attachment=False)
  • as_attachment=False にすることで、クライアント側は「ダウンロード」ではなく、同じブラウザで表示しようとします。
  • 一括ダウンロードは、トークンを複数発行したくない場合に効果的です。

3. クライアント側でのダウンロード抑制とユーザー体験の向上

3-1. ファイルのダウンロードリンクを隠す

ページ内に PDF を表示している段階で、ユーザーに「ダウンロード」リンクを提示しない設計にします。
しかし、万が一リンクがある場合でも、クリックした瞬間にモーダルに遷移させ、実際の PDF ファイルは非表示にしておくとユーザーがダウンロードできません。

<a href="/pdfs/report.pdf" class="download-link" style="display:none;"></a>

3-2. ブラウザ固有の右クリックメニューを無効化

JavaScript で contextmenu イベントを抑制することで、右クリックの「保存」オプションを消します。
ただし、完全にブラウザの機能を止めることはできませんが、ユーザーにとっては手間が増えるだけです。

document.addEventListener('contextmenu', function(e) {
  // クリック位置が PDF の表示領域内か確認
  const viewer = document.getElementById('pdfViewer');
  if (viewer.contains(e.target)) {
    e.preventDefault();  // 右クリックメニューを無効化
  }
});

3-3. ブラウザのタブ制御を意図的に抑える

モーダル内に PDF を表示している場合、ユーザーは別タブへ移動したくても、表示領域が固定です。
さらに、タブに移動した際にフォーカスを失わせることで、UI を一層抑制します。

// タブ選択と同時にポップアップを閉じる
document.addEventListener('visibilitychange', function() {
  if (document.hidden) {
    const modal = document.getElementById('pdfModal');
    if (modal) $(modal).modal('hide');
  }
});

3-4. モバイルデバイスでの対応

モバイルブラウザは「共有」や「書き込み」オプションが簡単にアクセスできるため、ダウンロード抑制は難しいです。
そこで、モバイル専用に iframe ではなく、WebAssembly を活用した PDF.js を直接描画し、「スクロールのみ」 のインターフェイスにします。

# wasm パッケージをビルド
wasm-pack build --target web
<div id="pdfContainer" style="width:100%;height:100vh;overflow-y:auto;"></div>

<script type="module">
import init, { render_pdf } from './pkg/pdf_renderer.js'; // Rust/WASM 版

await init();

const container = document.getElementById('pdfContainer');
render_pdf('/pdfs/mobile.pdf', container); // JavaScript API で描画
</script>
  • これは 1 ページずつ描画しているだけでなく、ファイルサイズを削減できるWASM を利用しているため、レスポンスが速い。
  • モバイルでのタップ時にダウンロードは発生しません。

まとめ

  1. PDF をモーダル(ポップアップ)で埋め込みし、ユーザーの視界に入れた上で inline 表示します。
  2. サーバーサイドで Content-Disposition: inline とキャッシュポリシー を設定し、ブラウザにダウンロードを促させない。
  3. Token ベースのアクセス制御 でダウンロードを一律管理したり、JavaScript で右クリックタブ切替 を弱めます。
  4. PDF.jsWASM を活用すれば、ブラウザの再実装やモバイル固有の問題も軽減できます。

これらを組み合わせることで、ブラウザでの一括ダウンロードを防ぎつつ、ユーザーは快適に PDF を閲覧できます。
ぜひご自身のプロジェクトに合わせて、上記のコード例をカスタマイズしてご活用ください。

コメント