はじめに — 社内ネットワークの壁
社内 PC から 外部 CDN に一切アクセスできない
そんな閉じたネットワークでも「PDF をサクッと画像に変換したい」瞬間はやってきます。
オンライン変換サービスは使えず、バックエンドを立てるのも大げさ。
そこで
「ライブラリを全部ローカル配置して、ブラウザだけで完結する PDF→JPG 変換ツール」
を 10分で 作ったときのメモをまとめました。
いや、使いたいだけやねんの人はこちらへ
https://pdf2jpg.liempia.app/
(cloudflare Pageにてホスティングしています)
使用スタック(すべてローカル)
目的 | ライブラリ | 備考 |
---|---|---|
UI | TailwindCSS 3.4.x (Standalone build) |
tailwind.min.css を libs/ 配下に保存 |
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 お待ちしています!