3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PDFをJPGに変換するWebツールを10分で作った話

Last updated at Posted at 2025-05-23

はじめに — 社内ネットワークの壁

社内 PC から 外部 CDN に一切アクセスできない
そんな閉じたネットワークでも「PDF をサクッと画像に変換したい」瞬間はやってきます。
オンライン変換サービスは使えず、バックエンドを立てるのも大げさ。

そこで
「ライブラリを全部ローカル配置して、ブラウザだけで完結する PDF→JPG 変換ツール」
を 10分で 作ったときのメモをまとめました。

いや、使いたいだけやねんの人はこちらへ
https://pdf2jpg.liempia.app/
(cloudflare Pageにてホスティングしています)

使用スタック(すべてローカル)

目的 ライブラリ 備考
UI TailwindCSS 3.4.x (Standalone build) tailwind.min.csslibs/ 配下に保存
PDF レンダリング pdf.js 2.16.105 (UMD) pdf.min.js / pdf.worker.min.js
画像生成 HTMLCanvas API canvas.toBlob()
ZIP 生成 JSZip 3.10.x jszip.min.js
ファイル保存 FileSaver.js 2.0.x FileSaver.min.js

フォルダ構成

pdf-jpg/
├─ index.html
└─ libs/
   ├─ tailwind.min.css
   ├─ pdf.min.js
   ├─ pdf.worker.min.js
   ├─ jszip.min.js
   └─ FileSaver.min.js

CD に焼いて配布しても USB メモリに入れても、そのままダブルクリックで動きます 🎉

コード

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PDF → JPG Converter</title>
    <script src="libs/tailwindcss.js"></script>
    <script src="libs/pdf.js"></script>
    <script>
      pdfjsLib.GlobalWorkerOptions.workerSrc =
        "libs/pdf.worker.js";
    </script>

    <script src="libs/jszip.min.js"></script>
    <script src="libs/FileSaver.min.js"></script>
  </head>
  <body class="min-h-screen bg-gray-100 flex flex-col items-center p-4">
    <h1 class="text-2xl font-bold mb-4">PDF → JPG コンバータ</h1>
    
    <label
      id="dropzone"
      for="fileInput"
      class="w-full max-w-xl border-2 border-dashed border-gray-400 rounded-2xl p-8 flex flex-col items-center justify-center bg-white cursor-pointer hover:bg-gray-50 transition"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        class="h-12 w-12 mb-3"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
     <path d="M12 5v14M5 12h14"/>

      </svg>
      <p class="text-gray-600">クリックまたはドラッグ&ドロップで PDF を選択</p>
      <input id="fileInput" type="file" accept="application/pdf" class="hidden" />
    </label>

    <button
      id="downloadAllBtn"
      class="mt-6 px-6 py-2 bg-blue-600 text-white rounded-xl shadow disabled:opacity-40"
      disabled
    >
      すべてを ZIP でダウンロード
    </button>

    <section id="thumbs" class="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-3 w-full max-w-6xl"></section>

    <script>
      const fileInput = document.getElementById("fileInput");
      const dropzone = document.getElementById("dropzone");
      const thumbs = document.getElementById("thumbs");
      const downloadAllBtn = document.getElementById("downloadAllBtn");

      let jpgBlobs = [];

      function handleFiles(files) {
        const file = files[0];
        if (!file || file.type !== "application/pdf") return;

        thumbs.innerHTML = "";
        jpgBlobs = [];
        downloadAllBtn.disabled = true;

        const reader = new FileReader();
        reader.onload = async (e) => {
          const typedArray = new Uint8Array(e.target.result);

          const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
          const total = pdf.numPages;

          for (let pageNum = 1; pageNum <= total; pageNum++) {
            const page = await pdf.getPage(pageNum);
            const viewport = page.getViewport({ scale: 2 });
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            canvas.width = viewport.width;
            canvas.height = viewport.height;

            await page.render({ canvasContext: ctx, viewport }).promise;

            canvas.toBlob(
              (blob) => {
                const url = URL.createObjectURL(blob);
                jpgBlobs.push({ name: `page-${pageNum}.jpg`, blob });

                const card = document.createElement("div");
                card.className = "bg-white rounded-xl shadow p-4 flex flex-col items-center";
                const img = document.createElement("img");
                img.src = url;
                img.className = "max-h-52 object-contain mb-2";
                const link = document.createElement("a");
                link.href = url;
                link.download = `page-${pageNum}.jpg`;
                link.textContent = `ページ ${pageNum} を保存`;
                link.className = "text-blue-600 hover:underline";
                card.appendChild(img);
                card.appendChild(link);
                thumbs.appendChild(card);

                if (jpgBlobs.length === total) {
                  downloadAllBtn.disabled = false;
                }
              },
              "image/jpeg",
              0.92 // Jpeg quality
            );
          }
        };
        reader.readAsArrayBuffer(file);
      }

      downloadAllBtn.addEventListener("click", async () => {
        const zip = new JSZip();
        jpgBlobs.forEach(({ name, blob }) => {
          zip.file(name, blob);
        });
        const content = await zip.generateAsync({ type: "blob" });
        saveAs(content, "pdf-pages.zip");
      });

      fileInput.addEventListener("change", (e) => handleFiles(e.target.files));

      ["dragenter", "dragover", "dragleave", "drop"].forEach((evt) => {
        dropzone.addEventListener(evt, (e) => {
          e.preventDefault();
          e.stopPropagation();
        });
      });
      ["dragenter", "dragover"].forEach((evt) => {
        dropzone.addEventListener(evt, () => dropzone.classList.add("bg-gray-200"));
      });
      ["dragleave", "drop"].forEach((evt) => {
        dropzone.addEventListener(evt, () => dropzone.classList.remove("bg-gray-200"));
      });
      dropzone.addEventListener("drop", (e) => handleFiles(e.dataTransfer.files));
    </script>
  </body>
</html>

オフライン環境ならではの利点

セキュリティ

  • ファイルは一切ネットに出ず、情報漏洩リスクゼロ。

速度

  • 読み込みはローカルディスクだけ。キャッシュ要らずで即応答。

配布の簡単さ

  • HTML と libs/ フォルダを渡すだけ。インストール不要。

今後の改善アイデア

  • PNG / WEBP 出力対応
  • ページ範囲指定 UI (1-3,5 など)
  • 複数 PDF バッチ変換
  • Service Worker で PWA 化し、ホーム画面アイコン対応

まとめ

  • オフライン + CDN フリー でも、pdf.js と Canvas だけで PDF→JPG は完結する

  • 10分で MVP を組む鍵は「全部ローカル配置 & UMD ビルド」

  • 社内ネットワークが厳しくても、フロントエンドだけで十分戦える

もし同じ課題を抱えているなら、ぜひリポジトリを clone して即席ツールを体験してみてください。
「PDF を画像にしたいだけ」な小さなニーズにサクッと応える武器になります 🚀

ソースコード
GitHub: https://github.com/liempia/pdf-to-jpg
お気軽に Fork & PR お待ちしています!

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?